0.1.03 mindmap问题修改

This commit is contained in:
liaibo
2026-01-07 07:25:07 +08:00
parent 9884ea0ea6
commit 103e36b1c5
@@ -11,7 +11,14 @@ import React, {
} from "react"; } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { createReactBlockSpec } from "@blocknote/react"; import { createReactBlockSpec } from "@blocknote/react";
import type { Block, BlockNoteEditor } from "@blocknote/core"; import type {
BlockConfig,
BlockNoteEditor,
DefaultInlineContentSchema,
DefaultStyleSchema,
PropSchema,
SpecificBlock,
} from "@blocknote/core";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import type { CustomBlockSchema } from "../schema"; import type { CustomBlockSchema } from "../schema";
import { MindmapToolbar } from "./MindmapToolbar"; import { MindmapToolbar } from "./MindmapToolbar";
@@ -21,17 +28,16 @@ import { MindmapNavigator } from "./MindmapNavigator";
import { MindmapMiniMap } from "./MindmapMiniMap"; import { MindmapMiniMap } from "./MindmapMiniMap";
import { MindmapCount } from "./MindmapCount"; import { MindmapCount } from "./MindmapCount";
import type { SidebarPanel } from "./mindmapSidebarConfig"; import type { SidebarPanel } from "./mindmapSidebarConfig";
import type { MindMapNode } from "./mindmapTypes";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react"; import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react";
import iconConfig from "./mindmapIconConfig"; import iconConfig from "./mindmapIconConfig";
import { emitAssetsChanged } from "@/lib/events"; import { emitAssetsChanged } from "@/lib/events";
import { useEditorBridgeStore } from "@/store/editor-bridge";
// 避免 SSR/Node 环境提前评估 simple-mind-map 内部依赖 document // 避免 SSR/Node 环境提前评估 simple-mind-map 内部依赖 document
// @ts-expect-error 第三方库缺少类型定义
const loadIconModules = async () => { const loadIconModules = async () => {
const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js"); const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js");
const { mergerIconList } = await import("simple-mind-map/src/utils/index.js"); const { mergerIconList } = await import("simple-mind-map/src/utils/index.js");
return { nodeIconList, mergerIconList }; return { nodeIconList, mergerIconList };
}; };
@@ -47,17 +53,37 @@ const getImageSizeSafe = (url: string): Promise<{ width: number; height: number
}); });
type MindMapInstance = { type MindMapInstance = {
execCommand: (...args: unknown[]) => void; execCommand: (command: string, ...args: unknown[]) => void;
destroy: () => void; destroy: () => void;
setData: (data: unknown) => void; setData: (data: unknown) => void;
getData: (withConfig?: boolean) => unknown; getData: (withConfig?: boolean) => unknown;
on?: (event: string, handler: (...args: unknown[]) => void) => void; on?: <TArgs extends unknown[]>(event: string, handler: (...args: TArgs) => void) => void;
emit?: (event: string, ...args: unknown[]) => void; emit?: (event: string, ...args: unknown[]) => void;
off?: (event: string, handler: (...args: unknown[]) => void) => void; off?: <TArgs extends unknown[]>(event: string, handler: (...args: TArgs) => void) => void;
setMode?: (mode: string) => void; setMode?: (mode: string) => void;
command: { clearHistory: () => void }; command: { clearHistory: () => void };
view: { fit: () => void; scale: number; enlarge: () => void; narrow: () => void; setScale: (scale: number, cx: number, cy: number) => void }; editNodeClassList?: string[];
renderer: { activeNodeList: unknown[]; renderTree: { _node: unknown }; setRootNodeCenter: () => void }; view: {
fit: () => void;
scale: number;
enlarge: () => void;
narrow: () => void;
setScale: (scale: number, cx: number, cy: number) => void;
};
renderer: {
activeNodeList: unknown[];
lastActiveNodeList?: unknown[];
root?: unknown;
renderTree: { _node: unknown };
setRootNodeCenter?: () => void;
clearActiveNodeList?: () => void;
addNodeToActiveList?: (node: unknown, isActive?: boolean) => void;
emitNodeActiveEvent?: (node: unknown) => void;
findNodeByUid?: (uid: string) => unknown;
copy?: () => void;
paste?: () => void;
beingCopyData?: unknown;
};
painter?: { startPainter: () => void }; painter?: { startPainter: () => void };
doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise<unknown> }; doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise<unknown> };
width: number; width: number;
@@ -69,13 +95,6 @@ type MindMapInstance = {
setLayout: (layout: string) => void; setLayout: (layout: string) => void;
}; };
type MindMapNode = {
getStyle?: (key: string, inherit?: boolean) => unknown;
getData?: (key: string) => unknown;
nodeData?: { data?: Record<string, unknown> };
data?: Record<string, unknown>;
};
type MindMapData = { type MindMapData = {
data: Record<string, unknown>; data: Record<string, unknown>;
children?: unknown[]; children?: unknown[];
@@ -109,22 +128,27 @@ const patchSvgRbox = async () => {
// 仅在浏览器环境生效 // 仅在浏览器环境生效
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const svgModule = await import("@svgdotjs/svg.js"); const svgModule = await import("@svgdotjs/svg.js");
const candidates = [(svgModule as any).Element, (svgModule as any).G]; type SvgCtorPrototype = {
rbox?: (ref?: unknown) => unknown;
__wolaiRboxPatched?: boolean;
};
type SvgCtor = { prototype?: SvgCtorPrototype } | undefined;
const svgModuleTyped = svgModule as unknown as { Element?: SvgCtor; G?: SvgCtor };
const candidates = [svgModuleTyped.Element, svgModuleTyped.G];
let warned = false; let warned = false;
candidates.forEach((Ctor) => { candidates.forEach((Ctor) => {
if (!Ctor?.prototype) return; if (!Ctor?.prototype) return;
if (Ctor.prototype.__wolaiRboxPatched) return; if (Ctor.prototype.__wolaiRboxPatched) return;
const original = Ctor.prototype.rbox; const original = Ctor.prototype.rbox;
if (typeof original !== "function") return; if (typeof original !== "function") return;
// @ts-expect-error 动态扩展第三方原型
Ctor.prototype.rbox = function patchedRbox(ref?: unknown) { Ctor.prototype.rbox = function patchedRbox(ref?: unknown) {
try { try {
return original.call(this, ref); return original.call(this, ref);
} catch (error) { } catch (error) {
// 退化到 DOM 的 getBoundingClientRect 计算,避免初次渲染时出现 (0,0) // 退化到 DOM 的 getBoundingClientRect 计算,避免初次渲染时出现 (0,0)
const el = type SvgElementLike = { node?: Element | null; el?: Element | null };
// svg.js Element 实例通常持有 node 属性 const maybeThis = this as unknown as SvgElementLike;
(this as any)?.node ?? (this as any)?.el ?? null; const el = maybeThis?.node ?? maybeThis?.el ?? null;
const rect = const rect =
el && typeof el.getBoundingClientRect === "function" el && typeof el.getBoundingClientRect === "function"
? el.getBoundingClientRect() ? el.getBoundingClientRect()
@@ -178,7 +202,7 @@ const applyToActiveNodes = (
const list = renderer?.activeNodeList; const list = renderer?.activeNodeList;
if (!list || list.length === 0) { if (!list || list.length === 0) {
// 优先尝试 lastActiveNodeList(部分操作会短暂清空 activeNodeList // 优先尝试 lastActiveNodeList(部分操作会短暂清空 activeNodeList
const lastActive = (renderer as any)?.lastActiveNodeList?.[0]; const lastActive = renderer?.lastActiveNodeList?.[0];
const root = renderer?.root ?? renderer?.renderTree?._node; const root = renderer?.root ?? renderer?.renderTree?._node;
const fallback = lastActive ?? root; const fallback = lastActive ?? root;
if (fallback) { if (fallback) {
@@ -202,15 +226,15 @@ const ensureActiveBefore = (mindmap?: MindMapInstance | null) => {
const renderer = mm.renderer; const renderer = mm.renderer;
const list = renderer?.activeNodeList ?? []; const list = renderer?.activeNodeList ?? [];
if (!list || list.length === 0) { if (!list || list.length === 0) {
const lastActive = (renderer as any)?.lastActiveNodeList?.[0]; const lastActive = renderer?.lastActiveNodeList?.[0];
const root = renderer?.root ?? renderer?.renderTree?._node; const root = renderer?.root ?? renderer?.renderTree?._node;
const fallback = lastActive ?? root; const fallback = lastActive ?? root;
if (fallback) { if (fallback) {
// 主动维护激活列表,避免首次插入子节点时因无激活节点导致插入位置错误 // 主动维护激活列表,避免首次插入子节点时因无激活节点导致插入位置错误
// 先确保根节点居中,避免后续插入节点跑到 (0,0) // 先确保根节点居中,避免后续插入节点跑到 (0,0)
renderer?.setRootNodeCenter && renderer.setRootNodeCenter(); if (renderer?.setRootNodeCenter) renderer.setRootNodeCenter();
mm.view?.fit?.(); mm.view?.fit?.();
renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(fallback, true); if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(fallback, true);
if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(fallback); if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(fallback);
mm.execCommand?.("SET_NODE_ACTIVE", fallback, true); mm.execCommand?.("SET_NODE_ACTIVE", fallback, true);
@@ -227,23 +251,25 @@ const MindmapBlockView = ({
editor, editor,
fullscreen = false, fullscreen = false,
}: { }: {
block: Block<CustomBlockSchema, "mindmap">; block: SpecificBlock<
CustomBlockSchema,
"mindmap",
DefaultInlineContentSchema,
DefaultStyleSchema
>;
editor: BlockNoteEditor<CustomBlockSchema>; editor: BlockNoteEditor<CustomBlockSchema>;
fullscreen?: boolean; fullscreen?: boolean;
}) => { }) => {
const currentDocumentId = useEditorBridgeStore(
(state) => state.currentDocumentId,
);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const [mindmap, setMindmap] = useState<MindMapInstance | null>(null); const [mindmap, setMindmap] = useState<MindMapInstance | null>(null);
const mindmapReadyRef = useRef(false); const mindmapReadyRef = useRef(false);
const mindmapRef = useRef<MindMapInstance | null>(null); const mindmapRef = useRef<MindMapInstance | null>(null);
const hasLocalEditsRef = useRef(false); const hasLocalEditsRef = useRef(false);
const applyingRemoteRef = useRef(false); const applyingRemoteRef = useRef(false);
const [canBack, setCanBack] = useState(false); const [canBack, setCanBack] = useState(false);
const [canForward, setCanForward] = useState(false); const [canForward, setCanForward] = useState(false);
const [activeNodes, setActiveNodes] = useState<unknown[]>([]); const [activeNodes, setActiveNodes] = useState<MindMapNode[]>([]);
const [painterMode, setPainterMode] = useState(false); const [painterMode, setPainterMode] = useState(false);
const [showMiniMap, setShowMiniMap] = useState(false); const [showMiniMap, setShowMiniMap] = useState(false);
const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null); const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null);
@@ -264,11 +290,10 @@ const MindmapBlockView = ({
const docId = useMemo( const docId = useMemo(
() => () =>
block.props.docId || block.props.docId ||
currentDocumentId ||
(typeof window !== "undefined" (typeof window !== "undefined"
? window.location.pathname.split("/").pop() ?? "" ? window.location.pathname.split("/").pop() ?? ""
: ""), : ""),
[block.props.docId, currentDocumentId], [block.props.docId],
); );
useEffect(() => { useEffect(() => {
@@ -387,8 +412,10 @@ const MindmapBlockView = ({
if (document.fullscreenElement) return; if (document.fullscreenElement) return;
// 必须在“用户手势回调”中同步调用,避免 Fullscreen API 被浏览器拒绝 // 必须在“用户手势回调”中同步调用,避免 Fullscreen API 被浏览器拒绝
try { try {
const target = document.documentElement as any; const target = document.documentElement as unknown as {
const p = target?.requestFullscreen?.(); requestFullscreen?: () => Promise<void>;
};
const p = target.requestFullscreen?.();
if (p && typeof p.catch === "function") { if (p && typeof p.catch === "function") {
p.catch(() => { p.catch(() => {
// ignore:失败则保持 Portal 伪全屏 // ignore:失败则保持 Portal 伪全屏
@@ -457,14 +484,14 @@ const MindmapBlockView = ({
if (!mm || !mindmapReadyRef.current) return; if (!mm || !mindmapReadyRef.current) return;
const target = e.target as HTMLElement | null; const target = e.target as HTMLElement | null;
const editClasses = (mm as any)?.editNodeClassList as string[] | undefined; const editClasses = mm.editNodeClassList;
const wrapper = wrapperRef.current; const wrapper = wrapperRef.current;
const isInWrapper = Boolean(wrapper && target && wrapper.contains(target)); const isInWrapper = Boolean(wrapper && target && wrapper.contains(target));
if (target && isInWrapper) { if (target && isInWrapper) {
const tag = target.tagName; const tag = target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return; if (tag === "INPUT" || tag === "TEXTAREA") return;
// contenteditable 输入中不拦截:避免破坏节点文本编辑/粘贴 // contenteditable 输入中不拦截:避免破坏节点文本编辑/粘贴
if ((target as any).isContentEditable) return; if (target.isContentEditable) return;
const editableAncestor = target.closest?.('[contenteditable="true"]'); const editableAncestor = target.closest?.('[contenteditable="true"]');
if (editableAncestor) return; if (editableAncestor) return;
if (editClasses && target.classList) { if (editClasses && target.classList) {
@@ -484,11 +511,11 @@ const MindmapBlockView = ({
}; };
const pickTargetNode = (inst: MindMapInstance) => { const pickTargetNode = (inst: MindMapInstance) => {
const renderer = inst.renderer as any; const renderer = inst.renderer;
const active: any[] = renderer?.activeNodeList ?? []; const active = renderer.activeNodeList ?? [];
const last: any[] = renderer?.lastActiveNodeList ?? []; const last = renderer.lastActiveNodeList ?? [];
const root = renderer?.root ?? renderer?.renderTree?._node ?? null; const root = renderer.root ?? renderer.renderTree?._node ?? null;
return (active && active[0]) || (last && last[0]) || root || null; return active[0] ?? last[0] ?? root ?? null;
}; };
if (key === "Enter" && !e.shiftKey && !e.altKey && !isMod) { if (key === "Enter" && !e.shiftKey && !e.altKey && !isMod) {
@@ -517,8 +544,7 @@ const MindmapBlockView = ({
stop(); stop();
const inst = ensureActiveBefore(mm); const inst = ensureActiveBefore(mm);
if (!inst) return; if (!inst) return;
const renderer = inst.renderer as any; inst.renderer.copy?.();
if (typeof renderer?.copy === "function") renderer.copy();
return; return;
} }
@@ -526,13 +552,13 @@ const MindmapBlockView = ({
stop(); stop();
const inst = ensureActiveBefore(mm); const inst = ensureActiveBefore(mm);
if (!inst) return; if (!inst) return;
const renderer = inst.renderer as any; const renderer = inst.renderer;
const copyData = renderer?.beingCopyData ?? null; const copyData = renderer.beingCopyData ?? null;
if (copyData) { if (copyData) {
inst.execCommand?.("PASTE_NODE", copyData); inst.execCommand?.("PASTE_NODE", copyData);
return; return;
} }
if (typeof renderer?.paste === "function") renderer.paste(); renderer.paste?.();
} }
}; };
@@ -614,9 +640,8 @@ const MindmapBlockView = ({
if (!mindmap || activeNodes.length > 0) return; if (!mindmap || activeNodes.length > 0) return;
const rootNode = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node; const rootNode = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node;
if (rootNode) { if (rootNode) {
setActiveNodes([rootNode]); setActiveNodes([rootNode as MindMapNode]);
mindmap.renderer.activeNodeList = [rootNode]; mindmap.renderer.activeNodeList = [rootNode];
// @ts-expect-error 第三方库字段
mindmap.renderer.lastActiveNodeList = [rootNode]; mindmap.renderer.lastActiveNodeList = [rootNode];
mindmap.emit?.("node_active", rootNode, [rootNode]); mindmap.emit?.("node_active", rootNode, [rootNode]);
} }
@@ -695,33 +720,41 @@ const MindmapBlockView = ({
if (destroyed) return; if (destroyed) return;
// 兜底修补 simple-mind-map 在创建富文本节点时 host/缓存未就绪导致的空指针 // 兜底修补 simple-mind-map 在创建富文本节点时 host/缓存未就绪导致的空指针
const MindMapNodeCtor = (MindMapNodeModule as any)?.default; type MindMapNodeCtorLike = {
prototype: {
__wolaiRichtextPatched?: boolean;
createRichTextNode?: (...args: unknown[]) => unknown;
};
};
const MindMapNodeCtor = (MindMapNodeModule as unknown as { default?: MindMapNodeCtorLike })?.default;
if ( if (
MindMapNodeCtor && MindMapNodeCtor &&
!MindMapNodeCtor.prototype.__wolaiRichtextPatched !MindMapNodeCtor.prototype.__wolaiRichtextPatched
) { ) {
const originalCreate = MindMapNodeCtor.prototype.createRichTextNode; const originalCreate = MindMapNodeCtor.prototype.createRichTextNode;
MindMapNodeCtor.prototype.__wolaiRichtextPatched = true; MindMapNodeCtor.prototype.__wolaiRichtextPatched = true;
// @ts-expect-error 第三方库原型扩展
MindMapNodeCtor.prototype.createRichTextNode = function patched(...args: unknown[]) { MindMapNodeCtor.prototype.createRichTextNode = function patched(...args: unknown[]) {
type RichtextThis = {
mindMap?: { el?: HTMLElement | null; commonCaches?: Record<string, unknown> } | null;
};
const self = this as unknown as RichtextThis;
// host 兜底:优先使用实例容器,否则退回 body // host 兜底:优先使用实例容器,否则退回 body
const host: HTMLElement = const host: HTMLElement = (self?.mindMap?.el as HTMLElement | null) ?? document.body;
(this?.mindMap?.el as HTMLElement | null) ?? document.body; if (!self.mindMap) {
if (!this.mindMap) { self.mindMap = { el: host, commonCaches: {} };
this.mindMap = { el: host, commonCaches: {} } as any;
} }
if (!this.mindMap.el || typeof (this.mindMap.el as any).appendChild !== "function") { const el = self.mindMap.el;
this.mindMap.el = host; if (!el || typeof el.appendChild !== "function") {
self.mindMap.el = host;
} }
if (!this.mindMap.commonCaches) { const caches = self.mindMap.commonCaches ?? (self.mindMap.commonCaches = {});
this.mindMap.commonCaches = {}; const measureKey = "measureRichtextNodeTextSizeEl";
} if (!caches[measureKey]) {
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
const measureDiv = document.createElement("div"); const measureDiv = document.createElement("div");
measureDiv.style.position = "fixed"; measureDiv.style.position = "fixed";
measureDiv.style.left = "-999999px"; measureDiv.style.left = "-999999px";
(this.mindMap.el ?? document.body).appendChild(measureDiv); (self.mindMap.el ?? document.body).appendChild(measureDiv);
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl = measureDiv; caches[measureKey] = measureDiv;
} }
if (typeof originalCreate === "function") { if (typeof originalCreate === "function") {
return originalCreate.apply(this, args); return originalCreate.apply(this, args);
@@ -759,15 +792,17 @@ const MindmapBlockView = ({
console.warn(`思维导图插件加载失败:${name}`); console.warn(`思维导图插件加载失败:${name}`);
return; return;
} }
// @ts-expect-error 第三方库缺少类型定义 const MindMapCtor = MindMap as unknown as {
if (MindMap.hasPlugin(plugin) === -1) { hasPlugin?: (p: unknown) => number;
usePlugin?: (p: unknown) => void;
};
const hasPlugin = MindMapCtor.hasPlugin;
const notRegistered =
typeof hasPlugin === "function" ? hasPlugin(plugin) === -1 : true;
const registerPlugin = MindMapCtor.usePlugin;
if (notRegistered && typeof registerPlugin === "function") {
console.log(`注册插件: ${name}`); console.log(`注册插件: ${name}`);
const registerPlugin = registerPlugin(plugin);
// @ts-expect-error simple-mind-map 插件注册无类型声明
(MindMap as { usePlugin?: (p: unknown) => void }).usePlugin;
if (registerPlugin) {
registerPlugin(plugin);
}
} }
}); });
@@ -785,7 +820,22 @@ const MindmapBlockView = ({
hostEl.innerHTML = ""; hostEl.innerHTML = "";
} }
const instance = new MindMap({ type MindMapConstructor = new (options: {
el: HTMLElement;
data: unknown;
theme: string;
layout: string;
mousewheelAction: string;
enableFreeDrag: boolean;
enableCtrlKeyNodeSelection: boolean;
fit: boolean;
useLeftKeySelectionRightKeyDrag: boolean;
createNewNodeBehavior: string;
iconList: unknown[];
}) => MindMapInstance;
const MindMapCtor = MindMap as unknown as MindMapConstructor;
const instance = new MindMapCtor({
el: hostEl, el: hostEl,
data: initialDataRef.current, data: initialDataRef.current,
theme: "classic", theme: "classic",
@@ -802,22 +852,25 @@ const MindmapBlockView = ({
...nodeIconList, ...nodeIconList,
...(iconConfig as unknown[]), ...(iconConfig as unknown[]),
]), ]),
}) as MindMapInstance; });
createdInstance = instance; createdInstance = instance;
mindmapRef.current = instance; mindmapRef.current = instance;
// 尽早标记为可用:主页面通过 `/mind` 插入后,若依赖 render_end/尺寸探测,可能导致按钮永远不可用 // 尽早标记为可用:主页面通过 `/mind` 插入后,若依赖 render_end/尺寸探测,可能导致按钮永远不可用
//render_end 可能早于监听注册触发,或容器尺寸长时间为 0)。命令本身不需要等待 render_end。 //render_end 可能早于监听注册触发,或容器尺寸长时间为 0)。命令本身不需要等待 render_end。
mindmapReadyRef.current = true; mindmapReadyRef.current = true;
// 确保测量节点的缓存容器始终挂在有效 DOM 上,避免 appendChild 空指针 // 确保测量节点的缓存容器始终挂在有效 DOM 上,避免 appendChild 空指针
if (!(instance as any).commonCaches) { type MindMapWithCaches = MindMapInstance & { commonCaches?: Record<string, unknown> };
(instance as any).commonCaches = {}; const instanceWithCaches = instance as MindMapWithCaches;
if (!instanceWithCaches.commonCaches) {
instanceWithCaches.commonCaches = {};
} }
if (!(instance as any).commonCaches.measureRichtextNodeTextSizeEl) { const measureKey = "measureRichtextNodeTextSizeEl";
if (!instanceWithCaches.commonCaches[measureKey]) {
const measureDiv = document.createElement("div"); const measureDiv = document.createElement("div");
measureDiv.style.position = "fixed"; measureDiv.style.position = "fixed";
measureDiv.style.left = "-999999px"; measureDiv.style.left = "-999999px";
((instance as any).commonCaches).measureRichtextNodeTextSizeEl = measureDiv; instanceWithCaches.commonCaches[measureKey] = measureDiv;
(hostEl ?? document.body).appendChild(measureDiv); hostEl.appendChild(measureDiv);
} }
instance.setMode?.("edit"); instance.setMode?.("edit");
const centerAndFit = (retry = 0) => { const centerAndFit = (retry = 0) => {
@@ -833,11 +886,7 @@ const MindmapBlockView = ({
//(尤其在首次构造时同步渲染),因此不要完全依赖 render_end 来标记就绪。 //(尤其在首次构造时同步渲染),因此不要完全依赖 render_end 来标记就绪。
mindmapReadyRef.current = true; mindmapReadyRef.current = true;
const renderer = instance.renderer; const renderer = instance.renderer;
const rootNode = const rootNode = renderer?.root ?? renderer?.renderTree?._node;
// @ts-expect-error 第三方库字段
renderer?.root ??
// @ts-expect-error 第三方库字段
renderer?.renderTree?._node;
if (rootNode && renderer?.setRootNodeCenter) { if (rootNode && renderer?.setRootNodeCenter) {
renderer.setRootNodeCenter(); renderer.setRootNodeCenter();
} }
@@ -855,7 +904,6 @@ const MindmapBlockView = ({
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// 便于开发阶段在控制台直接调试实例 // 便于开发阶段在控制台直接调试实例
// @ts-expect-error 调试用全局变量
window.__mindmapInstance = instance; window.__mindmapInstance = instance;
} }
@@ -892,28 +940,28 @@ const MindmapBlockView = ({
} }
}; };
instance.on?.("render_end", onRenderEndOnce); instance.on?.("render_end", onRenderEndOnce);
instance.on?.("node_active", (_node: unknown, list: unknown[]) => { instance.on?.("node_active", (_node: unknown, list: unknown[]) => {
if (!list || list.length === 0) return; if (!list || list.length === 0) return;
setActiveNodes(list || []); setActiveNodes((list || []) as MindMapNode[]);
}); });
instance.on?.("node_click", (node: unknown) => { instance.on?.("node_click", (node: unknown) => {
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致) // 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
// @ts-expect-error 第三方库节点对象
const renderer = instance.renderer; const renderer = instance.renderer;
if (typeof (node as any)?.active === "function") { const nodeWithActive = node as unknown as { active?: () => void };
// @ts-expect-error simple-mind-map 节点对象缺少类型 if (typeof nodeWithActive.active === "function") {
node.active(); nodeWithActive.active();
} else { } else {
// 兜底:手动维护激活列表 // 兜底:手动维护激活列表
// @ts-expect-error simple-mind-map 渲染器缺少类型 if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(node, true);
// @ts-expect-error simple-mind-map 渲染器缺少类型 if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(node);
renderer?.addNodeToActiveList && renderer.addNodeToActiveList(node, true);
// @ts-expect-error simple-mind-map 渲染器缺少类型
renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(node);
} }
const list = renderer?.activeNodeList ?? []; const list = renderer?.activeNodeList ?? [];
setActiveNodes(list.length === 0 && node ? [node] : (list as unknown[])); setActiveNodes(
list.length === 0 && node
? [node as MindMapNode]
: (list as MindMapNode[]),
);
}); });
instance.on?.("painter_start", () => setPainterMode(true)); instance.on?.("painter_start", () => setPainterMode(true));
instance.on?.("painter_end", () => setPainterMode(false)); instance.on?.("painter_end", () => setPainterMode(false));
@@ -927,19 +975,19 @@ const MindmapBlockView = ({
const rootNode = getRootNode(instance); const rootNode = getRootNode(instance);
if (rootNode) { if (rootNode) {
// 初始化时主动标记根节点为选中,确保后续插入子节点有合法的父节点 // 初始化时主动标记根节点为选中,确保后续插入子节点有合法的父节点
renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
renderer?.addNodeToActiveList && renderer.addNodeToActiveList(rootNode, true); if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(rootNode, true);
renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(rootNode); if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(rootNode);
setActiveNodes([rootNode]); setActiveNodes([rootNode as MindMapNode]);
} }
// 若首次渲染时 root 仍未就绪,设置兜底延迟激活 // 若首次渲染时 root 仍未就绪,设置兜底延迟激活
window.setTimeout(() => { window.setTimeout(() => {
const root = getRootNode(instance); const root = getRootNode(instance);
if (!root) return; if (!root) return;
renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
renderer?.addNodeToActiveList && renderer.addNodeToActiveList(root, true); if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(root, true);
renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(root); if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(root);
setActiveNodes([root]); setActiveNodes([root as MindMapNode]);
}, 120); }, 120);
if (destroyed) { if (destroyed) {
@@ -981,9 +1029,7 @@ const MindmapBlockView = ({
// ignore // ignore
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// @ts-expect-error 调试用全局变量
if (window.__mindmapInstance === createdInstance) { if (window.__mindmapInstance === createdInstance) {
// @ts-expect-error 调试用全局变量
window.__mindmapInstance = null; window.__mindmapInstance = null;
} }
} }
@@ -997,8 +1043,8 @@ const MindmapBlockView = ({
const getInstanceCandidate = () => const getInstanceCandidate = () =>
mindmap ?? mindmap ??
(typeof window !== "undefined" (typeof window !== "undefined"
? // @ts-expect-error 调试用全局变量 ? ((window as unknown as { __mindmapInstance?: MindMapInstance | null })
(window.__mindmapInstance as MindMapInstance | undefined | null) .__mindmapInstance ?? null)
: null); : null);
const runWhenReady = ( const runWhenReady = (
@@ -1030,11 +1076,16 @@ const MindmapBlockView = ({
const pickActiveOrRoot = (mm: MindMapInstance) => { const pickActiveOrRoot = (mm: MindMapInstance) => {
const list = mm.renderer?.activeNodeList; const list = mm.renderer?.activeNodeList;
const last = (mm.renderer as any)?.lastActiveNodeList; const last = mm.renderer?.lastActiveNodeList;
// @ts-expect-error 第三方库类型缺失
return (list && list[0]) || (last && last[0]) || getRootNode(mm) || null; return (list && list[0]) || (last && last[0]) || getRootNode(mm) || null;
}; };
const getNodeUid = (node: unknown): string | null => {
const maybeNode = node as unknown as { getData?: (key: string) => unknown; uid?: unknown };
const raw = typeof maybeNode.getData === "function" ? maybeNode.getData("uid") : maybeNode.uid;
return typeof raw === "string" && raw ? raw : null;
};
const handleUndo = () => runWhenReady((mm) => mm.execCommand("BACK")); const handleUndo = () => runWhenReady((mm) => mm.execCommand("BACK"));
const handleRedo = () => runWhenReady((mm) => mm.execCommand("FORWARD")); const handleRedo = () => runWhenReady((mm) => mm.execCommand("FORWARD"));
const handlePainter = () => { const handlePainter = () => {
@@ -1049,11 +1100,7 @@ const MindmapBlockView = ({
execWithReflow((mm) => { execWithReflow((mm) => {
const target = pickActiveOrRoot(mm); const target = pickActiveOrRoot(mm);
if (!target) return; if (!target) return;
const targetUid = const targetUid = getNodeUid(target);
// @ts-expect-error simple-mind-map 节点缺少类型
(typeof (target as any)?.getData === "function"
? (target as any).getData("uid")
: (target as any)?.uid) ?? null;
mm.execCommand?.("SET_NODE_ACTIVE", target, true); mm.execCommand?.("SET_NODE_ACTIVE", target, true);
// openEdit=false:保持与 createNewNodeBehavior=activeOnly 的策略一致,避免强制进入编辑模式带来的时序问题 // openEdit=false:保持与 createNewNodeBehavior=activeOnly 的策略一致,避免强制进入编辑模式带来的时序问题
mm.execCommand?.("INSERT_NODE", false, [target]); mm.execCommand?.("INSERT_NODE", false, [target]);
@@ -1062,13 +1109,15 @@ const MindmapBlockView = ({
// 这里尝试在下一帧将新插入的节点设为激活(插入同级节点时,新节点位于 target 后)。 // 这里尝试在下一帧将新插入的节点设为激活(插入同级节点时,新节点位于 target 后)。
window.setTimeout(() => { window.setTimeout(() => {
try { try {
const renderer = mm.renderer as any; const renderer = mm.renderer;
if ((renderer?.activeNodeList?.length ?? 0) > 0) return; if ((renderer.activeNodeList?.length ?? 0) > 0) return;
if (!targetUid || typeof renderer?.findNodeByUid !== "function") return; if (!targetUid || typeof renderer.findNodeByUid !== "function") return;
const currentTarget = renderer.findNodeByUid(targetUid); const currentTarget = renderer.findNodeByUid(targetUid) as unknown as {
const parent = currentTarget?.parent; parent?: { children?: unknown[] } | null;
const siblings: any[] = parent?.children ?? []; };
const idx = siblings.indexOf(currentTarget); const parent = currentTarget?.parent ?? null;
const siblings = (parent?.children ?? []) as unknown[];
const idx = siblings.indexOf(currentTarget as unknown);
const inserted = idx >= 0 ? siblings[idx + 1] : null; const inserted = idx >= 0 ? siblings[idx + 1] : null;
if (!inserted) return; if (!inserted) return;
renderer?.clearActiveNodeList?.(); renderer?.clearActiveNodeList?.();
@@ -1085,11 +1134,7 @@ const MindmapBlockView = ({
execWithReflow((mm) => { execWithReflow((mm) => {
const parent = pickActiveOrRoot(mm); const parent = pickActiveOrRoot(mm);
if (!parent) return; if (!parent) return;
const parentUid = const parentUid = getNodeUid(parent);
// @ts-expect-error simple-mind-map 节点缺少类型
(typeof (parent as any)?.getData === "function"
? (parent as any).getData("uid")
: (parent as any)?.uid) ?? null;
mm.execCommand?.("SET_NODE_ACTIVE", parent, true); mm.execCommand?.("SET_NODE_ACTIVE", parent, true);
// openEdit=false:同上,避免插入瞬间强制进入编辑导致异常 // openEdit=false:同上,避免插入瞬间强制进入编辑导致异常
mm.execCommand?.("INSERT_CHILD_NODE", false, [parent]); mm.execCommand?.("INSERT_CHILD_NODE", false, [parent]);
@@ -1097,11 +1142,13 @@ const MindmapBlockView = ({
// 兜底:确保新插入的子节点成为激活节点,保证随后点击“同级节点/删除节点”可用 // 兜底:确保新插入的子节点成为激活节点,保证随后点击“同级节点/删除节点”可用
window.setTimeout(() => { window.setTimeout(() => {
try { try {
const renderer = mm.renderer as any; const renderer = mm.renderer;
if ((renderer?.activeNodeList?.length ?? 0) > 0) return; if ((renderer.activeNodeList?.length ?? 0) > 0) return;
if (!parentUid || typeof renderer?.findNodeByUid !== "function") return; if (!parentUid || typeof renderer.findNodeByUid !== "function") return;
const currentParent = renderer.findNodeByUid(parentUid); const currentParent = renderer.findNodeByUid(parentUid) as unknown as {
const children: any[] = currentParent?.children ?? []; children?: unknown[];
};
const children = (currentParent?.children ?? []) as unknown[];
const inserted = children.length > 0 ? children[children.length - 1] : null; const inserted = children.length > 0 ? children[children.length - 1] : null;
if (!inserted) return; if (!inserted) return;
renderer?.clearActiveNodeList?.(); renderer?.clearActiveNodeList?.();
@@ -1182,18 +1229,23 @@ const MindmapBlockView = ({
const handler = (node: MindMapNode, e?: Event) => { const handler = (node: MindMapNode, e?: Event) => {
e?.stopPropagation?.(); e?.stopPropagation?.();
e?.preventDefault?.(); e?.preventDefault?.();
const src = const srcRaw =
node?.nodeData?.data?.image || node?.nodeData?.data?.image ||
node?.getData?.("image") || node?.getData?.("image") ||
node?.data?.image; node?.data?.image;
const src = typeof srcRaw === "string" ? srcRaw : "";
if (src) { if (src) {
setViewerSrc(src); setViewerSrc(src);
const size = node?.getData?.("imageSize") || {}; const sizeRaw = node?.getData?.("imageSize");
const size =
sizeRaw && typeof sizeRaw === "object"
? (sizeRaw as { width?: unknown; height?: unknown })
: ({} as { width?: unknown; height?: unknown });
const title = node?.getData?.("imageTitle") || ""; const title = node?.getData?.("imageTitle") || "";
setViewerMeta({ setViewerMeta({
title: typeof title === "string" ? title : "", title: typeof title === "string" ? title : "",
width: Number(size?.width) || undefined, width: Number(size.width) || undefined,
height: Number(size?.height) || undefined, height: Number(size.height) || undefined,
}); });
setShowImageViewer(true); setShowImageViewer(true);
} }
@@ -1377,10 +1429,10 @@ const MindmapBlockView = ({
if (ext === "xmind") { if (ext === "xmind") {
const xmindParser = await import("simple-mind-map/src/parse/xmind.js"); const xmindParser = await import("simple-mind-map/src/parse/xmind.js");
const blob = new Blob([await file.arrayBuffer()]); const blob = new Blob([await file.arrayBuffer()]);
const data = await xmindParser.default.parseXmindFile(blob, (content: unknown[]) => { const data = await xmindParser.default.parseXmindFile(blob, (content) => {
const list = Array.isArray(content) ? content : []; const list = content;
if (list.length > 1) { if (list.length > 1) {
window.alert("检测到 XMind 多画布,自动导入第一个画布。"); window.alert("检测到 XMind 多画布,自动导入第一个画布。");
} }
return list.length > 0 ? list[0] : content; return list.length > 0 ? list[0] : content;
}); });
@@ -1773,10 +1825,9 @@ const MindmapBlockView = ({
activeSidebar={activeSidebar} activeSidebar={activeSidebar}
onSelect={setActiveSidebar} onSelect={setActiveSidebar}
/> />
{/* @ts-expect-error mindmap type */}
<MindmapSidebar <MindmapSidebar
mindmap={mindmap} mindmap={mindmap}
activeNodes={activeNodes} // @ts-expect-error type activeNodes={activeNodes}
activeTab={activeSidebar} activeTab={activeSidebar}
onClose={() => setActiveSidebar(null)} onClose={() => setActiveSidebar(null)}
/> />
@@ -1795,7 +1846,6 @@ const MindmapBlockView = ({
miniMapOpen={showMiniMap} miniMapOpen={showMiniMap}
/> />
<MindmapCount mindmap={mindmap} /> <MindmapCount mindmap={mindmap} />
{/* @ts-expect-error mindmap type */}
<MindmapMiniMap mindmap={mindmap} show={showMiniMap} /> <MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
{imgToolbar} {imgToolbar}
{noteModal} {noteModal}
@@ -1854,14 +1904,13 @@ const MindmapBlockView = ({
contentEditable={false} contentEditable={false}
/> />
<MindmapSidebarTrigger <MindmapSidebarTrigger
activeSidebar={activeSidebar} activeSidebar={activeSidebar}
onSelect={setActiveSidebar} onSelect={setActiveSidebar}
/> />
{/* @ts-expect-error mindmap type */} <MindmapSidebar
<MindmapSidebar mindmap={mindmap}
mindmap={mindmap} activeNodes={activeNodes}
activeNodes={activeNodes} // @ts-expect-error type
activeTab={activeSidebar} activeTab={activeSidebar}
onClose={() => setActiveSidebar(null)} onClose={() => setActiveSidebar(null)}
/> />
@@ -1880,7 +1929,6 @@ const MindmapBlockView = ({
miniMapOpen={showMiniMap} miniMapOpen={showMiniMap}
/> />
<MindmapCount mindmap={mindmap} /> <MindmapCount mindmap={mindmap} />
{/* @ts-expect-error mindmap type */}
<MindmapMiniMap mindmap={mindmap} show={showMiniMap} /> <MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
{imgToolbar} {imgToolbar}
{noteModal} {noteModal}
@@ -1893,6 +1941,8 @@ const MindmapBlockView = ({
export { MindmapBlockView }; export { MindmapBlockView };
type MindmapBlockViewProps = Parameters<typeof MindmapBlockView>[0];
export const mindmapBlock = createReactBlockSpec( export const mindmapBlock = createReactBlockSpec(
{ {
type: "mindmap", type: "mindmap",
@@ -1901,8 +1951,10 @@ export const mindmapBlock = createReactBlockSpec(
data: { default: defaultMindmapData }, data: { default: defaultMindmapData },
}, },
content: "none", content: "none",
}, } as unknown as BlockConfig<"mindmap", PropSchema, "none">,
{ {
render: (props) => <MindmapBlockView {...props} />, render: (props) => (
<MindmapBlockView {...(props as unknown as MindmapBlockViewProps)} />
),
}, },
); );