美文网首页我爱编程
基于Draftjs实现的Electron富文本聊天输入框(五)

基于Draftjs实现的Electron富文本聊天输入框(五)

作者: charles0427 | 来源:发表于2018-04-13 16:18 被阅读472次

    虽然draftjs是个Facebook推出的较为成熟的开源项目,但毕竟实际运用时的需求很多样,而且引用了draft-js-mention-plugin这样的插件。开发过程中,遇到不少问题。

    输入框组件移除再切换回来,decorators和plugins不起作用,移除当前会话后重新打开则正常

    分析:受影响的是draft plugins库提供的plugins和decorators两个属性,draftjs提供的功能均正常;因此主要查看draft plugin的issue。

    issue里有很多相关但都没有准确解决方案的问题,https://github.com/draft-js-plugins/draft-js-plugins/issues/251

    draft plugin的Editor是在componentWillMount的方法里去注册decorators,issue里提到注册的时候需要正确的EditorState。修改如下:

    let initialDraft = null;
    
    // 根据issue, 这里需重设editorState,否则出现decorators失效问题
    componentWillMount() {
        if (!!this.props.draft) {
          initialDraft = this.props.draft;
          this.setStateWithDraft(this.props.draft);
        } else {
          this.setEmptyState();
        }
      }
    
    componentWillReceiveProps(nextProps) {
        .......
        
        if (this.props.draft && nextProps.draft && initialDraft !== nextProps.draft) {
          this.setStateWithDraft(nextProps.draft);
        }
      }
    
    

    在自定义的Editor组件里添加了对componentWillMount方法的重写,通过EditorState.push去重设editorState。同时记录了Mount时的draft值,避免在componentWillReceiveProps重复赋值。

    问题是解决了,但原因并不是很清楚

    删除换行,在输入框显示正常,但发送出去仍有'\r'

    Draftjs在执行删除换行时,虽然解析出来的text显示正常,但通过escape解析发现其text中原换行处仍有\r,即使在Draft输入框显示是正常的。

    在Draftjs输入框中,\r\n才表现出换行,因此发送事件在获取输入框内容时,可以全局替换\r符,在消息显示处,无论有\r或\n都能表现出换行。替换后并不影响正常换行的样式

    输入框滚轮不能保证光标位于可视区域

    分析

    draftjs项目有相关issue,当输入框添加过decorator时,滚轮无法随内容auto scroll。

    我们输入框的滚轮由draftEditor外面的div控制,因此,可以通过在输入框手动更新state的回调中去手动控制滚轮位置

    方案

    参考slate源码,主要通过selection的相关API拿到光标在输入框中的相关位置:

    setTimeout(()=> {
            this.focus();
            if (this.editor && this.container) {
              const editor = this.editor.editor.refs.editor;
              const selection = window.getSelection();
    
              if (selection && editor.scrollHeight > this.container.clientHeight) {
                const range = selection.getRangeAt(0).cloneRange();
                const rangeRect = range.getBoundingClientRect();
                const containerRect = this.container.getBoundingClientRect();
                console.log('editor rect', rangeRect, containerRect);
    
                if (rangeRect.top === 0) {
                  this.container.scrollTop = editor.scrollHeight;
                } else {
                  this.container.scrollTop += rangeRect.top - containerRect.top - rangeRect.height;
                }
              }
            }
          }, 100);
    

    selection.getRangeAt(0).cloneRange().getBoundingClientRect()拿到光标所在节点的clientRect值。

    setTimeOut()是考虑到插入图片时,有个改变图片大小的过程。

    mention插件导致输入框上下键无反应

    分析

    draft-js-mention-plugin源码中,mentionSuggestion组件对上下键做了监听处理,并且通过e.preventDefault()禁止了正常上下键事件。按道理来说,没有@字符触发时,该组件不存在,也不会触发事件监听。然而,初始化时,即使没有@,其组件也存在,只是没有显示出来。这似乎是另外一个bug

    解决

    issue中有相关问题:https://github.com/draft-js-plugins/draft-js-plugins/pull/1002

    但并没有完全解决我们的问题

    //node-modules/draft-js-mention-plugin/lib/MentionSuggestions/index.js
    
    onDownArrow = (keyboardEvent) => {
        if (!this.state.isActive || document.getElementsByClassName('mention-suggestion').length === 0) return;
        keyboardEvent.preventDefault();
        const newIndex = this.state.focusedOptionIndex + 1;
        this.onMentionFocus(newIndex >= this.props.suggestions.size ? 0 : newIndex);
      };
    
    onTab = (keyboardEvent) => {
        if (!this.state.isActive ) return;
        keyboardEvent.preventDefault();
        this.commitSelection();
      };
    
    onUpArrow = (keyboardEvent) => {
        if (!this.state.isActive || document.getElementsByClassName('mention-suggestion').length === 0) return;
        keyboardEvent.preventDefault();
        if (this.props.suggestions.size > 0) {
          const newIndex = this.state.focusedOptionIndex - 1;
          this.onMentionFocus(newIndex < 0 ? this.props.suggestions.size - 1 : newIndex);
        }
      };
    

    输入框初始化就会触发mentionSuggestions的openDropdown()方法,导致isActive的值为true,直接通过isActive无法进行控制,后使用this.props.store.getIsOpened()的值判断是否弹出了mention列表。

    后解决如下:

    onDownArrow = (keyboardEvent) => {
        if (!this.props.store.getIsOpened()) return;
        keyboardEvent.preventDefault();
        const newIndex = this.state.focusedOptionIndex + 1;
        this.onMentionFocus(newIndex >= this.props.suggestions.size ? 0 : newIndex);
      };
    
    onTab = (keyboardEvent) => {
        if (!this.props.store.getIsOpened()) return;
        keyboardEvent.preventDefault();
        this.commitSelection();
      };
    
    onUpArrow = (keyboardEvent) => {
        if (!this.props.store.getIsOpened()) return;
        keyboardEvent.preventDefault();
        if (this.props.suggestions.size > 0) {
          const newIndex = this.state.focusedOptionIndex - 1;
          this.onMentionFocus(newIndex < 0 ? this.props.suggestions.size - 1 : newIndex);
        }
      };
    

    @后面紧跟非空白字符时,选择后的@文本会取代后面的所有文本

    分析

    定位相关逻辑:

    // draft-js-mention-plugin/src/modifiers/addMention.js
    const addMention = (editorState, mention, mentionPrefix, mentionTrigger, entityMutability) => {
      ......
    
      const currentSelectionState = editorState.getSelection();
      const { begin, end } = getSearchText(editorState, currentSelectionState, mentionTrigger);
    
      // get selection of the @mention search text
      const mentionTextSelection = currentSelectionState.merge({
        anchorOffset: begin,
        focusOffset: end,
      });
    
      let mentionReplacedContent = Modifier.replaceText(
        editorState.getCurrentContent(),
        mentionTextSelection,
        `${mentionPrefix}${mention.name}`,
        null, // no inline style needed
        entityKey
      );
    
      // If the mention is inserted at the end, a space is appended right after for
      // a smooth writing experience.
      const blockKey = mentionTextSelection.getAnchorKey();
      const blockSize = editorState.getCurrentContent().getBlockForKey(blockKey).getLength();
      if (blockSize === end) {
        mentionReplacedContent = Modifier.insertText(
          mentionReplacedContent,
          mentionReplacedContent.getSelectionAfter(),
          ' ',
        );
      }
    ......
    };
    

    上述代码执行用户选择了@文本后,mention插件将该文本插入到当前editorState的过程。debug发现,当@后面紧跟非空白符时,currentSelectionState覆盖的范围从@直到整个文本结尾,而不仅仅是@字符..因此在Modifier.replaceText 后,@文本替换了整个文本。

    解决

    虽然问题分析下来,原因似乎出在selectionState上,但这并不好改,也不应该改..毕竟,我们本来就不应该让@后面紧跟非空白符时也触发,于是,查看mention插件的trigger逻辑:

    // draft-js-mention-plugin/src/mentionSuggestionsStrategy.js
    
    import findWithRegex from 'find-with-regex';
    import escapeRegExp from 'lodash.escaperegexp';
    
    export default (trigger: string, regExp: string) => (contentBlock: Object, callback: Function) => {
      const reg = new RegExp(String.raw({
        raw: `(\\s|^)${escapeRegExp(trigger)}${regExp}` // eslint-disable-line no-useless-escape
      }), 'g');
      findWithRegex(reg, contentBlock, callback);
    };
    

    trigger和regExp是我们传进来的,我们只用到trigger:@,regExp没用到就忽略了..即正则为:/(\s|^)@/g,匹配的是以@开头或以空白符+@开始,那为了满足我们的需求,修改为/(\s|^)@(\s|$)/g,对应源码:

    const reg = new RegExp(String.raw({
        raw: `(\\s|^)${escapeRegExp(trigger)}${regExp}$(\\s|$)` // eslint-disable-line no-useless-escape
      }), 'g');
    

    tab键问题

    分析

    draftjs只在UL/OL中对tab键做了处理,其他情况下tab键触发浏览器默认行为,即focus到页面中下一个输入框或可focus的元素。同时其提供的API:keyBindingFn也监听不到tab键。

    解决

    在draftEditor外层的div中添加onKeyDown监听:

    handleKeyEvent(e) {
        if (e.keyCode === 9) {
          // 插入tab制表符
          e.preventDefault();
          this.appendContent('\t', 'insert-characters');
        }
      }
    
    <div
      onKeyDown={this.handleKeyEvent.bind(this)}>
    </div>
    

    拖动txt文件至输入框问题

    分析

    在拖动txt格式文件至输入框时,文件的内容被读取和插入到输入框中。Drag相关事件我们是在父组件上处理的,封装的Draft组件本身没有对drag做什么处理,也没有相关props。因此,分析是draft本身的处理机制导致。

    // draft-js/src/component/handlers/drag/DraftEditorDragHandler.js
    /**
     * Handle data being dropped.
     */
      onDrop: function(editor: DraftEditor, e: Object): void {
        const data = new DataTransfer(e.nativeEvent.dataTransfer);
    
        const editorState: EditorState = editor._latestEditorState;
        const dropSelection: ?SelectionState = getSelectionForEvent(
          e.nativeEvent,
          editorState,
        );
    
        e.preventDefault();
        editor.exitCurrentMode();
    
        if (dropSelection == null) {
          return;
        }
    
        const files = data.getFiles();
        if (files.length > 0) {
          if (
            editor.props.handleDroppedFiles &&
            isEventHandled(editor.props.handleDroppedFiles(dropSelection, files))
          ) {
            return;
          }
    
          getTextContentFromFiles(files, fileText => {
            fileText &&
              editor.update(
                insertTextAtSelection(editorState, dropSelection, fileText),
              );
          });
          return;
        }
    
    

    上述代码是draft对拖入文件的处理部分,getTextContentFromFiles是其内部封装的读取文件内容方法,当读取到文本内容时,draft会将其插入输入框光标处。

    为了避免走到这段逻辑,需要editor.props.handleDroppedFile && isEventHandled(editor.props.handleDroppedFiles(dropSelection, files))

    解决

    添加prop方法handleDroppedFiles并且返回handled;

    handleDroppedFiles(selection, files) {
        if (files) {
          // 不进入draft的默认逻辑
          return 'handled';
        }
      }
      
    <Editor
        handleDroppedFiles={this.handleDroppedFiles.bind(this)}
    />    
    

    相关文章

      网友评论

        本文标题:基于Draftjs实现的Electron富文本聊天输入框(五)

        本文链接:https://www.haomeiwen.com/subject/fbzckftx.html