Emoji
在Draftjs plugins中,emoji plugin是基于
EmojiOne
这个库。本项目中是使用的本地图片,通过中文含义与图片一一映射实现。因此,之前emoji在输入框是显示对应的文本。
_emojiMap
为存储emoji id和中文名称映射关系的对象,
onAddEmoji
为添加emoji表情到输入框的方法,根据id返回[name];改为getEmojiById
直接返回对于html字符串
// 添加emoji对应文本
appendEmoji(emojiText) {
console.log('emoji', emojiText);
const editorState = this.state.editorState;
const selection = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const ncs = Modifier.insertText(contentState, selection, emojiText);
const newEditorState = EditorState.push(editorState, ncs, 'insert-fragment');
this.onChange(newEditorState);
}
通过Modifier的insertText
方法将emoji文本插入输入框,但到这里效果也和之前一样,只是显示了表情对应的中文,如[开心]
。
Draftjs提供了修饰器Decorators
这个概念,根据自定义正则去扫描文本快中的内容,然后根据自定义的样式去渲染,可用来高亮文本等。
// 针对emoji的修饰器
const findWithRegex = (regex, contentBlock, callback) => {
const text = contentBlock.getText();
let matchArr;
let start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
};
const draftDecorator = new CompositeDecorator([
{
strategy: (contentBlock, callback, contentState) => {
const emojiRegex = /\[[^\[\]]+\]/g;
findWithRegex(emojiRegex, contentBlock, callback);
},
component: (props) => {
const text = props.decoratedText.trim();
console.log('decorated text', text);
return <span data-offset-key={props.offsetKey}>{props.children}</span>;
},
},
]);
-
坑1:实际将改decorators传进去报错了,因为
Editor
组件是引用的draft-js-plugins-editor
,这是它们的一个bug,issue里给出的解决方案是:import { CompositeDecorator } from 'draft-js'; import MultiDecorator from 'draft-js-plugins-editor/lib/Editor/MultiDecorator'; .... const createDecorator = (...) => { const decorators = ...; return new MultiDecorator([new CompositeDecorator(decorators)]); };
坑2:decorators虽然帮助扫描文本中的表情字符并替换了样式,但替换后不会自动Focus,且无法作为一个字符去选择,不同于标准emoji的做法,都有对应的unicode...
坑3:当选中一段文本再调用Modifier.insertText()方法去插入文本时,会报
Target range must be collapsed for insertText
错误;-
坑4:自定义的decorators注册到输入框后,与mention插件冲突,导致mention失效...
issue: https://github.com/draft-js-plugins/draft-js-plugins/issues/395
const customDecorators = [ { strategy: findLinkEntities, component: Link, }, ]; // Editor accepts a prop called decorators. const MyEditor = ({ editorState, onChange }) => ( <Editor editorState={editorState} onChange={onChange} decorators={customDecorators} plugins={[plugin1, plugin2]} /> );
emoji优化
-
因decorator只是替换了block中对应文本的样式,实际仍未对应字符的占位,而项目目前采用
[xx]
的格式表示一个emoji表情,可以考虑再设计一层map映射,对应一个标准emoji的unicode,这样在插入到输入框时,emoji对应的文本只占一个字符。/** * 映射的emoji unicode范围为\u1f600 - \u1f64f */ module.exports.getEmojiUnicode = function(id) { const i = parseInt(id); const emojiInt = 0x1f600 + i - 1; const code16 = emojiInt.toString(16); const emojiUnicode = unescape('%u' + code16); return emojiUnicode; }; module.exports.getIdByUnicode = function(unicode) { const codeStr = escape(unicode); const code16 = codeStr.split('%u')[1]; const id = parseInt('0x'+ code16) - parseInt(0x1f600); return id + 1; };
首先在网上查找了unicode中emoji主要存在的范围,这里限定了
\u1f600 - \u1f64f
大概80个unicode,对16进制int进行计算
(0x)
,再通过toString(16)
转为16进制的字符串。一开始直接用'\u'.concat(code16)
的方法会报错,因为\u
是转义符,不能直接作为字符串使用。其对应%u
的反转义结果; 参考企业微信PC的做法,在检测到输入框有emoji时,改变了行高,字体大小不变,行高适应emoji大小(TODO)
-
实际做下来,优化1并不适用,最终还是选择再建立一层emoji id和uniocde的映射。
修改emoji的decorator中的正则:
const emojiRegex = /[\u2600-\u2665]/g;
到这里已经基本解决了emoji在Draftjs中的显示问题。但实际输出的文本里,emoji是其对应的unicode,因此还需要在获取输入框内容时,添加对文本的转换:
// 对文本进行正则匹配,替换unicode为对应emoji parseContentWithEmoji(content) { const regex = /[\u2600-\u2665]+/g; const unicodeArr = content.match(regex); if (unicodeArr === null) { return content; } unicodeArr.forEach((unicode) => { const id = emoji.getIdByUnicode(unicode); if (!!id) { const text = emoji.getEmojiText(id); const replaceReg = new RegExp(unicode+ ' ', 'g'); content = content.replace(replaceReg, text); console.log('replace '+ unicode+ 'by '+ text); } }); return content; }
先对文本进行
match
匹配,获取文本中所有需要转换的unicode数组,对数组进行遍历,获取unicode对应的emoji文本,同时使用replace
将unicode替换之; -
实现上述优化后,emoji已能够正常显示到输入框和发送出去,但仍忽略了复制粘贴带有emoji文本的情况, 因为复制得到的emoji对应的是
[xx]
格式,需要在输入框的handlePastedText
事件中添加对包含emoji文本的处理:// 替换emojiText为对应unicode parseContentWithEmojiText(text) { const regex = /\[[^\[\]]+\]+/g; const emojiTextArr = text.match(regex); if (emojiTextArr === null) { return text; } text = text.replace(/\[+/g, ''); text = text.replace(/\]+/g, ''); emojiTextArr.forEach((emojiText) => { const id = emoji.getEmojiIdByText(emojiText); if (!!id) { const unicode = emoji.getEmojiUnicode(id); const replaceText = emojiText.substr(1, emojiText.length-2); const replaceReg = new RegExp(replaceText, 'g'); text = text.replace(replaceReg, unicode +' '); } }); return text; }
-
在实现4时,发现新的问题,当选中输入框中部分文本后再粘贴,
Modifier.insertText()
方法会报错:Target range must be collapsed for `insertText`
查看Api得知这种情况下需使用
Modifier.replaceText()
, 封装输入框添加文本方法如下:// 添加文本内容 appendContent(content, type) { const editorState = this.state.editorState; const selection = editorState.getSelection(); const contentState = editorState.getCurrentContent(); let newContentState = null; // 判断是否有选中,有则替换,无则插入 const selectionEnd = selection.getEndOffset(); const selectionStart = selection.getStartOffset(); if (selectionEnd === selectionStart) { newContentState = Modifier.insertText(contentState, selection, content); } else { newContentState = Modifier.replaceText(contentState, selection, content); } const newEditorState = EditorState.push(editorState, newContentState, type); this.onChange(newEditorState); setTimeout(()=>{ this.focus(); }, 0); }