fix(editor): 代码块内 Backspace 删整块(ignoreMutation 误忽略 contentDOM 编辑)
CodeBlockNodeView.ignoreMutation 无条件返回 true,导致 contentDOM(<code>) 内的 characterData/childList mutation 被忽略,ProseMirror 文档状态与 DOM 失同步——输入文字它不知道,Backspace 时基于错误状态重建,表现为删整块。 改为对齐 ProseMirror/Tiptap 默认 NodeView 逻辑: - contentDOM 内的 mutation(characterData/childList)→ false(交给 PM) - selection → false - contentDOM 自身 attributes(高亮改 class)→ true(忽略) - 工具栏/结果区装饰元素 → true(忽略) 补 2 个回归用例覆盖 contentDOM 内/外分支。参数类型改 ViewMutationRecord (含 selection,ProseMirror 扩展类型)。
This commit is contained in:
parent
c8b2248066
commit
d8036c6e70
@ -1,3 +1,4 @@
|
||||
import type { ViewMutationRecord } from '@tiptap/pm/view';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CodeBlockNodeView } from '../code-block-view';
|
||||
|
||||
@ -97,12 +98,47 @@ describe('CodeBlockNodeView', () => {
|
||||
expect(view.contentDOM?.classList.contains('runnable')).toBe(false);
|
||||
});
|
||||
|
||||
it('ignoreMutation 返回 true(工具栏不触发编辑事务)', () => {
|
||||
it('ignoreMutation: contentDOM 内的编辑 mutation 返回 false(让 ProseMirror 处理)', () => {
|
||||
// 回归:ignoreMutation 无条件返回 true 会导致 contentDOM 内的输入/退格被忽略,
|
||||
// 文档状态与 DOM 失同步,表现为 Backspace 删整块而非删字符。
|
||||
const view = new CodeBlockNodeView({
|
||||
node: mockNode('python'),
|
||||
editor: mockEditor(),
|
||||
} as any);
|
||||
expect(view.ignoreMutation()).toBe(true);
|
||||
const code = view.contentDOM!;
|
||||
// 模拟 code 内文本节点的 characterData mutation(输入/退格)
|
||||
const textNode = document.createTextNode('fn main');
|
||||
code.appendChild(textNode);
|
||||
const editMutation = {
|
||||
type: 'characterData',
|
||||
target: textNode,
|
||||
} as unknown as ViewMutationRecord;
|
||||
expect(view.ignoreMutation(editMutation)).toBe(false);
|
||||
// selection mutation 也应交给 ProseMirror
|
||||
const selMutation = { type: 'selection', target: code } as unknown as ViewMutationRecord;
|
||||
expect(view.ignoreMutation(selMutation)).toBe(false);
|
||||
});
|
||||
|
||||
it('ignoreMutation: 工具栏/结果区装饰元素的 mutation 返回 true(忽略)', () => {
|
||||
const view = new CodeBlockNodeView({
|
||||
node: mockNode('python'),
|
||||
editor: mockEditor(),
|
||||
} as any);
|
||||
// 工具栏(装饰元素)的 childList mutation 应忽略
|
||||
const toolbarMutation = {
|
||||
type: 'childList',
|
||||
target: view.dom.querySelector('.tiptap-codeblock-toolbar')!,
|
||||
addedNodes: [] as Node[],
|
||||
removedNodes: [] as Node[],
|
||||
} as unknown as ViewMutationRecord;
|
||||
expect(view.ignoreMutation(toolbarMutation)).toBe(true);
|
||||
// contentDOM 自身的 attributes 变化(如高亮改 class)忽略
|
||||
const attrMutation = {
|
||||
type: 'attributes',
|
||||
target: view.contentDOM!,
|
||||
attributeName: 'class',
|
||||
} as unknown as ViewMutationRecord;
|
||||
expect(view.ignoreMutation(attrMutation)).toBe(true);
|
||||
});
|
||||
|
||||
it('点击运行按钮调用 onRunCode(storage 回调)', () => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import type { Node as PMNode } from '@tiptap/pm/model';
|
||||
import type { ViewMutationRecord } from '@tiptap/pm/view';
|
||||
import { extractLang, extractOverridesJson } from './highlight';
|
||||
|
||||
/** editor.storage 的 key,宿主(index.ts)在此注入 onRunCode 回调。 */
|
||||
@ -122,8 +123,25 @@ export class CodeBlockNodeView {
|
||||
}
|
||||
}
|
||||
|
||||
/** 工具栏/结果区交互不触发 ProseMirror 编辑事务。 */
|
||||
ignoreMutation(): boolean {
|
||||
/**
|
||||
* 判断 DOM mutation 是否应被 ProseMirror 忽略。
|
||||
*
|
||||
* contentDOM(<code>)内的编辑(characterData/childList)必须返回 false,
|
||||
* 让 ProseMirror 正常处理事务——否则输入/退格会让文档状态与 DOM 失同步
|
||||
* (表现为 Backspace 删整块而非删字符)。
|
||||
*
|
||||
* 仅对工具栏/结果区等装饰元素返回 true(避免它们的变化触发无谓的事务)。
|
||||
* 对齐 Tiptap NodeViewWrapper 默认 ignoreMutation 逻辑。
|
||||
*/
|
||||
ignoreMutation(mutation: ViewMutationRecord): boolean {
|
||||
// selection:让 ProseMirror 管光标
|
||||
if (mutation.type === 'selection') return false;
|
||||
// contentDOM 自身的 attributes 变化(如高亮 decoration 改 class):忽略
|
||||
if (mutation.target === this.contentDOM && mutation.type === 'attributes') return true;
|
||||
// contentDOM 内的 mutation(characterData/childList):交给 ProseMirror(编辑核心)
|
||||
// 注意 contains 对 target===contentDOM 自身也返回 true,故上面的 attributes 判断须在前。
|
||||
if (this.contentDOM && this.contentDOM.contains(mutation.target)) return false;
|
||||
// 工具栏/结果区等装饰元素的 mutation:忽略
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user