基于Draftjs实现的Electron富文本聊天输入框(五) —— 问题总结与解决

虽然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');
    }
  }

拖动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';
    }
  }
  
    

你可能感兴趣的:(基于Draftjs实现的Electron富文本聊天输入框(五) —— 问题总结与解决)