我们如何开发在Messenger(Gem4me)中输入新消息的领域

大家好!



我叫Alexander Baltsevich,我在Gem4me项目的网络团队中担任领导职务。该项目是每个人和每个人的创新信使(到目前为止,我一直幻想着,但是我们为此努力;-))


简要介绍一下网络版本:ReactJS(对此会产生疑问的人)+ mobX(个人而言,我一点也不热心,但我们不打算迁移到任何地方;如果您对这些细节感兴趣,那么到底什么不适合您,请写评论-也许我会另写一篇文章)+故事书+ wdio(屏幕截图测试)。


Messenger最初是一个用于交换短消息的程序(然后才进行所有这些电话,贴纸和其他会议)。消息是应该正常工作的基本功能。不,不是那样的:它应该可以完美运行。而且只有至少一次经历了此功能开发的人员才知道必须克服的所有痛苦,以便使一切看起来都美好。我们都和团队一起经历了这一过程。我们决定分享。




要求


输入字段应提供哪些功能?


  • 打印文本很陈旧,可以多行书写(通过按shift + Enter)。高级用户还习惯于使用降价字符来快速格式化消息。



  • 从缓冲区粘贴文本。通常在复制文本时,还会复制此文本下的背景(请参见下面的示例)并进行格式设置。但是干净,整洁的文本以及其他内容都不应进入输入字段。



  • 从剪贴板插入图像。在我看来,没有这个功能,谁也无法生存。
  • 表情符号=)早已成为交流文化的一部分。因此,您不能忘记微笑。在本文的开头,在中间,从内部库插入或从另一资源复制的三个单词连续十次或一次十次-不管有什么变化和变化,都应始终加五个以加号表示。
  • , , ( ). — , , . , , . 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