Draft.js实现微信emoji功能

1,352 阅读4分钟

最近公司需要在编辑器中增加微信表情包,需要在原有的基础上增加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的支持

image.png 只是选中的时候会有点丑。。。

image.png

常规优化

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