如何在Lexical编辑器中实现修订追踪(Track Changes)功能?
在Lexical中实现修订追踪(Track Changes)的正确方式
问题根源
你之前用registerUpdateListener事后对比全文本再修改编辑器状态的方式,会额外触发一次更新,直接污染历史栈;而且通过全文本对比定位变更的逻辑精度不足,没法精准对应到具体的插入/删除位置。
正确实现思路
Lexical的修订追踪应该在变更发生时直接处理,利用自定义Mark节点和Mutation监听来实现,同时避免干扰编辑器的正常历史记录。
步骤1:定义自定义Mark节点
先创建对应插入、删除状态的Mark节点,绑定需求的CSS类:
class InsertedMark extends MarkNode { static getType() { return 'inserted'; } static clone(node) { return new InsertedMark(node.__key); } createDOM() { const dom = document.createElement('span'); dom.className = 'inserted'; dom.style.textDecoration = 'underline'; dom.style.textDecorationColor = 'green'; return dom; } static importDOM() { return { span: (domNode) => { if (domNode.className === 'inserted') { return { node: $createInsertedMark() }; } return null; }, }; } } class DeletedMark extends MarkNode { static getType() { return 'deleted'; } static clone(node) { return new DeletedMark(node.__key); } createDOM() { const dom = document.createElement('span'); dom.className = 'deleted'; dom.style.textDecoration = 'line-through'; dom.style.textDecorationColor = 'red'; return dom; } static importDOM() { return { span: (domNode) => { if (domNode.className === 'deleted') { return { node: $createDeletedMark() }; } return null; }, }; } } // 注册自定义节点 function registerCustomMarks(editor) { editor.registerNode(InsertedMark); editor.registerNode(DeletedMark); }
步骤2:用Mutation监听处理变更
通过registerMutationListener精准捕获文本节点的创建、更新、删除事件,实时标记变更内容:
function TrackChangesPlugin() { const [editor] = useLexicalComposerContext(); useLayoutEffect(() => { registerCustomMarks(editor); const unregisterMutation = editor.registerMutationListener( TextNode, (mutations) => { editor.update(() => { for (const mutation of mutations) { const { target, type, payload } = mutation; // 处理插入操作:新文本节点或文本内容增加 if (type === 'create' || (type === 'update' && payload.text.length > target.__text.length)) { const start = target.__text.length; const end = payload.text.length; const selection = $createRangeSelection(); selection.anchor = $createPoint(target.__key, start); selection.focus = $createPoint(target.__key, end); $wrapSelectionInMarkNode(selection, false, InsertedMark); } // 处理删除操作:文本内容减少或节点被删除 if ((type === 'update' && payload.text.length < target.__text.length) || type === 'delete') { if (type === 'update') { const start = payload.text.length; const end = target.__text.length; const selection = $createRangeSelection(); selection.anchor = $createPoint(target.__key, start); selection.focus = $createPoint(target.__key, end); $wrapSelectionInMarkNode(selection, false, DeletedMark); } else if (type === 'delete') { // 保留删除痕迹:把删除的文本用DeletedMark包裹后插入原位置 const deletedNode = $createTextNode(target.__text); deletedNode.addMark($createDeletedMark()); const parent = target.getParent(); parent?.insertBefore(deletedNode, null); } } } }, { tag: 'track-changes' }); // 合并到同一历史条目,避免污染栈 } ); return unregisterMutation; }, [editor]); return null; }
关键细节
- 在
editor.update中添加tag参数,Lexical会将标记操作与用户原始操作合并为同一个历史记录条目,不会额外生成历史栈节点。 - 用
registerMutationListener替代registerUpdateListener,可以直接捕获节点级别的变更,比全文本对比效率更高、精度更准。 - 如果需要区分多用户修订,可以给Mark节点添加
userId等自定义属性,在创建时记录相关信息。
内容的提问来源于stack exchange,提问作者Sai Krishnan
相关产品推荐
相关产品推荐

