feat(mindmap): 引入官方工具栏/侧栏并修复节点选择
This commit is contained in:
@@ -7,6 +7,12 @@ import type { Block, BlockNoteEditor } from "@blocknote/core";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import type { CustomBlockSchema } from "../schema";
|
||||
import { MindmapToolbar } from "./MindmapToolbar";
|
||||
import { MindmapSidebar } from "./MindmapSidebar";
|
||||
import { MindmapSidebarTrigger } from "./MindmapSidebarTrigger";
|
||||
import { MindmapNavigator } from "./MindmapNavigator";
|
||||
import { MindmapMiniMap } from "./MindmapMiniMap";
|
||||
import { MindmapCount } from "./MindmapCount";
|
||||
import type { SidebarPanel } from "./mindmapSidebarConfig";
|
||||
|
||||
type MindMapInstance = {
|
||||
execCommand: (...args: unknown[]) => void;
|
||||
@@ -14,11 +20,21 @@ type MindMapInstance = {
|
||||
setData: (data: unknown) => void;
|
||||
getData: (withConfig?: boolean) => unknown;
|
||||
on?: (event: string, handler: (...args: unknown[]) => void) => void;
|
||||
emit?: (event: string, ...args: unknown[]) => void;
|
||||
off?: (event: string, handler: (...args: unknown[]) => void) => void;
|
||||
setMode?: (mode: string) => void;
|
||||
command: { clearHistory: () => void };
|
||||
view: { fit: () => void };
|
||||
renderer: { activeNodeList: unknown[] };
|
||||
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 };
|
||||
painter?: { startPainter: () => void };
|
||||
doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise<unknown> };
|
||||
width: number;
|
||||
height: number;
|
||||
miniMap?: unknown;
|
||||
getThemeConfig: (key: string) => unknown;
|
||||
setThemeConfig: (config: unknown) => void;
|
||||
setTheme: (theme: string) => void;
|
||||
setLayout: (layout: string) => void;
|
||||
};
|
||||
|
||||
const defaultMindmapData = {
|
||||
@@ -48,11 +64,22 @@ const applyToActiveNodes = (
|
||||
mindmap: MindMapInstance | null,
|
||||
handler: (node: unknown) => void,
|
||||
) => {
|
||||
if (!mindmap?.renderer?.activeNodeList || mindmap.renderer.activeNodeList.length === 0) {
|
||||
window.alert("请选择至少一个节点再执行该操作");
|
||||
if (!mindmap) {
|
||||
window.alert("思维导图尚未初始化");
|
||||
return;
|
||||
}
|
||||
mindmap.renderer.activeNodeList.forEach(handler);
|
||||
const list = mindmap.renderer?.activeNodeList;
|
||||
if (!list || list.length === 0) {
|
||||
// 尝试兜底选中根节点
|
||||
const root = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node;
|
||||
if (root) {
|
||||
mindmap.execCommand?.("SET_NODE_ACTIVE", root, true);
|
||||
} else {
|
||||
window.alert("请选择至少一个节点再执行该操作");
|
||||
return;
|
||||
}
|
||||
}
|
||||
(mindmap.renderer?.activeNodeList ?? []).forEach(handler);
|
||||
};
|
||||
|
||||
const MindmapBlockView = ({
|
||||
@@ -69,8 +96,17 @@ const MindmapBlockView = ({
|
||||
const [mindmap, setMindmap] = useState<MindMapInstance | null>(null);
|
||||
const [canBack, setCanBack] = useState(false);
|
||||
const [canForward, setCanForward] = useState(false);
|
||||
const [activeCount, setActiveCount] = useState(1);
|
||||
const [activeNodes, setActiveNodes] = useState<unknown[]>([]);
|
||||
const activeCount = activeNodes.length;
|
||||
const [painterMode, setPainterMode] = useState(false);
|
||||
const [showMiniMap, setShowMiniMap] = useState(false);
|
||||
const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null);
|
||||
|
||||
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
||||
const instance = mm ?? mindmap;
|
||||
return instance?.renderer?.root ?? instance?.renderer?.renderTree?._node ?? null;
|
||||
}, [mindmap]);
|
||||
|
||||
const autosaveKey = useMemo(() => `${STORAGE_PREFIX}${block.id}`, [block.id]);
|
||||
const initialDataRef = useRef<unknown>(null);
|
||||
if (initialDataRef.current === null) {
|
||||
@@ -99,6 +135,19 @@ const MindmapBlockView = ({
|
||||
persistData(data);
|
||||
}, 500);
|
||||
|
||||
// 初始选中根节点,后续不强制抢焦点,允许用户自由选择
|
||||
useEffect(() => {
|
||||
if (!mindmap || activeNodes.length > 0) return;
|
||||
const rootNode = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node;
|
||||
if (rootNode) {
|
||||
setActiveNodes([rootNode]);
|
||||
mindmap.renderer.activeNodeList = [rootNode];
|
||||
// @ts-expect-error 第三方库字段
|
||||
mindmap.renderer.lastActiveNodeList = [rootNode];
|
||||
mindmap.emit?.("node_active", rootNode, [rootNode]);
|
||||
}
|
||||
}, [mindmap, activeNodes.length]);
|
||||
|
||||
useEffect(() => {
|
||||
let destroyed = false;
|
||||
(async () => {
|
||||
@@ -112,6 +161,10 @@ const MindmapBlockView = ({
|
||||
{ default: Formula },
|
||||
{ default: FormulaStyle },
|
||||
{ default: RichText },
|
||||
{ default: MiniMapPlugin },
|
||||
{ default: Select },
|
||||
{ default: Drag },
|
||||
{ default: KeyboardNavigation },
|
||||
] = await Promise.all([
|
||||
import("simple-mind-map"),
|
||||
import("simple-mind-map/src/plugins/Painter.js"),
|
||||
@@ -121,9 +174,13 @@ const MindmapBlockView = ({
|
||||
import("simple-mind-map/src/plugins/Formula.js"),
|
||||
import("simple-mind-map/src/plugins/FormulaStyle.js"),
|
||||
import("simple-mind-map/src/plugins/RichText.js"),
|
||||
import("simple-mind-map/src/plugins/MiniMap.js"),
|
||||
import("simple-mind-map/src/plugins/Select.js"),
|
||||
import("simple-mind-map/src/plugins/Drag.js"),
|
||||
import("simple-mind-map/src/plugins/KeyboardNavigation.js"),
|
||||
]);
|
||||
|
||||
[
|
||||
const plugins = [
|
||||
{ name: "Painter", plugin: Painter },
|
||||
{ name: "AssociativeLine", plugin: AssociativeLine },
|
||||
{ name: "OuterFrame", plugin: OuterFrame },
|
||||
@@ -131,15 +188,21 @@ const MindmapBlockView = ({
|
||||
{ name: "Formula", plugin: Formula },
|
||||
{ name: "FormulaStyle", plugin: FormulaStyle },
|
||||
{ name: "RichText", plugin: RichText },
|
||||
].forEach(({ name, plugin }) => {
|
||||
{ name: "MiniMap", plugin: MiniMapPlugin },
|
||||
{ name: "Select", plugin: Select },
|
||||
{ name: "Drag", plugin: Drag },
|
||||
{ name: "KeyboardNavigation", plugin: KeyboardNavigation },
|
||||
];
|
||||
|
||||
plugins.forEach(({ name, plugin }) => {
|
||||
if (!plugin) {
|
||||
console.warn(`思维导图插件加载失败:${name}`);
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
if (MindMap.hasPlugin(plugin) === -1) {
|
||||
console.log(`注册插件: ${name}`);
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
MindMap.usePlugin(plugin);
|
||||
}
|
||||
});
|
||||
@@ -153,7 +216,9 @@ const MindmapBlockView = ({
|
||||
enableFreeDrag: true,
|
||||
enableCtrlKeyNodeSelection: true,
|
||||
fit: true,
|
||||
useLeftKeySelectionRightKeyDrag: true,
|
||||
}) as MindMapInstance;
|
||||
instance.setMode?.("edit");
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// 便于开发阶段在控制台直接调试实例
|
||||
@@ -161,40 +226,42 @@ const MindmapBlockView = ({
|
||||
window.__mindmapInstance = instance;
|
||||
}
|
||||
|
||||
instance.execCommand("RESET_LAYOUT");
|
||||
setMindmap(instance);
|
||||
|
||||
instance.on?.("back_forward", (index: number, len: number) => {
|
||||
setCanBack(index > 0);
|
||||
setCanForward(index < len - 1);
|
||||
});
|
||||
instance.on?.("node_active", (_node: unknown, list: unknown[]) => {
|
||||
console.debug("node_active", list?.length ?? 0);
|
||||
setActiveCount(list.length);
|
||||
instance.on?.("node_active", (_node: unknown, list: unknown[]) => {
|
||||
if (!list || list.length === 0) return;
|
||||
setActiveNodes(list || []);
|
||||
});
|
||||
instance.on?.("node_click", (node: unknown) => {
|
||||
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
|
||||
// @ts-expect-error 第三方库节点对象
|
||||
if (typeof node?.active === "function") {
|
||||
// @ts-expect-error 第三方库节点对象
|
||||
node.active();
|
||||
} else {
|
||||
// 兜底:手动维护激活列表
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
instance.renderer?.clearActiveNodeList?.();
|
||||
// @ts-expect-error
|
||||
instance.renderer?.addNodeToActiveList?.(node, true);
|
||||
// @ts-expect-error
|
||||
instance.renderer?.emitNodeActiveEvent?.(node);
|
||||
}
|
||||
const list = instance.renderer?.activeNodeList ?? [];
|
||||
setActiveNodes(list.length === 0 && node ? [node] : (list as unknown[]));
|
||||
});
|
||||
instance.on?.("painter_start", () => setPainterMode(true));
|
||||
instance.on?.("painter_end", () => setPainterMode(false));
|
||||
instance.on?.("data_change", (data: unknown) => {
|
||||
debouncedPersist(data);
|
||||
});
|
||||
const rootNode = instance.renderer?.renderTree?._node;
|
||||
if (rootNode && (!instance.renderer.activeNodeList || instance.renderer.activeNodeList.length === 0)) {
|
||||
instance.renderer.activeNodeList = [rootNode];
|
||||
}
|
||||
const initialActive =
|
||||
(instance.renderer && instance.renderer.activeNodeList && instance.renderer.activeNodeList.length) ||
|
||||
(rootNode ? 1 : 0);
|
||||
if (initialActive > 0) {
|
||||
setActiveCount(initialActive);
|
||||
}
|
||||
const rootNode = getRootNode(instance);
|
||||
if (rootNode) {
|
||||
setTimeout(() => {
|
||||
if (instance.renderer) {
|
||||
instance.renderer.activeNodeList = [rootNode];
|
||||
instance.emit?.("node_active", rootNode, [rootNode]);
|
||||
setActiveCount(1);
|
||||
}
|
||||
}, 0);
|
||||
setActiveNodes([rootNode]);
|
||||
}
|
||||
|
||||
if (destroyed) {
|
||||
@@ -221,12 +288,36 @@ const MindmapBlockView = ({
|
||||
}
|
||||
mindmap.painter.startPainter();
|
||||
};
|
||||
const handleSibling = () => mindmap?.execCommand("INSERT_NODE");
|
||||
const handleChild = () => mindmap?.execCommand("INSERT_CHILD_NODE");
|
||||
const handleDelete = () => mindmap?.execCommand("REMOVE_NODE");
|
||||
const handleSummary = () => mindmap?.execCommand("ADD_GENERALIZATION");
|
||||
const handleAssociativeLine = () => mindmap?.execCommand("ADD_ASSOCIATIVE_LINE");
|
||||
const handleOuterFrame = () => mindmap?.execCommand("ADD_OUTER_FRAME");
|
||||
const handleSibling = () => {
|
||||
if (!mindmap) return;
|
||||
window.setTimeout(() => mindmap.execCommand("INSERT_NODE"), 0);
|
||||
};
|
||||
const handleChild = () => {
|
||||
if (!mindmap) return;
|
||||
window.setTimeout(() => mindmap.execCommand("INSERT_CHILD_NODE"), 0);
|
||||
};
|
||||
const handleDelete = () => {
|
||||
if (!mindmap) return;
|
||||
window.setTimeout(() => mindmap.execCommand("REMOVE_NODE"), 0);
|
||||
};
|
||||
const handleSummary = () => {
|
||||
const mm = ensureActiveBefore();
|
||||
if (mm) {
|
||||
window.setTimeout(() => mm.execCommand("ADD_GENERALIZATION"), 0);
|
||||
}
|
||||
};
|
||||
const handleAssociativeLine = () => {
|
||||
const mm = ensureActiveBefore();
|
||||
if (mm) {
|
||||
window.setTimeout(() => mm.execCommand("ADD_ASSOCIATIVE_LINE"), 0);
|
||||
}
|
||||
};
|
||||
const handleOuterFrame = () => {
|
||||
const mm = ensureActiveBefore();
|
||||
if (mm) {
|
||||
window.setTimeout(() => mm.execCommand("ADD_OUTER_FRAME"), 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImage = () => {
|
||||
const url = createSimplePrompt("请输入图片链接");
|
||||
@@ -373,8 +464,29 @@ const MindmapBlockView = ({
|
||||
<div className="fixed left-1/2 top-4 z-20 flex w-fit -translate-x-1/2 transform items-center justify-center gap-4">
|
||||
<MindmapToolbar {...toolbarProps} />
|
||||
</div>
|
||||
<div className="absolute inset-0">
|
||||
<div className="relative h-full w-full">
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
|
||||
<MindmapSidebarTrigger
|
||||
activeSidebar={activeSidebar}
|
||||
onSelect={setActiveSidebar}
|
||||
/>
|
||||
{/* @ts-expect-error mindmap type */}
|
||||
<MindmapSidebar
|
||||
mindmap={mindmap}
|
||||
activeNodes={activeNodes} // @ts-expect-error type
|
||||
activeTab={activeSidebar}
|
||||
onClose={() => setActiveSidebar(null)}
|
||||
/>
|
||||
<MindmapNavigator
|
||||
mindmap={mindmap}
|
||||
fullscreen={fullscreen}
|
||||
onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
|
||||
miniMapOpen={showMiniMap}
|
||||
/>
|
||||
<MindmapCount mindmap={mindmap} />
|
||||
{/* @ts-expect-error mindmap type */}
|
||||
<MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -389,6 +501,27 @@ const MindmapBlockView = ({
|
||||
</div>
|
||||
<div className="relative h-[520px] w-full overflow-hidden bg-white">
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
|
||||
<MindmapSidebarTrigger
|
||||
activeSidebar={activeSidebar}
|
||||
onSelect={setActiveSidebar}
|
||||
/>
|
||||
{/* @ts-expect-error mindmap type */}
|
||||
<MindmapSidebar
|
||||
mindmap={mindmap}
|
||||
activeNodes={activeNodes} // @ts-expect-error type
|
||||
activeTab={activeSidebar}
|
||||
onClose={() => setActiveSidebar(null)}
|
||||
/>
|
||||
<MindmapNavigator
|
||||
mindmap={mindmap}
|
||||
fullscreen={fullscreen}
|
||||
onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
|
||||
miniMapOpen={showMiniMap}
|
||||
/>
|
||||
<MindmapCount mindmap={mindmap} />
|
||||
{/* @ts-expect-error mindmap type */}
|
||||
<MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -407,4 +540,4 @@ export const mindmapBlock = createReactBlockSpec(
|
||||
{
|
||||
render: (props) => <MindmapBlockView {...props} />,
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user