0.1.09思维导图复制粘贴bug

This commit is contained in:
liaibo
2026-01-09 07:22:27 +08:00
parent e7350c87c6
commit 08ca2c099f
@@ -19,7 +19,6 @@ import type {
PropSchema,
SpecificBlock,
} from "@blocknote/core";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import type { CustomBlockSchema } from "../schema";
import { MindmapToolbar } from "./MindmapToolbar";
import { MindmapSidebar } from "./MindmapSidebar";
@@ -270,7 +269,16 @@ const ensureActiveBefore = (mindmap?: MindMapInstance | null) => {
if (fallback) {
// 主动维护激活列表,避免首次插入子节点时因无激活节点导致插入位置错误
// 先确保根节点居中,避免后续插入节点跑到 (0,0)
if (renderer?.setRootNodeCenter) renderer.setRootNodeCenter();
// 注意:setRootNodeCenter 内部依赖 renderer.root(而不是 renderTree._node)。
// 在“刚初始化/刚插入第二张导图”的窗口期里 renderer.root 可能仍是 null
// 此时调用会触发内部解构异常,导致快捷键/工具栏命令中断。
if (renderer?.root && renderer?.setRootNodeCenter) {
try {
renderer.setRootNodeCenter();
} catch {
// ignore
}
}
mm.view?.fit?.();
if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(fallback, true);
@@ -317,8 +325,11 @@ const MindmapBlockView = ({
const effectiveFullscreen = fullscreen || localFullscreen;
const wrapperRef = useRef<HTMLDivElement | null>(null);
const hotkeyScopeRef = useRef(false);
const lastInteractionAtRef = useRef(0);
const skipNextPasteRef = useRef(false);
const persistDataRef = useRef<((data: unknown) => void) | null>(null);
const recentNodeDblclickRef = useRef(false);
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
const deletingRef = useRef(false);
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
@@ -399,17 +410,104 @@ const MindmapBlockView = ({
const onPointerDownCapture = (e: Event) => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const target = e.target as Node | null;
hotkeyScopeRef.current = !!(target && wrapper.contains(target));
const targetNode = e.target as Node | null;
const inWrapper = !!(targetNode && wrapper.contains(targetNode));
hotkeyScopeRef.current = inWrapper;
lastInteractionAtRef.current = inWrapper ? Date.now() : 0;
// 关键:simple-mind-map 的画布/节点有时会 stopPropagation,导致外层 onPointerDown
// 不触发,从而无法把焦点从 BlockNote 编辑器移到思维导图块上,最终表现为 Enter/Tab
// 等快捷键不生效。
// 这里用 capture 阶段兜底:只要点击发生在 wrapper 内,就把焦点拉到 wrapper 上。
if (!inWrapper) return;
const target = (targetNode as HTMLElement | null) ?? null;
if (target) {
const tag = target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
// 节点文本编辑中不抢焦点,避免影响输入/粘贴(仅判断 simple-mind-map 的节点编辑元素)
const mm = mindmapRef.current;
const editClasses = mm?.editNodeClassList;
if (editClasses) {
for (const cls of editClasses) {
if (!cls) continue;
if (target.classList?.contains(cls)) return;
const found = target.closest?.(`.${cls}`);
if (found) return;
}
}
}
// 这里用 setTimeout(0) 而不是 microtaskBlockNote/ProseMirror 可能会在同一轮事件里重新抢回焦点,
// 导致“选中节点后 Ctrl+V 把思维导图替换成纯文本”。延后一拍把焦点拉回 wrapper,保证快捷键/粘贴作用域稳定。
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
};
document.addEventListener("pointerdown", onPointerDownCapture, true);
document.addEventListener("pointerdown", onPointerDownCapture, true);
document.addEventListener("mousedown", onPointerDownCapture, true);
return () => {
document.removeEventListener("pointerdown", onPointerDownCapture, true);
document.removeEventListener("mousedown", onPointerDownCapture, true);
document.removeEventListener("pointerdown", onPointerDownCapture, true);
document.removeEventListener("mousedown", onPointerDownCapture, true);
};
}, []);
// 关键:阻止鼠标事件冒泡到 BlockNote/ProseMirror(它们会在 contenteditable 上处理 mousedown,从而产生 NodeSelection)。
// 不能用 React 的 onMouseDown(事件委托在 document,太晚了),必须用原生监听挂在 wrapper 上,确保在 bubble 链路中先于 editor DOM。
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const stopBubble = (e: Event) => {
const target = e.target as HTMLElement | null;
if (!target) return;
// 节点文本编辑态 / 输入框:允许事件继续冒泡,避免影响输入法/选择/粘贴
const tag = target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
const mm = mindmapRef.current;
const editClasses = mm?.editNodeClassList;
if (editClasses) {
for (const cls of editClasses) {
if (!cls) continue;
if (target.classList?.contains(cls)) return;
const found = target.closest?.(`.${cls}`);
if (found) return;
}
}
// 只要在思维导图块内点击,就阻止冒泡到 editor,避免“Ctrl+V 替换整块”
e.stopPropagation();
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
// 同步/异步都尝试一次,把焦点拉到 wrapper,保证快捷键稳定
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
};
wrapper.addEventListener("mousedown", stopBubble);
wrapper.addEventListener("pointerdown", stopBubble);
return () => {
wrapper.removeEventListener("mousedown", stopBubble);
wrapper.removeEventListener("pointerdown", stopBubble);
};
}, [effectiveFullscreen]);
// 标记“节点双击”事件,避免和“画布双击进入全屏”产生冲突
useEffect(() => {
if (!mindmap) return;
@@ -519,7 +617,11 @@ const MindmapBlockView = ({
// 让 Enter/Tab/复制/粘贴在“思维导图块”内生效(避免 BlockNote 抢走快捷键)
useLayoutEffect(() => {
const onKeyDownCapture = (e: KeyboardEvent) => {
if (!hotkeyScopeRef.current) return;
const inScope =
hotkeyScopeRef.current ||
(lastInteractionAtRef.current > 0 &&
Date.now() - lastInteractionAtRef.current < 60_000);
if (!inScope) return;
const mm = mindmapRef.current;
if (!mm || !mindmapReadyRef.current) return;
@@ -527,19 +629,22 @@ const MindmapBlockView = ({
const editClasses = mm.editNodeClassList;
const wrapper = wrapperRef.current;
const isInWrapper = Boolean(wrapper && target && wrapper.contains(target));
if (target && isInWrapper) {
const isNodeTextEditing = (() => {
if (!target || !isInWrapper) return false;
const tag = target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
// contenteditable 输入中不拦截:避免破坏节点文本编辑/粘贴
if (target.isContentEditable) return;
const editableAncestor = target.closest?.('[contenteditable="true"]');
if (editableAncestor) return;
if (editClasses && target.classList) {
if (tag === "INPUT" || tag === "TEXTAREA") return true;
// 仅在 simple-mind-map 的“节点文本编辑元素”上判定为编辑态;不要用 isContentEditable 泛化判断,
// 否则会把库内部的隐藏输入层误判成编辑态,导致 Ctrl+C/Ctrl+V 失效。
if (editClasses) {
for (const cls of editClasses) {
if (cls && target.classList.contains(cls)) return;
if (!cls) continue;
if (target.classList?.contains(cls)) return true;
const found = target.closest?.(`.${cls}`);
if (found) return true;
}
}
}
return false;
})();
const isMod = e.ctrlKey || e.metaKey;
const key = e.key;
@@ -566,6 +671,16 @@ const MindmapBlockView = ({
if (!node) return;
inst.execCommand?.("SET_NODE_ACTIVE", node, true);
inst.execCommand?.("INSERT_NODE", false, [node]);
// 兜底:某些情况下 INSERT_NODE 不触发 data_change(例如被外层捕获键盘
// 事件拦截导致内部 Keyboard 插件不走),这里主动做一次防抖保存,确保
// 切换全屏/刷新后不会丢失。
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
return;
}
@@ -577,18 +692,39 @@ const MindmapBlockView = ({
if (!node) return;
inst.execCommand?.("SET_NODE_ACTIVE", node, true);
inst.execCommand?.("INSERT_CHILD_NODE", false, [node]);
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
return;
}
// 节点文本编辑中:只接管 Enter/Tab;其余快捷键交给默认输入行为
//(例如 Ctrl+C/Ctrl+V 作为文本复制粘贴)。
if (isNodeTextEditing) return;
if (isMod && lower === "c") {
stop();
const inst = ensureActiveBefore(mm);
if (!inst) return;
const node = pickTargetNode(inst);
if (node) {
inst.execCommand?.("SET_NODE_ACTIVE", node, true);
}
inst.renderer.copy?.();
return;
}
if (isMod && lower === "v") {
// 兜底:某些场景下(尤其是嵌入编辑器时)浏览器仍会触发原生 paste 事件。
// 这里标记一次,避免我们在 paste capture 里重复执行粘贴逻辑。
skipNextPasteRef.current = true;
window.setTimeout(() => {
skipNextPasteRef.current = false;
}, 200);
stop();
const inst = ensureActiveBefore(mm);
if (!inst) return;
@@ -596,15 +732,232 @@ const MindmapBlockView = ({
const copyData = renderer.beingCopyData ?? null;
if (copyData) {
inst.execCommand?.("PASTE_NODE", copyData);
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
return;
}
renderer.paste?.();
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
}
};
window.addEventListener("keydown", onKeyDownCapture, true);
return () => {
window.removeEventListener("keydown", onKeyDownCapture, true);
window.removeEventListener("keydown", onKeyDownCapture, true);
};
}, []);
// 兜底:在非全屏嵌入 BlockNote 时,Ctrl+V 可能仍然触发编辑器的 paste,导致思维导图块被替换成纯文本。
// 这里在 capture 阶段拦截 paste:当“最近一次指针交互在思维导图块内”且当前不在节点文本编辑态时,
// 强制把粘贴交给 simple-mind-map,并阻止 BlockNote 继续处理。
useLayoutEffect(() => {
const onPasteCapture = (e: ClipboardEvent) => {
const inScope =
hotkeyScopeRef.current ||
(lastInteractionAtRef.current > 0 &&
Date.now() - lastInteractionAtRef.current < 60_000);
if (!inScope) return;
const mm = mindmapRef.current;
if (!mm || !mindmapReadyRef.current) return;
const wrapper = wrapperRef.current;
const active = (typeof document !== "undefined"
? (document.activeElement as HTMLElement | null)
: null);
const isTextEditingNow = (() => {
if (!wrapper || !active) return false;
if (!wrapper.contains(active)) return false;
const tag = active.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return true;
const editClasses = mm.editNodeClassList;
if (editClasses) {
for (const cls of editClasses) {
if (!cls) continue;
if (active.classList?.contains(cls)) return true;
const found = active.closest?.(`.${cls}`);
if (found) return true;
}
}
return false;
})();
// 节点文本编辑态:允许默认粘贴(粘贴到节点文本)
if (isTextEditingNow) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation?.();
// 如果刚刚已经在 keydown 里处理过 Ctrl+V,这里只负责阻止 BlockNote 的 paste
if (skipNextPasteRef.current) {
skipNextPasteRef.current = false;
return;
}
const inst = ensureActiveBefore(mm);
if (!inst) return;
// 优先交给 simple-mind-map 自己的 clipboard 逻辑:支持外部文本/图片/链接以及内部节点复制格式
try {
inst.renderer.paste?.();
} catch {
// ignore
}
// 兜底持久化:避免快速切换视图导致“看起来没保存”
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
};
window.addEventListener("paste", onPasteCapture, true);
return () => {
window.removeEventListener("paste", onPasteCapture, true);
};
}, []);
// 兜底:Ctrl+C 可能被 BlockNote/ProseMirror 先行拦截,导致我们 keydown 捕获不到。
// 这里直接在 copy 事件的 capture 阶段接管,确保“选中节点 -> Ctrl+C”一定能复制节点数据。
useLayoutEffect(() => {
const onCopyCapture = (e: ClipboardEvent) => {
const inScope =
hotkeyScopeRef.current ||
(lastInteractionAtRef.current > 0 &&
Date.now() - lastInteractionAtRef.current < 60_000);
if (!inScope) return;
const mm = mindmapRef.current;
if (!mm || !mindmapReadyRef.current) return;
const wrapper = wrapperRef.current;
const active = (typeof document !== "undefined"
? (document.activeElement as HTMLElement | null)
: null);
const isTextEditingNow = (() => {
if (!wrapper || !active) return false;
if (!wrapper.contains(active)) return false;
const tag = active.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return true;
const editClasses = mm.editNodeClassList;
if (editClasses) {
for (const cls of editClasses) {
if (!cls) continue;
if (active.classList?.contains(cls)) return true;
const found = active.closest?.(`.${cls}`);
if (found) return true;
}
}
return false;
})();
if (isTextEditingNow) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation?.();
const inst = ensureActiveBefore(mm);
if (!inst) return;
const renderer = inst.renderer;
const node =
renderer.activeNodeList?.[0] ??
renderer.lastActiveNodeList?.[0] ??
renderer.root ??
renderer.renderTree?._node ??
null;
if (node) inst.execCommand?.("SET_NODE_ACTIVE", node, true);
try {
inst.renderer.copy?.();
} catch {
// ignore
}
};
window.addEventListener("copy", onCopyCapture, true);
return () => {
window.removeEventListener("copy", onCopyCapture, true);
};
}, []);
// 额外兜底:部分浏览器/编辑器会优先通过 beforeinput(insertFromPaste) 走粘贴路径,
// 即便我们拦截了 paste,也可能仍触发“替换掉思维导图块”的输入。
// 这里在 capture 阶段统一阻止 paste 相关的 beforeinput,并把实际粘贴交给 simple-mind-map。
useLayoutEffect(() => {
const onBeforeInputCapture = (e: InputEvent) => {
const inputType = (e as unknown as { inputType?: string }).inputType || "";
if (!inputType || !inputType.toLowerCase().includes("paste")) return;
const inScope =
hotkeyScopeRef.current ||
(lastInteractionAtRef.current > 0 &&
Date.now() - lastInteractionAtRef.current < 60_000);
if (!inScope) return;
const mm = mindmapRef.current;
if (!mm || !mindmapReadyRef.current) return;
// 节点文本编辑态:允许默认粘贴(粘贴到节点文本)
const wrapper = wrapperRef.current;
const active = (typeof document !== "undefined"
? (document.activeElement as HTMLElement | null)
: null);
const isTextEditingNow = (() => {
if (!wrapper || !active) return false;
if (!wrapper.contains(active)) return false;
const tag = active.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return true;
const editClasses = mm.editNodeClassList;
if (editClasses) {
for (const cls of editClasses) {
if (!cls) continue;
if (active.classList?.contains(cls)) return true;
const found = active.closest?.(`.${cls}`);
if (found) return true;
}
}
return false;
})();
if (isTextEditingNow) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation?.();
const inst = ensureActiveBefore(mm);
if (!inst) return;
try {
inst.renderer.paste?.();
} catch {
// ignore
}
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
};
window.addEventListener("beforeinput", onBeforeInputCapture, true);
return () => {
window.removeEventListener("beforeinput", onBeforeInputCapture, true);
};
}, []);
@@ -641,10 +994,45 @@ const MindmapBlockView = ({
},
[autosaveKey, block, docId, editor, mindmapId],
);
useEffect(() => {
persistDataRef.current = persistData;
}, [persistData]);
const debouncedPersist = useDebouncedCallback((data: unknown) => {
persistData(data);
}, 800);
// “有上限的防抖保存”:连续 data_change 只会合并为一次保存,但不会因为持续变化
// 而无限期推迟(避免测试/用户快速切换全屏时出现“看起来没保存”)。
const persistTimerRef = useRef<number | null>(null);
const pendingPersistRef = useRef<unknown | null>(null);
const schedulePersist = useCallback(
(data: unknown) => {
pendingPersistRef.current = data;
if (typeof window === "undefined") return;
if (persistTimerRef.current != null) return;
persistTimerRef.current = window.setTimeout(() => {
persistTimerRef.current = null;
const pending = pendingPersistRef.current;
pendingPersistRef.current = null;
if (pending) persistData(pending);
}, 800);
},
[persistData],
);
useEffect(() => {
return () => {
if (persistTimerRef.current != null && typeof window !== "undefined") {
window.clearTimeout(persistTimerRef.current);
persistTimerRef.current = null;
}
const pending = pendingPersistRef.current;
pendingPersistRef.current = null;
if (pending) {
try {
persistData(pending);
} catch {
// ignore
}
}
};
}, [persistData]);
const initialSyncDone = useRef(false);
useEffect(() => {
@@ -955,10 +1343,59 @@ const MindmapBlockView = ({
hostEl.appendChild(measureDiv);
}
instance.setMode?.("edit");
// 兜底:在某些环境/焦点状态下,simple-mind-map 的 data_change 事件可能不会触发,
// 或者键盘插件直接执行命令但未走到我们的保存逻辑,最终表现为“节点能插入但不会持久化”。
// 这里对 execCommand 做轻量包装:当执行潜在的“结构变更”命令时,主动触发一次防抖保存。
const shouldPersistAfterCommand = (cmd: unknown) => {
if (typeof cmd !== "string") return false;
if (
cmd === "INSERT_NODE" ||
cmd === "INSERT_CHILD_NODE" ||
cmd === "PASTE_NODE" ||
cmd === "REMOVE_NODE" ||
cmd === "DELETE_NODE"
) {
return true;
}
return (
cmd.startsWith("INSERT_") ||
cmd.startsWith("PASTE_") ||
cmd.startsWith("REMOVE_") ||
cmd.startsWith("DELETE_")
);
};
const originalExecCommand =
typeof instance.execCommand === "function"
? instance.execCommand.bind(instance)
: null;
if (originalExecCommand) {
(instance as unknown as { execCommand: (...args: any[]) => any }).execCommand = (
cmd: unknown,
...args: any[]
) => {
const ret = originalExecCommand(cmd as any, ...args);
if (
!applyingRemoteRef.current &&
!deletingRef.current &&
shouldPersistAfterCommand(cmd)
) {
hasLocalEditsRef.current = true;
try {
const snapshot =
instance.getData?.(true) ?? instance.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
}
return ret;
};
}
const centerAndFit = (retry = 0) => {
try {
const hostRect = hostEl.getBoundingClientRect?.();
if (!hostRect || hostRect.width < 10 || hostRect.height < 10) {
if (!hostRect || hostRect.width < 10 || hostRect.height < 10) {
if (retry < 5) {
window.setTimeout(() => centerAndFit(retry + 1), 80);
}
@@ -1030,9 +1467,20 @@ const MindmapBlockView = ({
instance.on?.("node_active", (_node: unknown, list: unknown[]) => {
if (!list || list.length === 0) return;
setActiveNodes((list || []) as MindMapNode[]);
// 兜底:部分场景下点击节点不会触发外层 pointerdown(被 stopPropagation),
// 这里用内部事件标记“当前在思维导图作用域内”,确保 Ctrl+C/Ctrl+V 不会被 BlockNote 抢走。
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
});
instance.on?.("node_click", (node: unknown) => {
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
const renderer = instance.renderer;
const nodeWithActive = node as unknown as { active?: () => void };
if (typeof nodeWithActive.active === "function") {
@@ -1049,6 +1497,15 @@ const MindmapBlockView = ({
? [node as MindMapNode]
: (list as MindMapNode[]),
);
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
});
instance.on?.("painter_start", () => setPainterMode(true));
instance.on?.("painter_end", () => setPainterMode(false));
@@ -1060,8 +1517,8 @@ const MindmapBlockView = ({
//(例如 children 非数组、text 缺失等),全屏切换重建时会触发 RichText 初始化崩溃。
// 这里统一改为从实例读取快照,确保保存/恢复的数据结构稳定。
const snapshot =
instance.getData?.(true) ?? instance.getData?.() ?? null;
if (snapshot) debouncedPersist(snapshot);
instance.getData?.(true) ?? instance.getData?.() ?? null;
if (snapshot) schedulePersist(snapshot);
});
const renderer = instance.renderer;
const rootNode = getRootNode(instance);
@@ -1540,7 +1997,7 @@ const MindmapBlockView = ({
const data = JSON.parse(text);
mindmap?.setData(data);
mindmap?.command.clearHistory();
debouncedPersist(data);
persistData(data);
return;
}
@@ -1557,7 +2014,7 @@ const MindmapBlockView = ({
});
mindmap?.setData(data);
mindmap?.command.clearHistory();
debouncedPersist(data);
persistData(data);
return;
}
@@ -1567,7 +2024,7 @@ const MindmapBlockView = ({
const data = await parseMindManagerMmapFile(file);
mindmap?.setData(data);
mindmap?.command.clearHistory();
debouncedPersist(data);
persistData(data);
return;
}
@@ -1583,7 +2040,7 @@ const MindmapBlockView = ({
}
mindmap?.setData(data);
mindmap?.command.clearHistory();
debouncedPersist(data);
persistData(data);
return;
}
@@ -1600,7 +2057,7 @@ const MindmapBlockView = ({
if (!window.confirm("确定要新建空白导图吗?当前未保存的修改将被覆盖。")) return;
mindmap?.setData(defaultMindmapData);
mindmap?.command.clearHistory();
debouncedPersist(defaultMindmapData);
persistData(defaultMindmapData);
};
const handleOpenDirectory = () => {
@@ -2011,9 +2468,12 @@ const MindmapBlockView = ({
</div>
<div
data-testid="mindmap-stage"
className="relative h-[520px] w-full overflow-hidden bg-white"
onPointerDown={() => {
className="relative h-[520px] w-full overflow-hidden bg-white"
onPointerDown={(e) => {
// 阻止事件冒泡到 BlockNote/ProseMirror,避免产生 NodeSelection 导致粘贴替换整块
e.stopPropagation();
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
// 点击后把焦点落到容器上,避免快捷键被编辑器抢走
try {
wrapperRef.current?.focus?.({ preventScroll: true });
@@ -2021,8 +2481,10 @@ const MindmapBlockView = ({
// ignore
}
}}
onMouseDown={() => {
onMouseDown={(e) => {
e.stopPropagation();
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
// 兼容:某些环境下 pointer 事件不触发
try {
wrapperRef.current?.focus?.({ preventScroll: true });