CKEditor5多行粘贴操作撤销时触发model-position-path-incorrect-format错误
CKEditor5插件多行内容撤销报错:model-position-path-incorrect-format
问题背景
我基于CKEditor5开发了一款插件,用于将粘贴内容中用()、$$等分隔符包裹的公式,转换为ckeditor5-math兼容的数学公式。为此修改了AutoMath插件以支持这类分隔符,但遇到了撤销异常:单行内容的Ctrl+Z操作正常,多行内容执行两次撤销后会抛出错误。
复现步骤
为定位问题,我做了一个无数学插件依赖的测试版本(将&包裹的文本转为粗体):
- 将光标置于单词内部(非文本首尾,暂未明确原因)
- 粘贴以下内容:
aa &bb& cc dd ee &ff& gg
- 执行两次撤销操作,触发报错。
错误信息
Uncaught CKEditorError: model-position-path-incorrect-format {"path":[]}
问题分析
已知错误与操作批次有关,大概率涉及模型位置的父元素缓存失效,但尚未明确具体实现层面的问题。核心疑点包括:
- 延迟执行的转换操作与粘贴操作不在同一undo批次,导致撤销时位置跟踪错乱
- LiveRange/LivePosition在模型结构变化后失效,仍被后续操作引用
- 批量修改模型时,TextProxy对象失效引发位置路径错误
测试插件修复版代码
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange'; import LivePosition from '@ckeditor/ckeditor5-engine/src/model/liveposition'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; export default class Test extends Plugin { static get requires() { return [Undo]; } static get pluginName() { return 'Test'; } constructor(editor) { super(editor); this._timeoutId = null; this._equationRange = null; // 保存当前处理的LiveRange,用于撤销时清理 } init() { const editor = this.editor; const modelDocument = editor.model.document; this.listenTo(editor.plugins.get('ClipboardPipeline'), 'inputTransformation', (evt, data) => { const firstRange = modelDocument.selection.getFirstRange(); const leftLivePosition = LivePosition.fromPosition(firstRange.start); leftLivePosition.stickiness = 'toPrevious'; const rightLivePosition = LivePosition.fromPosition(firstRange.end); rightLivePosition.stickiness = 'toNext'; modelDocument.once('change:data', () => { this._boldBetweenPositions(leftLivePosition, rightLivePosition); leftLivePosition.detach(); rightLivePosition.detach(); }, {priority: 'high'}); }); editor.commands.get('undo').on('execute', () => { if (this._timeoutId) { global.window.clearTimeout(this._timeoutId); this._timeoutId = null; } // 撤销时清理失效的LiveRange,避免引用无效位置 if (this._equationRange) { this._equationRange.detach(); this._equationRange = null; } }, {priority: 'high'}); } _boldBetweenPositions(leftPosition, rightPosition) { const editor = this.editor; this._equationRange = new LiveRange(leftPosition, rightPosition); // 延迟执行,允许用户撤销转换操作 this._timeoutId = global.window.setTimeout(() => { this._timeoutId = null; // 检查范围是否仍附着在模型中,跳过无效操作 if (!this._equationRange.isAttached()) { this._equationRange.detach(); this._equationRange = null; return; } editor.model.change(writer => { // 先收集所有待修改内容,避免修改模型后Walker失效 const pendingChanges = []; const walker = this._equationRange.getWalker({ignoreElementEnd: true}); for (const node of walker) { const text = node.item.data; if (node.item.is('$textProxy') && text?.match(/&/g)) { pendingChanges.push({ range: writer.createRange(node.previousPosition, node.nextPosition), content: text }); } } // 反向执行修改,避免位置偏移 for (let i = pendingChanges.length - 1; i >= 0; i--) { const { range, content } = pendingChanges[i]; writer.remove(range); const splitParts = this._split(content); for (let j = splitParts.length - 1; j >= 0; j--) { if (j % 2 === 0) { writer.insertText(splitParts[j], range.start); } else { writer.insertText(splitParts[j], {bold: true}, range.start); } } } }); this._equationRange.detach(); this._equationRange = null; }, 100); } // 修复分割逻辑,正确提取普通文本与需要加粗的内容 _split(text) { const parts = text.split(/&([^&]+)&/g); return parts.filter(part => part); // 过滤空字符串 } }
修复说明
- LiveRange生命周期管理:新增
_equationRange实例变量,在撤销时主动清理失效范围,避免引用已被删除的模型位置 - 有效性前置检查:延迟执行转换前,先判断LiveRange是否仍附着在模型中,跳过无效操作
- 批量修改优化:先收集所有待修改的内容和范围,再反向执行修改,避免模型结构变化导致的位置偏移
- 分割逻辑修复:修正
_split方法,确保正确提取普通文本与需要加粗的片段,避免内容丢失
内容的提问来源于stack exchange,提问作者kinda_sus
相关产品推荐
相关产品推荐

