| 1 | "use client"; |
| 2 | |
| 3 | import { |
| 4 | BrushSelect, |
| 5 | ClickSelect, |
| 6 | DblClickEditText, |
| 7 | DragCanvas, |
| 8 | DragElement, |
| 9 | HotkeyHistory, |
| 10 | Infographic, |
| 11 | ResetViewBox, |
| 12 | ResizeElement, |
| 13 | SelectHighlight, |
| 14 | ZoomWheel, |
| 15 | type IInteraction, |
| 16 | type IPlugin, |
| 17 | } from "@antv/infographic"; |
| 18 | import { useEffect, useRef, type RefObject } from "react"; |
| 19 | |
| 20 | import { registerLucideIconLoader } from "./infographic-icon-loader"; |
| 21 | import { |
| 22 | InfographicSelectionPlugin, |
| 23 | type InfographicSelectionPayload, |
| 24 | } from "./InfographicSelectionPlugin"; |
| 25 | |
| 26 | registerLucideIconLoader(); |
| 27 | |
| 28 | type InfographicEditorInstance = { |
| 29 | editor?: { |
| 30 | interaction?: { |
| 31 | clearSelection?: () => void; |
| 32 | }; |
| 33 | }; |
| 34 | }; |
| 35 | |
| 36 | type InfographicInstanceParams = { |
| 37 | containerRef: RefObject<HTMLDivElement | null>; |
| 38 | editable?: boolean; |
| 39 | onSelectionChange?: (payload: InfographicSelectionPayload) => void; |
| 40 | }; |
| 41 | |
| 42 | const INLINE_TEXT_EDITOR_CLASS = "infographic-inline-text-editor"; |
| 43 | const INLINE_TEXT_SHORTCUT_KEYS = new Set(["a", "c", "v", "x"]); |
| 44 | |
| 45 | const isNativeInlineTextShortcut = (event: KeyboardEvent) => |
| 46 | (event.ctrlKey || event.metaKey) && |
| 47 | !event.altKey && |
| 48 | !event.shiftKey && |
| 49 | INLINE_TEXT_SHORTCUT_KEYS.has(event.key.toLowerCase()); |
| 50 | |
| 51 | const getEventElement = (target: EventTarget | null) => { |
| 52 | if (target instanceof Element) return target; |
| 53 | if (target instanceof Node) return target.parentElement; |
| 54 | return null; |
| 55 | }; |
| 56 | |
| 57 | const getInfographicInlineTextEditor = ( |
| 58 | target: EventTarget | null, |
| 59 | container: HTMLElement, |
| 60 | ) => { |
| 61 | const element = getEventElement(target); |
| 62 | const inlineTextEditor = element?.closest<HTMLElement>( |
| 63 | `.${INLINE_TEXT_EDITOR_CLASS}`, |
| 64 | ); |
| 65 | if (!inlineTextEditor) return null; |
| 66 | if (!inlineTextEditor.isContentEditable) return null; |
| 67 | if (!container.contains(inlineTextEditor)) return null; |
| 68 | |
| 69 | return inlineTextEditor; |
| 70 | }; |
| 71 | |
| 72 | export function useAntvInfographicInstance({ |
| 73 | containerRef, |
| 74 | editable = true, |
| 75 | onSelectionChange, |
| 76 | }: InfographicInstanceParams) { |
| 77 | const infographicRef = useRef<Infographic | null>(null); |
| 78 | |
| 79 | useEffect(() => { |
| 80 | const container = containerRef.current; |
| 81 | if (!container) return; |
| 82 | |
| 83 | const plugins: IPlugin[] = editable |
| 84 | ? [new ResizeElement(), new ResetViewBox()] |
| 85 | : []; |
| 86 | |
| 87 | if (editable && onSelectionChange) { |
| 88 | plugins.unshift(new InfographicSelectionPlugin(onSelectionChange)); |
| 89 | } |
| 90 | |
| 91 | const instance = new Infographic({ |
| 92 | container, |
| 93 | width: "100%", |
| 94 | height: "100%", |
| 95 | editable, |
| 96 | plugins, |
| 97 | interactions: editable |
| 98 | ? ([ |
| 99 | new DragCanvas({ trigger: ["Space"] }), |
| 100 | new DblClickEditText(), |
| 101 | new BrushSelect(), |
| 102 | new ClickSelect(), |
| 103 | new DragElement(), |
| 104 | new HotkeyHistory(), |
| 105 | new ZoomWheel(), |
| 106 | new SelectHighlight(), |
| 107 | ] satisfies IInteraction[]) |
| 108 | : [], |
| 109 | }); |
| 110 | |
| 111 | infographicRef.current = instance; |
| 112 | |
| 113 | const getSvg = () => container.querySelector("svg"); |
| 114 | |
| 115 | const handleDocumentClick = (event: MouseEvent) => { |
| 116 | const target = event.target as Node | null; |
| 117 | if (!target) return; |
| 118 | |
| 119 | const svg = getSvg(); |
| 120 | if (!svg) return; |
| 121 | |
| 122 | const inInfographicContainer = container.contains(target); |
| 123 | const inToolbarOrPanel = |
| 124 | target instanceof Element && |
| 125 | (!!target.closest(".antv-infographic-toolbar-floating") || |
| 126 | !!target.closest(".antv-infographic-template-panel") || |
| 127 | !!target.closest(".icon-picker-panel") || |
| 128 | !!target.closest(".presentation-right-panel") || |
| 129 | // Radix UI portals for dropdowns, popovers, color pickers |
| 130 | !!target.closest("[data-radix-popper-content-wrapper]") || |
| 131 | !!target.closest("[role='dialog']") || |
| 132 | !!target.closest("[data-slot='tooltip-content']")); |
| 133 | |
| 134 | if (!inInfographicContainer && !inToolbarOrPanel) { |
| 135 | ( |
| 136 | instance as unknown as InfographicEditorInstance |
| 137 | ).editor?.interaction?.clearSelection?.(); |
| 138 | } |
| 139 | }; |
| 140 | |
| 141 | // Use bubble phase so toolbar's stopPropagation can prevent this |
| 142 | document.addEventListener("click", handleDocumentClick); |
| 143 | |
| 144 | const handleInlineTextEditorShortcut = (event: KeyboardEvent) => { |
| 145 | if (!isNativeInlineTextShortcut(event)) return; |
| 146 | |
| 147 | const inlineTextEditor = getInfographicInlineTextEditor( |
| 148 | event.target, |
| 149 | container, |
| 150 | ); |
| 151 | if (!inlineTextEditor) return; |
| 152 | |
| 153 | event.stopPropagation(); |
| 154 | event.stopImmediatePropagation(); |
| 155 | }; |
| 156 | |
| 157 | const handleInlineTextEditorClipboard = (event: ClipboardEvent) => { |
| 158 | const inlineTextEditor = getInfographicInlineTextEditor( |
| 159 | event.target, |
| 160 | container, |
| 161 | ); |
| 162 | if (!inlineTextEditor) return; |
| 163 | |
| 164 | event.stopPropagation(); |
| 165 | event.stopImmediatePropagation(); |
| 166 | }; |
| 167 | |
| 168 | container.addEventListener("keydown", handleInlineTextEditorShortcut); |
| 169 | container.addEventListener("copy", handleInlineTextEditorClipboard); |
| 170 | container.addEventListener("cut", handleInlineTextEditorClipboard); |
| 171 | container.addEventListener("paste", handleInlineTextEditorClipboard); |
| 172 | |
| 173 | return () => { |
| 174 | document.removeEventListener("click", handleDocumentClick); |
| 175 | container.removeEventListener("keydown", handleInlineTextEditorShortcut); |
| 176 | container.removeEventListener("copy", handleInlineTextEditorClipboard); |
| 177 | container.removeEventListener("cut", handleInlineTextEditorClipboard); |
| 178 | container.removeEventListener("paste", handleInlineTextEditorClipboard); |
| 179 | |
| 180 | try { |
| 181 | instance.destroy(); |
| 182 | } catch { |
| 183 | // Ignore destruction errors |
| 184 | } |
| 185 | infographicRef.current = null; |
| 186 | }; |
| 187 | }, [containerRef, onSelectionChange, editable]); |
| 188 | |
| 189 | return infographicRef; |
| 190 | } |
| 191 |