返回 presentation-ai
infographic-data-editor-dialog.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / infographic-data-editor-dialog.tsx
1 "use client";
2
3 import { type InfographicOptions } from "@antv/infographic";
4 import { Network, X } from "lucide-react";
5 import { useEffect, useRef, useState, type SyntheticEvent } from "react";
6
7 import {
8 PRESENTATION_PORTAL_CONTENT_CLASS,
9 PRESENTATION_PORTAL_OVERLAY_CLASS,
10 } from "@/components/presentation/overlay-layers";
11 import { Button } from "@/components/ui/button";
12 import {
13 Credenza,
14 CredenzaContent,
15 CredenzaDescription,
16 CredenzaFooter,
17 CredenzaHeader,
18 CredenzaTitle,
19 } from "@/components/ui/credenza";
20 import {
21 buildInfographicDataFromParsed,
22 parseInfographicTemplate,
23 updateInfographicSyntaxWithParsedData,
24 type InfographicPaletteThemeColors,
25 } from "../utils/infographic-utils";
26 import { InfographicDataEditor } from "./infographic-data-editor";
27 import { InfographicDataPreview } from "./infographic-data-editor/preview";
28 import { type EditableInfographicData } from "./infographic-data-editor/types";
29 import {
30 createEditableInfographicData,
31 toParsedInfographicData,
32 } from "./infographic-data-editor/utils";
33
34 const stopEditorEventPropagation = (event: SyntheticEvent) => {
35 const targetElement = getEventTargetElement(event.target);
36
37 if (targetElement?.closest(INFOGRAPHIC_PREVIEW_INTERACTIVE_SELECTOR)) {
38 return;
39 }
40
41 event.stopPropagation();
42 };
43
44 const INFOGRAPHIC_PREVIEW_INTERACTIVE_SELECTOR =
45 "[data-infographic-preview-interactive='true']";
46
47 function getEventTargetElement(target: EventTarget | null): Element | null {
48 if (target instanceof Element) return target;
49 if (target instanceof Node) return target.parentElement;
50
51 return null;
52 }
53
54 interface InfographicDataEditorDialogProps {
55 data?: Partial<InfographicOptions>;
56 isDark: boolean;
57 onApply: (update: {
58 data: Partial<InfographicOptions>;
59 syntax: string;
60 }) => void;
61 onOpenChange: (open: boolean) => void;
62 open: boolean;
63 syntax: string;
64 themeColors: InfographicPaletteThemeColors | null;
65 }
66
67 export function InfographicDataEditorDialog({
68 data,
69 isDark,
70 onApply,
71 onOpenChange,
72 open,
73 syntax,
74 themeColors,
75 }: InfographicDataEditorDialogProps) {
76 const contentRef = useRef<HTMLDivElement>(null);
77 const [localData, setLocalData] = useState<EditableInfographicData>(() =>
78 createEditableInfographicData({ options: data, syntax }),
79 );
80
81 useEffect(() => {
82 if (!open) return;
83 setLocalData((current) =>
84 createEditableInfographicData({
85 options: data,
86 previousData: current,
87 syntax,
88 }),
89 );
90 }, [data, open, syntax]);
91
92 const handleCancel = () => {
93 setLocalData(createEditableInfographicData({ options: data, syntax }));
94 onOpenChange(false);
95 };
96
97 const handleSave = () => {
98 const parsed = toParsedInfographicData(localData);
99 const nextSyntax = updateInfographicSyntaxWithParsedData(syntax, parsed);
100 const nextInfographicData = buildInfographicDataFromParsed(parsed);
101 const template =
102 data?.template ?? parseInfographicTemplate(syntax) ?? undefined;
103
104 onApply({
105 syntax: nextSyntax,
106 data: {
107 ...(data ?? {}),
108 template,
109 data: nextInfographicData,
110 },
111 });
112 onOpenChange(false);
113 };
114
115 return (
116 <Credenza modal={false} open={open} onOpenChange={onOpenChange}>
117 <CredenzaContent
118 ref={contentRef}
119 overlayClassName={PRESENTATION_PORTAL_OVERLAY_CLASS}
120 shouldHaveClose={false}
121 onPointerDown={stopEditorEventPropagation}
122 onMouseDown={stopEditorEventPropagation}
123 onClick={stopEditorEventPropagation}
124 onKeyDown={stopEditorEventPropagation}
125 onInteractOutside={(event) => {
126 const target = getEventTargetElement(event.target);
127 if (
128 target?.closest(
129 [
130 ".antv-infographic-toolbar-floating",
131 ".icon-picker-panel",
132 "[data-radix-popper-content-wrapper]",
133 ].join(", "),
134 )
135 ) {
136 event.preventDefault();
137 }
138 }}
139 className={`${PRESENTATION_PORTAL_CONTENT_CLASS} ignore-click-outside/toolbar pointer-events-auto flex h-[92dvh] max-h-[92dvh] w-screen max-w-none flex-col overflow-hidden rounded-t-xl p-0 sm:rounded-xl md:h-[calc(100dvh-3rem)] md:max-h-[calc(100dvh-3rem)] md:rounded-xl`}
140 >
141 <CredenzaHeader className="shrink-0 border-b px-4 py-3 sm:px-5 sm:py-4">
142 <div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
143 <div className="min-w-0">
144 <CredenzaTitle className="flex items-center gap-2">
145 <Network className="size-5" />
146 Edit Infographic Data
147 </CredenzaTitle>
148 <CredenzaDescription className="line-clamp-2">
149 Add, remove, reorder, and edit the data driving this
150 infographic.
151 </CredenzaDescription>
152 </div>
153 <Button
154 variant="ghost"
155 className="size-9 p-0"
156 onClick={handleCancel}
157 aria-label="Close infographic data editor"
158 >
159 <X className="size-4" />
160 </Button>
161 </div>
162 </CredenzaHeader>
163
164 <div className="grid min-h-0 flex-1 grid-rows-[minmax(220px,34dvh)_minmax(0,1fr)] overflow-hidden lg:grid-cols-[minmax(340px,0.95fr)_minmax(460px,1.05fr)] lg:grid-rows-none">
165 <section className="min-h-0 border-b bg-muted/20 p-3 sm:p-4 lg:border-r lg:border-b-0 lg:p-5">
166 <InfographicDataPreview
167 data={localData}
168 isDark={isDark}
169 onDataChange={setLocalData}
170 options={data}
171 syntax={syntax}
172 toolbarPortalContainerRef={contentRef}
173 themeColors={themeColors}
174 />
175 </section>
176
177 <section className="min-h-0 overflow-auto p-3 sm:p-4 lg:p-5">
178 <InfographicDataEditor data={localData} onChange={setLocalData} />
179 </section>
180 </div>
181
182 <CredenzaFooter className="shrink-0 border-t px-4 py-3 sm:px-5 sm:py-4">
183 <Button variant="outline" onClick={handleCancel}>
184 Cancel
185 </Button>
186 <Button onClick={handleSave}>Save Changes</Button>
187 </CredenzaFooter>
188 </CredenzaContent>
189 </Credenza>
190 );
191 }
192
192 lines Plain Text