| 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 |