如何修改富文本编辑器的文本内容
English Version
最近,一些用户给PromptSnippets (opens in a new tab)插件提了一个需求,希望适配bard, claude等新兴生成式AI网站。而 PromptSnippets 当时仅支持基于 input、textarea 等简单输入框的网站,例如 chatgpt、poe。与此不同的是,bard 等网站的输入框采用了基于 contenteditable 的富文本编辑框,因此,插件无法简单地修改富文本编辑框中的文本内容。
为了适配各种网站的文本编辑框,我调研了市面上流行的网站和富文本编辑器技术方案,并找到了一种通用的文本内容管理方案,适用于主流富文本编辑器。
本文将介绍主流富文本编辑器如何管理内部文本内容状态,以及我如何通过浏览器插件来修改其状态。
富文本编辑器中的文本内容状态管理方案
方案一:同步HTML
一种简单的文本内容状态管理方案是自动同步HTML,不依赖或不绝对依赖内部的状态管理。采用这种方案的富文本编辑器使用MutationObserver的方法监听HTML的改动,并自动同步文本内容。
这类编辑器包括 ProseMirror、Quill 等,一些网站也简单地采用了这一方案,例如 Bard。
面对这类编辑器,插件直接修改HTML内容即可。
方案二:自控
另一种主流的方案是自己内部维护文本内容状态,通过监听 beforeinput
, input
文本输入事件来维护文本内容状态。
这类编辑器的典型特征是修改 HTML 并不会改变最终的文本内容,有些甚至可能导致编辑器崩溃。
采用这种方案的编辑器包括 Slate、Lexical 等。
对这类编辑器的文本内容进行修改较为复杂,需要模拟完整的输入事件流程:
先选择文本插入位置(点击鼠标或者脱选),然后输入文字触发beforeinput和input事件。
通用修改文本代码
针对上述两种编辑器类型,我总结出一种通用的修改文本的方法:先模拟输入流程,如果修改失败,再直接修改HTML。具体如下:
- 使用MutationObserver监听DOM改动(用于后续判断模拟输入是否成功)
- 设置选择区域包含当前所有文本内容
- 触发beforeinput和input事件
- 如果此时DOM没有改动就直接修改HTML
async function setElementValue(element: ContentEditableElement, value: string) {
let changed = false
const clear = setElementChangeListener(element, () => {
changed = true
})
// 1. simulate input event flow
// first, select all
const range = document.createRange()
range.selectNodeContents(element)
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
await new Promise((resolve) => setTimeout(resolve, 20))
// then dispatch beforeinput event
const beforeInputEvent = new InputEvent("beforeinput", {
bubbles: true,
cancelable: true,
inputType: "insertText",
data: value,
})
element.dispatchEvent(beforeInputEvent)
// then input event
if (!beforeInputEvent.defaultPrevented) {
const inputEvent = new InputEvent("input", {
bubbles: true,
cancelable: true,
inputType: "insertText",
data: value,
})
element.dispatchEvent(inputEvent)
}
await new Promise((resolve) => setTimeout(resolve, 20))
// 2. if not changed, then change html directly
if (!changed) {
const p = document.createElement("p")
p.textContent = value
element.innerHTML = p.outerHTML
}
clear()
}
通过上述方法,PromptSnippets (opens in a new tab)适配了所有富文本编辑器,几乎可以在任何网站上使用它了。