返回 presentation-ai
useAntvInfographicMutationSync.ts
根目录 / src / hooks / presentation / infographic / useAntvInfographicMutationSync.ts
1 "use client";
2
3 import {
4 type Infographic,
5 type InfographicOptions,
6 type ParsedInfographicOptions,
7 } from "@antv/infographic";
8 import debounce from "lodash.debounce";
9 import { type PlateEditor } from "platejs/react";
10 import {
11 useEffect,
12 useMemo,
13 type MutableRefObject,
14 type RefObject,
15 } from "react";
16
17 import { type TAntvInfographicElement } from "@/components/notebook/presentation/editor/plugins/antv-infographic-plugin";
18 import { syncInfographicSyntaxWithData } from "@/components/notebook/presentation/editor/utils/infographic-utils";
19 import { findInfographicEntryById } from "@/hooks/presentation/infographic/findInfographicNode";
20 import {
21 cloneSerializableOptions,
22 getSerializableOptionsKey,
23 pickSerializableOptions,
24 toSerializableOptionsFromParsed,
25 } from "./infographic-options";
26
27 type MutationSyncParams = {
28 infographicRef: RefObject<Infographic | null>;
29 editor: PlateEditor;
30 elementRef: RefObject<TAntvInfographicElement>;
31 skipNextRenderRef?: MutableRefObject<boolean>;
32 syntax: string;
33 };
34
35 export function useAntvInfographicMutationSync({
36 infographicRef,
37 editor,
38 elementRef,
39 skipNextRenderRef,
40 syntax,
41 }: MutationSyncParams) {
42 const debouncedSave = useMemo(
43 () =>
44 debounce((options: Partial<InfographicOptions>) => {
45 const stableData = cloneSerializableOptions(options);
46 let currentElement: TAntvInfographicElement | undefined;
47 let path: ReturnType<typeof editor.api.findPath>;
48
49 try {
50 path = editor.api.findPath(elementRef.current);
51 currentElement = elementRef.current;
52 } catch {
53 const elementId =
54 typeof elementRef.current.id === "string"
55 ? elementRef.current.id
56 : "";
57 const entry = findInfographicEntryById(editor, elementId);
58 currentElement = entry?.[0];
59 path = entry?.[1];
60 }
61
62 if (!path || !currentElement) {
63 return;
64 }
65
66 const syncedSyntax = syncInfographicSyntaxWithData(
67 currentElement.syntax ?? "",
68 stableData,
69 );
70 const nextSyntax = syncedSyntax || currentElement.syntax;
71 const currentDataKey = getSerializableOptionsKey(currentElement.data);
72 const nextDataKey = getSerializableOptionsKey(stableData);
73
74 if (
75 currentDataKey === nextDataKey &&
76 nextSyntax === currentElement.syntax
77 ) {
78 return;
79 }
80
81 const update: Partial<TAntvInfographicElement> = { data: stableData };
82
83 if (nextSyntax !== currentElement.syntax) {
84 update.syntax = nextSyntax;
85 }
86
87 if (skipNextRenderRef) {
88 skipNextRenderRef.current = true;
89 }
90
91 editor.tf.setNodes(update, { at: path });
92 }, 1000),
93 [editor, elementRef, skipNextRenderRef],
94 );
95
96 // Cancel any pending debounced save when syntax changes externally
97 // (e.g., template conversion). This prevents stale data from reverting the change.
98 useEffect(() => {
99 debouncedSave.cancel();
100 }, [syntax, debouncedSave]);
101
102 useEffect(() => {
103 return () => {
104 debouncedSave.flush();
105 debouncedSave.cancel();
106 };
107 }, [debouncedSave]);
108
109 useEffect(() => {
110 const instance = infographicRef.current;
111 if (!instance) return;
112
113 const handleOptionsChange = () => {
114 const internalEditor = (instance as unknown as { editor?: unknown })
115 .editor as
116 | {
117 state?: { getOptions?: () => Partial<ParsedInfographicOptions> };
118 }
119 | undefined;
120 const parsedOptions = internalEditor?.state?.getOptions?.();
121 const options = parsedOptions
122 ? toSerializableOptionsFromParsed(parsedOptions)
123 : pickSerializableOptions(instance.getOptions());
124 debouncedSave(options);
125 };
126
127 instance.on("options:change", handleOptionsChange);
128
129 return () => {
130 instance.off("options:change", handleOptionsChange);
131 };
132 }, [infographicRef, debouncedSave]);
133 }
134
134 lines TYPESCRIPT