基于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');
    }
  }

<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)}
/>    

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容