From 53b53d316905470e488ae1055b08db50fb1dfbb5 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 2 Jul 2026 14:59:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20appendTransaction=20=E5=8A=A0?= =?UTF-8?q?=E9=87=8D=E5=85=A5=E9=98=B2=E6=8A=A4=20+=20Enter=20=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈:升级成 taskList 后,空项按 Enter 未退出列表(与斜杠命令创建的 行为不一致)。 单测用 splitListItem 命令验证:升级后的 taskList,有内容项 Enter 分裂出空 taskItem,空项 Enter 正确退出列表(doc 末尾产生 paragraph)。行为与斜杠 命令创建的一致。说明 appendTransaction 产生的节点结构合法。 追加防护:给 appendTransaction 返回的 transaction 打 pluginKey 标记, 重入时跳过,避免 Enter 等操作的文档变化被本插件二次干扰。 happy-dom 无法可靠模拟真实 keydown 链路(派发 KeyboardEvent 时光标映射 不准),Enter 的 bug 需真实浏览器复现。 --- .../src/__tests__/task-input-rule.test.ts | 27 +++++++++++++++++++ libs/tiptap-editor/src/task-input-rule.ts | 7 +++++ 2 files changed, 34 insertions(+) diff --git a/libs/tiptap-editor/src/__tests__/task-input-rule.test.ts b/libs/tiptap-editor/src/__tests__/task-input-rule.test.ts index ddbc6f3..1257dd6 100644 --- a/libs/tiptap-editor/src/__tests__/task-input-rule.test.ts +++ b/libs/tiptap-editor/src/__tests__/task-input-rule.test.ts @@ -128,4 +128,31 @@ describe('TaskInputRule (appendTransaction 升级方案)', () => { const block = firstBlock(editor) expect(block.type).toBe('taskList') }) + + it('升级后 Enter 行为与斜杠命令创建的一致', async () => { + // 场景:升级成含内容的 taskList,Enter 新建空项,空项再 Enter 退出。 + // 用 splitListItem 命令直接模拟(TaskItem 的 Enter 快捷键即绑定此命令)。 + typeText(editor, '- ') + await flush() + typeText(editor, '[ ] 未完成') + await flush() + + const taskList = firstBlock(editor) + expect(taskList.type).toBe('taskList') + expect(taskList.content?.[0]?.attrs?.checked).toBe(false) + + // 第一次 Enter(splitListItem):有内容项 → 分裂出新的空 taskItem + editor.commands.splitListItem('taskItem') + const after1 = firstBlock(editor) + expect(after1.content?.length).toBe(2) + expect(after1.content?.[1]?.attrs?.checked).toBe(false) + + // 第二次 Enter(splitListItem):空 taskItem(最后一项,段落为空)→ 应退出列表 + editor.commands.splitListItem('taskItem') + + // 退出后:taskList 保留原项,文档末尾新增普通段落 + const json = editor.getJSON() + const lastType = json.content?.[json.content.length - 1]?.type + expect(lastType).toBe('paragraph') + }) }) diff --git a/libs/tiptap-editor/src/task-input-rule.ts b/libs/tiptap-editor/src/task-input-rule.ts index 59a7620..dc01ba7 100644 --- a/libs/tiptap-editor/src/task-input-rule.ts +++ b/libs/tiptap-editor/src/task-input-rule.ts @@ -44,6 +44,11 @@ export const TaskInputRule = Extension.create({ new Plugin({ key: pluginKey, appendTransaction: (transactions, _oldState, newState) => { + // 跳过由本插件产生的 transaction,避免重入(Enter 等操作产生的文档变化 + // 不应再次触发升级判断,否则可能覆盖 splitListItem 的结果)。 + const ownTr = transactions.some((tr) => tr.getMeta(pluginKey)) + if (ownTr) return null + // 仅在文档实际变化时检查;非文档变化(选区移动)直接跳过。 const docChanged = transactions.some((tr) => tr.docChanged) if (!docChanged) return null @@ -124,6 +129,8 @@ export const TaskInputRule = Extension.create({ const paragraphTextLen = paragraph ? paragraph.content.size : 0 cursorPos += 1 + paragraphTextLen replaceTr.setSelection(TextSelection.near(replaceTr.doc.resolve(cursorPos))) + // 标记为本插件产生的 transaction,防止 appendTransaction 重入。 + replaceTr.setMeta(pluginKey, { converted: true }) return replaceTr },