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 },