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