用 contenteditable 替代富文本实现聊天输入框

背景

最近在用 contenteditable 替换富文本做一个web聊天输入框,但关于这个属性的资料比较少,本文主要是记录下 contenteditable 的用法和遇到的一些问题;

contenteditable 有五个值:events, caret, typing, plaintext-only, true(当然还有一个false);
关于这些值的说明文档几乎找不到,只在一个文档上找到有一些描述:

Meaning of states
The states "events", "caret", "typing", "plaintext-only" and "true" are hierarchically ordered, so that the state "caret" also includes the features of the "events" state, the "typing" state includes the features of the "caret" state, the "plaintext-only" state includes the features of the "typing" state, and the "true" state includes all the features of the "plaintext-only" state.

The "events" state means that beforeinput events are triggered when the user asks for an editing operation. The "caret" state adds default browser controlled movement of the caret. The "typing" state adds handling of text input through IME and keyboard, and deletion within an IME composition. The "plaintext-only" state adds handling of text deletion within a text node. The "true" state adds handling of deletion deletion of non-textual content and editing commands through the execCommand command.

The states "events", "caret", "typing" and "plaintext-only" are defined in this document.

The state "true" is currently not well-defined and its usage is discouraged. An initial attempt has been made to specify the behavior of the "true" state in the contentEditable=True spec.

大概意思就是这些值是有依序包含关系的:
1、events: input event的事件绑定,e.g. change, input, focus, etc.
2、caret: 光标控制,包含events;
3、typing: 实现了输入框的功能(input), 包含 caret;
4、plaintext-only: 纯文本内容输入(text), 包含 typing;
5、true: 超文本内容输入(non-textual),另外还可以通过 execCommand 来操作里面的元素(execCommand 已废弃), 包含 plaintext-only;

前面三个只是 input 的一些特性,应该没有使用场景。只需要考虑 plaintext-only 和 true 即可;

方案选择

这里的方案选择是指选择 plaintext-only 还是 true 值来实现;

方案一: plaintext-only
复制黏贴的文案会保留换行空白效果,例如如果是 div/p 等块级元素,会在提取text内容后自动给你添加 \n(linux: \r\n, windows: \n, macOS: \r),不需要再考虑空白和换行的问题; 但如果拷贝的是图片,需要监听 paste 事件,将图片对象处理后 innerHtml 到正确位置上(光标位置);

方案二: true
需要将除了 img 标签外的所有标签进行处理(删除标签,提取内容,并区分是否需要替换为 br 标签),这个转换非常繁琐,目前暂时找不到合适的工具,只能自己写一堆的正则实现。
处理完成后再将内容插入到正确的位置上;

在我的使用场景上,选择方案一会简单很多,所以本文的实践是基于方案一实现的。

图片处理

监听 paste 事件,如果是图片文件,则进行处理后(我的场景下需要将图片上传到自己的静态资源服务器上,这里转成base64),再插入到 contenteditable 上:

// <div class="input" contenteditable="plaintext-only"></div>
const inputEle = document.querySelector('.input')

inputEle.addEventListener('paste', async(event) => {
  const items = event.clipboardData && event.clipboardData.items

  for(let i = 0, _len = items.length; i < _len; i += 1) {
    let _file = await toBase64(items[i].getAsFile())
    
    inputEle.innerHTML = `<img src="${_file}" >` // 后续 光标 再讲下如何插入准确位置
  }
})
function toBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()

    reader.readAsDataURL(file)
    reader.onload = () => resolve(reader.result)
    reader.onerror = () => reject(reader.error)
  })
}

其实表情同理,这里不展开了;

光标处理

通过 getSelection(Selection) 获取文本范围,再通过 getRangeAt(Range) 获取光标位置,通过 Range 对象的 insertNode 方法插入 dom:

// <div class="input" contenteditable="plaintext-only"></div>
const inputEle = document.querySelector('.input')

inputEle.addEventListener('paste', async(event) => {
  const items = event.clipboardData && event.clipboardData.items

  for(let i = 0, _len = items.length; i < _len; i += 1) {
    let _file = await toBase64(items[i].getAsFile())
    
    let imgNode = document.createElement('img') // 创建 img node 节点
    let selection = window.getSelection() // 获取文本范围
    let selRange = selection.getRangeAt(0) // 获取光标位置
    
    imgNode.src = _file
    selRange.insertNode(imgNode) // 插入元素
    selection.removeAllRanges() // 重置光标
  }
})
function toBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()

    reader.readAsDataURL(file)
    reader.onload = () => resolve(reader.result)
    reader.onerror = () => reject(reader.error)
  })
}

最后获取 contenteditable 的 innerHTML 内容提交到服务器即可。

参考文档

1、兼容性查询: developer.mozilla.org/zh-CN/docs/…
2、Element: paste 事件: developer.mozilla.org/zh-CN/docs/…
3、小tip: 如何让contenteditable元素只能输入纯文本: www.zhangxinxu.com/wordpress/2…

猜你喜欢

转载自juejin.im/post/7115351390360174629
今日推荐