From 08ca2c099fc50cc45cd5c347dae422424e4ba8af Mon Sep 17 00:00:00 2001 From: liaibo Date: Fri, 9 Jan 2026 07:22:27 +0800 Subject: [PATCH] =?UTF-8?q?0.1.09=E6=80=9D=E7=BB=B4=E5=AF=BC=E5=9B=BE?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/editor/blocks/MindmapBlock.tsx | 530 ++++++++++++++++-- 1 file changed, 496 insertions(+), 34 deletions(-) diff --git a/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx b/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx index 62f253df..ae1741dc 100644 --- a/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx +++ b/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx @@ -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(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) 而不是 microtask:BlockNote/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(null); + const pendingPersistRef = useRef(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 = ({
{ + 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 });