0.1.03 mindmap问题修改
This commit is contained in:
@@ -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)} />
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user