Bagaimana kami mengembangkan bidang untuk memasukkan pesan baru di messenger kami (Gem4me)

Halo semuanya!



Nama saya Alexander Baltsevich, saya bekerja pada posisi kepemimpinan tim Web proyek Gem4me. Proyek ini adalah pembawa pesan yang inovatif untuk semua orang dan semua orang (sejauh ini dalam fantasiku, tapi kami berusaha keras untuk ini ;-))


Secara singkat tentang tumpukan versi web: ReactJS (yang akan meragukannya) + mobX (secara pribadi, saya sama sekali tidak antusias, tetapi kami tidak berencana untuk bermigrasi ke mana pun; jika Anda tertarik pada detailnya, apa yang sebenarnya tidak cocok untuk Anda, tulis komentar - mungkin saya akan melakukan artikel terpisah tentang hal itu) + buku cerita + wdio (pengujian tangkapan layar).


Utusan utamanya adalah program untuk bertukar pesan singkat (dan hanya dengan demikian semua panggilan, stiker, dan konferensi lainnya). Pesan adalah fungsi dasar yang harus berfungsi dengan baik. Tidak, tidak seperti itu: itu harus bekerja dengan sempurna. Dan hanya orang yang setidaknya sekali mengalami pengembangan fungsi ini yang tahu semua rasa sakit yang harus diatasi agar semuanya terlihat indah. Kita semua melewati ini dengan tim. Dan kami memutuskan untuk berbagi.




Persyaratan


Fitur apa yang harus disediakan bidang input?


  • Itu basi untuk mencetak teks, dengan kemampuan untuk menulis dalam beberapa baris (dengan menekan shift + Enter). Pengguna mahir juga terbiasa menggunakan karakter penurunan harga untuk memformat pesan dengan cepat.



  • Rekatkan teks dari buffer. Seringkali saat menyalin teks, latar belakang di bawah teks ini juga disalin (lihat contoh di bawah) dan pemformatan. Tapi teks yang bersih dan rapi dan tidak ada lagi yang harus masuk ke kolom input.



  • Masukkan gambar dari clipboard. Sepertinya saya tidak ada yang bisa hidup tanpa fitur ini.
  • Smilies =) telah lama menjadi semacam bagian dari budaya komunikasi. Karenanya, Anda tidak bisa melupakan senyum. Di awal teks, di bagian tengah, sepuluh lembar berturut-turut atau satu demi satu melalui tiga kata yang dimasukkan dari perpustakaan internal atau disalin dari sumber lain - dalam variasi dan variasi apa pun, harus selalu diambil dalam lima dengan nilai tambah.
  • , , ( ). β€” , , . , , . Esc ( ).



" " ("mention") , . :


  • β€œ@β€œ β€œ ”. , :



  • "@" ("@T" => "T");
  • β€œ ” , , "@Tes", ("@Te|s"), . ;
  • , " @ ?", "@" ("| @ ?") "@" (" @ | ?"), β€œ ” ;
  • , (" @ ?|") β€” , (" @| ?") β€” , "@" ;
  • , ("@| "), " " . β€” , . , , (β€œ@ | ”).

, .



, , , β€” :


  • , , . , , .
  • . , - β€” ( ), β€” - . , . , .

, , β€” , .., , , .


!


HTML


, HTML . , , , , , , input, textarea , .


input , , .


textarea β€” " ", . , (. ). input, shift + Enter . 5 , . textarea , , .






, textarea, , , div. , , , ! contenteditable.


The contenteditable global attribute is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing. (MDN)

, , .


:


<div
  className={styles.input}
  placeholder="Type a message"
  contentEditable
/>


, β€” , . input onChange. div onInput, , . addEventListener , . :


class Input extends Component {
    setRef = (ref) => {
        this.ref = ref;
    };
    saveInputValue = () => {
        const text = this.ref.innerText;
        this.props.onChange(text);
    };
    componentDidMount() {
        this.ref.addEventListener('input', this.saveInputValue);
    }
    componentWillUnmount() {
        this.ref.removeEventListener('input', this.saveInputValue);
    }
    render() {
        return (
            <div
                className={styles.input}
                ref={this.setRef}
                placeholder="Type a message"
                contentEditable
            />
        );
    }
}

input ref innerText . div uncontrolled, .. , .



β€” . , Enter. Enter . . input keydown. :


onKeyDown = (event) => {
  if (event.keyCode === ENTER_KEY_CODE && event.shiftKey === false) {
    event.stopPropagation();
    event.preventDefault();

    this.props.submit();
  }
};
componentDidMount() {
  this.ref.addEventListener('keydown', this.onKeyDown);
  ...
}

event.preventDefault() Enter, event.stopPropogation() . , Shift+Enter .


-


β€” - . - . paste.


handlePaste = (event) => {
  event.preventDefault();

  const text = event.clipboardData.getData('text/plain');
  document.execCommand('insertText', false, text);
};
componentDidMount() {
  ...
  this.ref.addEventListener('paste', this.handlePaste);
}

event.preventDefault() , ( event.clipboardData === DataTransfer). , β€” . document.execCommand('insertText', false, text). , .. , .


. . fileList, β€” , , :


handlePaste = (event) => {
  event.preventDefault();

  if (event.clipboardData.files.length > 0) {
    // ...
  }

  const text = event.clipboardData.getData('text/plain');
  document.execCommand('insertText', false, text);
};

, .. , . .


Emoji


emoji . unicode emoji emoji . Emoji β€” , β€” execComand(β€˜insertText’). , , . ! , Emoji, , .




, , this.ref.focus() , :


insertEmoji = (emoji) => {
  if (this.ref) {
    this.ref.focus();
  }
  document.execCommand('insertText', false, emoji);
}

, : , Emoji, β€” . , - . , Emoji , , . .


, , .



API β€” Selection (MDN).


A Selection object represents the range of text selected by the user or the current position of the caret. To obtain a Selection object for examination or manipulation, call window.getSelection().

window.getSelection() , , , . , , β€” . API . , . :




gem4me, "m" "e". selection selection.anchorNode ( ) selection.anchorOffset (5). , selection.anchorNode , , , . , , β€” . :


updateCaretPosition = () => {
  const { node, cursorPosition } = this.state;

  const selection = window.getSelection();
  const newNode = selection.anchorNode;
  const newCursorPosition = selection.anchorOffset;

  if ( node === newNode && cursorPosition === newCursorPosition) {
    return;
  }

  this.setState({ node, cursorPosition });
}

, . , :


β€” onInput, , . paste , .


onInput. , β€” . β€” , keyup ( keydown β€” , , ). , 2 ( , input keyup). state, . , .. , =).


. , ? click .


. -!


Emoji


, , Emoji. API Range, Selection.


The Range interface represents a fragment of a document that can contain nodes and parts of text nodes

, . :


const { node, cursorPosition } = this.state;

const range = document.createRange();
range.setStart(node, cursorPosition);
range.setEnd(node, cursorPosition); 


 , . β€” . :


const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);

selection, , . this.ref.focus() ! , , :


document.execCommand('insertText', false, emoji);

:


customFocus = () => {
  const { node, cursorPosition } = this.state;

  const range = document.createRange();
  range.setStart(node, cursorPosition);
  range.setEnd(node, cursorPosition);

  const selection = window.getSelection();
  selection.removeAllRanges();
  selection.addRange(range);
}

insertEmoji = (emoji) => {
  this.customFocus();
  document.execCommand('insertText', false, emoji);
};


, , . :


, , ( ). β€” , , . , , . Esc ( ).

, . keydown. , . :


if (
  event.keyCode === UP_ARROW_KEY_CODE &&
  this.props.isTextEmpty &&
  this.props.mode === INPUT_CONTEXT_TYPES.STANDARD
) {
  …      
}

, , . :


if (
  event.keyCode === UP_ARROW_KEY_CODE &&
  this.props.isTextEmpty &&
  this.props.mode === INPUT_CONTEXT_TYPES.STANDARD
) {
  event.preventDefault();
  event.stopPropagation();

  this.props.setMode('edit');
  document.execCommand('insertText', false, this.props.lastMessage); 
}

document.execCommand('insertText') , .


Esc . keydown . this.ref.textContent = "".



, , . β€” , .. , . , "@". updateCaretPosition:


updateAndProcessCaretPosition = () => {
  const { node, cursorPosition } = this.state;

  const selection = window.getSelection();
  const newNode = selection.anchorNode;
  const newCursorPosition = selection.anchorOffset;

  if (node === newNode && cursorPosition === newCursorPosition) {
    return;
  }

  if (this.props.isAvailableMention) {
    this.props.parseMention(node.textContent, cursorPosition);
  };

  this.setState({ node, cursorPosition });
}

isAvailableMention , . , true. :


parseMention = (text, cursorPosition) => {
  if (text && cursorPosition && text.includes('@')) {
    const lastWord = text
      .substring(0, cursorPosition)
      .split(' ')
      .pop();

    if (lastWord[0] === '@') {
      this.setFilter(lastWord.substring(1, cursorPosition));
      return;
    }
  }
  this.clearFilter();
};

, "@" . β€” , , "@", . , .


insertMention = (insertRestPieceOfMention) => {
  this.customFocus();
  document.execCommand('insertText', false, insertRestPieceOfMention + ' ');
};

, document.execCommand('insertText') : , , .



, , , , , 20+ , , . , - . β€” , , (, , , , , - ), , , , . β€” , .


, Gem4me


All Articles