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