最近公司需要在编辑器中增加微信表情包,需要在原有的基础上增加emoji表情包,特此写下文章记录emoji的艰辛历程。TT
主要实现思路
1.建立Map映射
由于微信emoji不是一般的emoji,没有对应的unicode,而且在发送的时候也不可能直接发送图片过去,发现微信发送表情包可以通过[xx]的形式发送对应的微信表情包,所以实现思路就是建立[xxx]=>微信icon的映射表,在编辑器里面显示图片,当发送的时候则是转换为[xx]的形式发送。但是因为emoji只占一个字符,所以[xx]这种显示方式会出现问题,所以还需要一层[xx] => unicode的转换。 unicode我是随便找的,从\u2400开始,根据图片的长度生成对应的unicode
2.插入编辑器中
当选择表情包之后,插入到编辑器中
const insertText =
(emoji: string) => {
if (disabled) return
const selection = editorState.getSelection()
const contentState = editorState.getCurrentContent()
// 判断是否有选中,有则替换,无则插入
const selectionEnd = selection.getEndOffset()
const selectionStart = selection.getStartOffset()
let ncs: ContentState
if (selectionStart === selectionEnd) {
ncs = Modifier.insertText(contentState, selection, emoji)
} else {
ncs = Modifier.replaceText(contentState, selection, emoji)
}
const newEditorState = EditorState.push(editorState, ncs, 'insert-characters')
onStateChange(newEditorState)
}
3.通过draft提供的装饰器,实现对于特定字符串的转换,渲染对应的组件
// 针对emoji的修饰器
const findWithRegex = (regex: RegExp, contentBlock: ContentBlock, callback: (start: number, end: number) => void) => {
const text = contentBlock.getText()
let matchArr
let start
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index as number
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
callback(start, start + matchArr[0].length)
}
}
export const emojiDecorator = [
{
strategy: (contentBlock, callback, contentState) => {
const startUnicode = unescape(`%u${startUnicodeIdx}`)
const endUnicode = unescape(`%u${startUnicodeIdx + emojiJson.length}`)
const emojiRegex = new RegExp(`[${startUnicode}-${endUnicode}]`, 'g')
findWithRegex(emojiRegex, contentBlock, callback)
},
component: (props) => {
const text = props.decoratedText.trim()
const id = emojiUnicodeToIdMap[text]
const icon = emojiIdToIconMap[id]
if (icon) {
return (
<span
data-offset-key={props.offsetKey}
className="wx-texteditor-emoji-icon"
style={{ backgroundImage: `url(${icon as string})` }}
>
{text}
</span>
)
}
},
},
]
到此大致的实现思路就是这样,下面总结下在做的过程中遇到的坑。
遇到的坑
1.Decorator中使用img标签
刚开始的时候实现思路里面是span里面直接使用img表情的,碰到对应的id则转换成icon进行加载,显然是没有问题,但是当想在后面进行添加的时候就会出现无法选中,不能删除,只能在前面进行输入等一些列的问题,找了半天无果,github上面也有个是增加对inline-block的支持issue,但是没有相应的解决办法。只能放弃了。
2.使用块结构来进行渲染
在img 内联方法不行的情况下,尝试使用entity和AtomicBlockUtils来创建一个块状结构,通过blockRenderFn来进行选择type为atomic的类型从而渲染对应的组件,但是这时候会有个问题,就是会另起一行,而不能和前面的内容关联在一起。试着使用blockStyleFn来对div增加className,想全部改成inline-block类型,但是随之也出现了回车没办法回车的问题。
在搜索问题的时候出现的意外惊喜
最终解决方案
在搜索解决方案的时候,找到了Draft插件的emoji,发现里面有一种emoji是通过给span background-image来改变背景实现的。里面进行了一点点的改动
.wx-texteditor-emoji-icon {
display: inline-block;
overflow: hidden;
width: 20px;
height: 20px;
margin: -.2ex 0 .2ex;
color: transparent;
vertical-align: middle;
background-position: 50%;
background-repeat: no-repeat;
background-size: contain;
line-height: inherit;
}
终于实现了编辑器对于emoji的支持
只是选中的时候会有点丑。。。
常规优化
1. 复制剪切初始化获取值的时候需要更改unicode为对应的[xx]
修改剪切板navigator的API,参考的是阮一峰老师的文章剪贴板操作 Clipboard API 教程
这里需要注意的就是剪切的onCut方法需要先调用官方的editOnCut,在进行我们的逻辑,否则会报错
import editOnCut from 'draft-js/lib/editOnCut'
const handleCut = async (editor, e) => {
// 需要先调用默认事件,不然会报错
editOnCut(editor, e)
try {
const text = await navigator.clipboard.readText()
const newText = emojiUnicodeToId(text)
navigator.clipboard.writeText(newText)
} catch (e) {
console.log('剪切失败', e)
}
}
2. 粘贴的时候需要将[xxx]转换为对应的unicode
3. 保存到服务器上的时候需要转换unicode => [xxx]
4. 选中的时候插入emoji,insertText Api会报错 需要判断是否选中,若选中则使用replaceText Api
至此,艰难的draft-wx-emoji终于完成了,没找到background-url方法之前真的是想重构改用slate.js了T T