返回 presentation-ai
ToolbarContext.tsx
根目录 / src / components / presentation / floating-toolbar / ToolbarContext.tsx
1 "use client";
2
3 import { BlockSelectionPlugin } from "@platejs/selection/react";
4 import { KEYS, NodeApi, PathApi, type TImageElement } from "platejs";
5 import { useEditorRef, usePluginOption } from "platejs/react";
6 import * as React from "react";
7
8 import {
9 type ChartDataMode,
10 type ChartDataType,
11 type SeriesChartType,
12 } from "@/components/notebook/presentation/editor/custom-elements/chart-data-editor-dialog";
13 import {
14 BOX_GROUP,
15 COMPOSED_CHART_ELEMENT,
16 getAlignmentOptions,
17 getAvailableConversionOptions,
18 getChartDataCategory,
19 getColumnSizeOptions,
20 getDirectLayoutToolbarTargetEntry,
21 getElementDisplayName,
22 getOrientationOptions,
23 getSidednessOptions,
24 getVariantOptions,
25 ICON_LIST,
26 isChartType,
27 isLayoutChildType,
28 handleLayoutChange as layoutChange,
29 handleNodePropertyUpdate as nodePropertyUpdate,
30 supportsAlignment,
31 supportsColumnSize,
32 supportsNumbered,
33 supportsOrientation,
34 supportsShowLine,
35 supportsSidedness,
36 supportsVariant,
37 } from "@/components/notebook/presentation/editor/lib";
38 import { PALETTE_DROP_MUTABLE_KEY } from "@/components/notebook/presentation/editor/utils/paletteDrop";
39 import { type MyEditor } from "@/components/plate/editor-kit";
40 import {
41 CALLOUT_VARIANTS,
42 getCalloutVariant,
43 } from "@/components/plate/ui/callout-variants";
44 import {
45 usePresentationState,
46 type ImageEditorMode,
47 } from "@/states/presentation-state";
48
49 export interface ToolbarContextValue {
50 editor: MyEditor;
51 element: Record<string, unknown> | undefined;
52 elementId: string | undefined;
53 elementType: string;
54
55 // Current property values
56 currentOrientation: string;
57 currentSidedness: string;
58 currentNumbered: boolean;
59 currentShowLine: boolean;
60 currentColor: string;
61 currentBackgroundColor: string;
62 currentTextColor: string;
63 currentColumnSize: "sm" | "md" | "lg" | "xl";
64 currentAlignment: "left" | "center" | "right";
65 currentVariant: string;
66
67 // Capability checks
68 supportsOrientationControl: boolean;
69 supportsSidednessControl: boolean;
70 supportsNumberedControl: boolean;
71 supportsShowLineControl: boolean;
72 supportsColumnSizeControl: boolean;
73 supportsAlignmentControl: boolean;
74 supportsVariantControl: boolean;
75 isCurrentElementChart: boolean;
76 isComposedChart: boolean;
77 isImageElement: boolean;
78 isInfographicElement: boolean;
79 isLayoutChildElement: boolean;
80 isMediaEmbedElement: boolean;
81 imageUrl: string | undefined;
82 handleOpenImageEditor: (mode: ImageEditorMode) => void;
83
84 // Chart-specific handler
85 handleOpenChartEditor: () => void;
86
87 // Infographic-specific handler
88 handleOpenInfographicEditor: () => void;
89
90 // Available options
91 orientationOptions: readonly string[];
92 sidednessOptions: readonly string[];
93 columnSizeOptions: readonly string[];
94 alignmentOptions: readonly ("left" | "center" | "right")[];
95 variantOptions: readonly string[];
96 availableOptionsGrouped: ReturnType<typeof getAvailableConversionOptions>;
97 selectedOption: string;
98
99 // Chart-specific
100 chartData: ChartDataType;
101 chartDataType: ChartDataMode;
102 seriesChartTypes: Record<string, SeriesChartType>;
103
104 // Handlers
105 handleNodePropertyUpdate: (
106 property: string,
107 value:
108 | string
109 | boolean
110 | number
111 | Record<string, unknown>
112 | unknown[]
113 | undefined,
114 ) => void;
115 handleLayoutChange: (
116 type: string,
117 additionalData?: Record<string, unknown>,
118 ) => void;
119 handleChartDataUpdate: (newData: ChartDataType) => void;
120 handleSeriesChartTypesUpdate: (
121 types: Record<string, SeriesChartType>,
122 ) => void;
123 }
124
125 const ToolbarContext = React.createContext<ToolbarContextValue | null>(null);
126 const SMART_LAYOUT_COLOR_FALLBACK =
127 "var(--presentation-smart-layout, var(--presentation-primary))";
128
129 function getStringColor(value: unknown): string | undefined {
130 return typeof value === "string" ? value : undefined;
131 }
132
133 export function useToolbarContext() {
134 const context = React.useContext(ToolbarContext);
135 if (!context) {
136 throw new Error("useToolbarContext must be used within a ToolbarProvider");
137 }
138 return context;
139 }
140
141 export function ToolbarProvider({ children }: { children: React.ReactNode }) {
142 const editor = useEditorRef<MyEditor>();
143
144 // Get current selection
145 const selectionIds = usePluginOption(BlockSelectionPlugin, "selectedIds");
146 const selectedElementId = React.useMemo(() => {
147 const firstSelectedId = Array.from(selectionIds ?? [])[0];
148 return typeof firstSelectedId === "string" ? firstSelectedId : undefined;
149 }, [selectionIds]);
150 const [committedElementId, setCommittedElementId] = React.useState<
151 string | undefined
152 >();
153
154 React.useEffect(() => {
155 if (selectedElementId) {
156 setCommittedElementId(selectedElementId);
157 }
158 }, [selectedElementId]);
159
160 const elementId = selectedElementId ?? committedElementId;
161 const targetEntry = getDirectLayoutToolbarTargetEntry(editor, elementId);
162 const [element, elementPath] = targetEntry ?? [];
163
164 // Get current element type and available options
165 const elementType = (element?.type as string) ?? "";
166 const availableOptionsGrouped = React.useMemo(
167 () => getAvailableConversionOptions(elementType),
168 [elementType],
169 );
170
171 // Get display name for current element
172 const selectedOption = React.useMemo(() => {
173 return getElementDisplayName(
174 elementType,
175 element as Record<string, unknown> | undefined,
176 );
177 }, [elementType, element]);
178
179 // Get current properties
180 const currentOrientation =
181 (element as { orientation?: string })?.orientation ??
182 (elementType === ICON_LIST ? "side" : "vertical");
183 const currentSidedness =
184 (element as { sidedness?: string })?.sidedness ?? "single";
185 const currentNumbered =
186 (element as { numbered?: boolean })?.numbered ?? false;
187 const currentShowLine = (element as { showLine?: boolean })?.showLine ?? true;
188 const elementColors = element as
189 | {
190 backgroundColor?: string;
191 color?: string;
192 textColor?: string;
193 variant?: unknown;
194 }
195 | undefined;
196 const calloutVariant = getCalloutVariant(elementColors?.variant);
197 const isLayoutChildElement = isLayoutChildType(elementType);
198 const layoutParentElement =
199 isLayoutChildElement && elementPath && elementPath.length > 0
200 ? NodeApi.get(editor, PathApi.parent(elementPath))
201 : undefined;
202 const parentColor =
203 typeof layoutParentElement === "object" && layoutParentElement !== null
204 ? getStringColor((layoutParentElement as { color?: unknown }).color)
205 : undefined;
206 const currentBackgroundColor =
207 elementColors?.backgroundColor ??
208 (elementType === KEYS.callout
209 ? CALLOUT_VARIANTS[calloutVariant].backgroundColor
210 : "#ffffff");
211 const currentColor =
212 elementColors?.color ?? parentColor ?? SMART_LAYOUT_COLOR_FALLBACK;
213 const currentTextColor =
214 elementColors?.textColor ??
215 elementColors?.color ??
216 (elementType === KEYS.callout
217 ? CALLOUT_VARIANTS[calloutVariant].textColor
218 : "#ffffff");
219 const currentColumnSize =
220 (element as { columnSize?: "sm" | "md" | "lg" | "xl" })?.columnSize ?? "md";
221 const currentAlignment =
222 (element as { alignment?: "left" | "center" | "right" })?.alignment ??
223 (element as { align?: "left" | "center" | "right" })?.align ??
224 "left";
225 // Check capabilities
226 const supportsOrientationControl =
227 supportsOrientation(elementType) &&
228 (elementType !== BOX_GROUP ||
229 (element as { boxType?: string })?.boxType === "alternating");
230 const supportsSidednessControl = supportsSidedness(elementType);
231 const supportsNumberedControl = supportsNumbered(elementType);
232 const supportsShowLineControl = supportsShowLine(elementType);
233 const supportsColumnSizeControl = supportsColumnSize(elementType);
234 const supportsAlignmentControl = supportsAlignment(elementType);
235 const supportsVariantControl = supportsVariant(elementType);
236
237 // Get available options
238 const orientationOptions = getOrientationOptions(elementType);
239 const sidednessOptions = getSidednessOptions(elementType);
240 const columnSizeOptions = getColumnSizeOptions(elementType);
241 const alignmentOptions = getAlignmentOptions(elementType);
242 const variantOptions = getVariantOptions(elementType);
243 const currentVariant =
244 (element as { variant?: string })?.variant ??
245 variantOptions[0] ??
246 "default";
247
248 // Chart data
249 const isCurrentElementChart = isChartType(elementType);
250 const chartData = (element as { data?: unknown })?.data as ChartDataType;
251 const chartDataType: ChartDataMode =
252 (getChartDataCategory(elementType) as ChartDataMode) ?? "multi-series";
253 const isComposedChart = elementType === COMPOSED_CHART_ELEMENT;
254 const seriesChartTypes =
255 (element as { seriesChartTypes?: Record<string, SeriesChartType> })
256 ?.seriesChartTypes ?? {};
257
258 // Image-specific
259 const isImageElement = elementType === "img";
260 const isMediaEmbedElement = elementType === KEYS.mediaEmbed;
261 // Infographic-specific
262 const isInfographicElement = elementType === "antv-infographic";
263 const imageUrl = (element as TImageElement | undefined)?.url;
264 const openPresentationImageEditor = usePresentationState(
265 (s) => s.openPresentationImageEditor,
266 );
267 const setPaletteDropTarget = usePresentationState(
268 (s) => s.setPaletteDropTarget,
269 );
270
271 const handleOpenImageEditor = React.useCallback(
272 (mode: ImageEditorMode) => {
273 if (!element?.id) return;
274 // Create a bound updateElement function that captures the current editor and element
275 const boundUpdateElement = (updateProps: Record<string, unknown>) => {
276 setPaletteDropTarget(null);
277 editor.tf.setNodes(
278 {
279 ...(updateProps as Partial<TImageElement>),
280 [PALETTE_DROP_MUTABLE_KEY]: false,
281 },
282 {
283 at: [],
284 match: (n) => n.id === element.id,
285 },
286 );
287 };
288 // Open the presentation image editor panel with the selected element snapshot.
289 openPresentationImageEditor(
290 mode,
291 boundUpdateElement,
292 element as Record<string, unknown>,
293 );
294 },
295 [editor, element, openPresentationImageEditor, setPaletteDropTarget],
296 );
297
298 // Chart-specific handler
299 const openChartEditor = usePresentationState((s) => s.openChartEditor);
300
301 const handleOpenChartEditor = React.useCallback(() => {
302 if (!element?.id) return;
303
304 // Get chart data from element
305 const chartElement = element as {
306 type?: string;
307 data?: unknown;
308 variant?: string;
309 orientation?: string;
310 interpolation?: string;
311 [key: string]: unknown;
312 };
313
314 // Extract chart options from element
315 const {
316 type,
317 data,
318 id: _id,
319 children: _children,
320 ...chartOptions
321 } = chartElement;
322
323 // Create a bound updateElement function that captures the current editor and element
324 const boundUpdateElement = (updateProps: Record<string, unknown>) => {
325 setPaletteDropTarget(null);
326 editor.tf.setNodes(
327 { ...updateProps, [PALETTE_DROP_MUTABLE_KEY]: false },
328 {
329 at: [],
330 match: (n) => n.id === element.id,
331 },
332 );
333 };
334
335 // Open the chart editor panel with the element data and bound function
336 openChartEditor(
337 {
338 chartType: (type as string) ?? "",
339 chartData: data,
340 chartOptions: chartOptions as Record<string, unknown>,
341 },
342 boundUpdateElement,
343 );
344 }, [editor, element, openChartEditor, setPaletteDropTarget]);
345
346 // Infographic-specific handler
347 const openInfographicEditor = usePresentationState(
348 (s) => s.openInfographicEditor,
349 );
350
351 const handleOpenInfographicEditor = React.useCallback(() => {
352 if (!element?.id) return;
353
354 // Create a bound updateElement function that captures the current editor and element
355 const boundUpdateElement = (updateProps: Record<string, unknown>) => {
356 setPaletteDropTarget(null);
357 editor.tf.setNodes(
358 { ...updateProps, [PALETTE_DROP_MUTABLE_KEY]: false },
359 {
360 at: [],
361 match: (n) => n.id === element.id,
362 },
363 );
364 };
365
366 openInfographicEditor(boundUpdateElement);
367 }, [editor, element, openInfographicEditor, setPaletteDropTarget]);
368
369 // Handlers
370 const handleNodePropertyUpdate = React.useCallback(
371 (
372 property: string,
373 value:
374 | string
375 | boolean
376 | number
377 | Record<string, unknown>
378 | unknown[]
379 | undefined,
380 ) => {
381 const targetElementId =
382 typeof element?.id === "string" ? element.id : elementId;
383
384 setPaletteDropTarget(null);
385 nodePropertyUpdate(editor, property, value, targetElementId);
386 },
387 [editor, element, elementId, setPaletteDropTarget],
388 );
389
390 const handleLayoutChange = React.useCallback(
391 (type: string, additionalData?: Record<string, unknown>) => {
392 const targetElementId =
393 typeof element?.id === "string" ? element.id : elementId;
394
395 setPaletteDropTarget(null);
396 return layoutChange(editor, type, additionalData, targetElementId);
397 },
398 [editor, element, elementId, setPaletteDropTarget],
399 );
400
401 const handleChartDataUpdate = React.useCallback(
402 (newData: ChartDataType) => {
403 if (!element) return;
404 setPaletteDropTarget(null);
405 editor.tf.setNodes(
406 { data: newData, [PALETTE_DROP_MUTABLE_KEY]: false },
407 { at: editor.api.findPath(element) },
408 );
409 },
410 [editor, element, setPaletteDropTarget],
411 );
412
413 const handleSeriesChartTypesUpdate = React.useCallback(
414 (types: Record<string, SeriesChartType>) => {
415 if (!element) return;
416 setPaletteDropTarget(null);
417 editor.tf.setNodes(
418 { seriesChartTypes: types, [PALETTE_DROP_MUTABLE_KEY]: false },
419 { at: editor.api.findPath(element) },
420 );
421 },
422 [editor, element, setPaletteDropTarget],
423 );
424
425 const value: ToolbarContextValue = {
426 editor,
427 element: element as Record<string, unknown> | undefined,
428 elementId,
429 elementType,
430 currentOrientation,
431 currentSidedness,
432 currentNumbered,
433 currentShowLine,
434 currentColor,
435 currentBackgroundColor,
436 currentTextColor,
437 currentColumnSize,
438 currentAlignment,
439 currentVariant,
440 supportsOrientationControl,
441 supportsSidednessControl,
442 supportsNumberedControl,
443 supportsShowLineControl,
444 supportsColumnSizeControl,
445 supportsAlignmentControl,
446 supportsVariantControl,
447 isCurrentElementChart,
448 isComposedChart,
449 isImageElement,
450 isInfographicElement,
451 isLayoutChildElement,
452 isMediaEmbedElement,
453 imageUrl,
454 handleOpenImageEditor,
455 orientationOptions,
456 sidednessOptions,
457 columnSizeOptions,
458 alignmentOptions,
459 variantOptions,
460 availableOptionsGrouped,
461 selectedOption,
462 chartData,
463 chartDataType,
464 seriesChartTypes,
465 handleNodePropertyUpdate,
466 handleLayoutChange,
467 handleChartDataUpdate,
468 handleSeriesChartTypesUpdate,
469 handleOpenChartEditor,
470 handleOpenInfographicEditor,
471 };
472
473 return (
474 <ToolbarContext.Provider value={value}>{children}</ToolbarContext.Provider>
475 );
476 }
477
477 lines Plain Text