返回 presentation-ai
LayoutImageDrop.tsx
根目录 / src / components / notebook / presentation / editor / dnd / components / LayoutImageDrop.tsx
1 import { DRAG_ITEM_BLOCK } from "@platejs/dnd";
2 import { ImagePlugin } from "@platejs/media/react";
3 import { type TElement } from "platejs";
4 import { useEditorRef, type PlateEditor } from "platejs/react";
5 import { useEffect, useRef } from "react";
6 import { useDrop } from "react-dnd";
7
8 import { type LayoutType } from "@/components/notebook/presentation/utils/parser";
9 import { cn } from "@/lib/utils";
10 import { usePresentationState } from "@/states/presentation-state";
11 import { isChartType } from "../../lib";
12
13 const canDropElement = (item: { element: TElement }) => {
14 if (!item?.element) return false;
15 return (
16 item.element.type === ImagePlugin.key || isChartType(item.element.type)
17 );
18 };
19
20 function removeNodeById(editor: PlateEditor, element: TElement) {
21 const path = editor.api.findPath(element);
22
23 if (!path) return;
24 editor.tf.removeNodes({ at: path });
25 return element;
26 }
27
28 export default function LayoutImageDrop({ slideId }: { slideId: string }) {
29 // Create drop zones for top, left, and right
30 const topRef = useRef<HTMLDivElement>(null);
31 const leftRef = useRef<HTMLDivElement>(null);
32 const rightRef = useRef<HTMLDivElement>(null);
33 const editor = useEditorRef();
34 const isReorderingSlides = usePresentationState((s) => s.isReorderingSlides);
35
36 // Check if element is image or chart
37
38 const handleDrop = (item: { element: TElement }, layoutType: LayoutType) => {
39 if (!item?.element) return;
40
41 const elementType = item.element.type;
42 const isImage = elementType === ImagePlugin.key;
43 const isChart = isChartType(elementType);
44
45 if (!isImage && !isChart) return;
46
47 // Get the current slides state
48 const { slides, setSlides, setCurrentSlideId } =
49 usePresentationState.getState();
50
51 if (isImage) {
52 // Handle image drop
53 let imageUrl = item.element.url as string;
54 let imageQuery = item.element.query as string;
55
56 // Check if the image is from the editor and needs to be removed
57 const element = removeNodeById(editor, item.element);
58 if (element?.url) imageUrl = element.url as string;
59 if (element?.query) imageQuery = element.query as string;
60
61 // Update the slides array with the new root image and layout type
62 // When dropping an image, we keep any existing chart data but set the image URL
63 // This allows restoring the chart later if needed
64 const updatedSlides = slides.map((slide) => {
65 if (slide.id === slideId) {
66 const existingRootImage = slide.rootImage;
67 return {
68 ...slide,
69 rootImage: {
70 ...existingRootImage,
71 url: imageUrl,
72 query: imageQuery,
73 imageSource: undefined,
74 embedType: undefined,
75 // Keep chartType, chartData, chartOptions but they won't be shown since url is set
76 // Reset size when layout changes so default dimensions are used
77 size: undefined,
78 },
79 layoutType: layoutType,
80 };
81 }
82 return slide;
83 });
84
85 setSlides(updatedSlides);
86 setCurrentSlideId(slideId);
87 } else if (isChart) {
88 // Handle chart drop
89 const chartType = item.element.type;
90 const chartData = (item.element as unknown as { data?: unknown }).data;
91 const chartOptions = {
92 variant: (item.element as unknown as { variant?: string }).variant,
93 disableAnimation: true,
94 };
95
96 // Remove the chart element from its original position
97 removeNodeById(editor, item.element);
98
99 // Update the slides array with the chart data
100 const updatedSlides = slides.map((slide) => {
101 if (slide.id === slideId) {
102 return {
103 ...slide,
104 rootImage: {
105 ...slide.rootImage,
106 chartType,
107 chartData,
108 chartOptions,
109 // Clear any existing image/embed data
110 url: undefined,
111 embedType: undefined,
112 imageSource: undefined,
113 query: slide.rootImage?.query ?? "",
114 // Reset size when layout changes so default dimensions are used
115 size: undefined,
116 },
117 layoutType: layoutType,
118 };
119 }
120 return slide;
121 });
122
123 setSlides(updatedSlides);
124 setCurrentSlideId(slideId);
125 }
126 };
127
128 // Setup drop zones
129 const [{ isTopOver }, dropTop] = useDrop({
130 accept: [DRAG_ITEM_BLOCK],
131 canDrop: (item: { element: TElement }) =>
132 !isReorderingSlides && canDropElement(item),
133 drop: (item) => {
134 handleDrop(item, "vertical");
135 return { droppedInLayoutZone: true };
136 },
137 collect: (monitor) => {
138 return {
139 isTopOver: monitor.isOver() && monitor.canDrop(),
140 };
141 },
142 });
143
144 const [{ isLeftOver }, dropLeft] = useDrop({
145 accept: [DRAG_ITEM_BLOCK],
146 canDrop: (item: { element: TElement }) =>
147 !isReorderingSlides && canDropElement(item),
148 drop: (item) => {
149 handleDrop(item, "left");
150 return { droppedInLayoutZone: true };
151 },
152 collect: (monitor) => ({
153 isLeftOver: monitor.isOver() && monitor.canDrop(),
154 }),
155 });
156
157 const [{ isRightOver }, dropRight] = useDrop({
158 accept: [DRAG_ITEM_BLOCK],
159 canDrop: (item: { element: TElement }) =>
160 !isReorderingSlides && canDropElement(item),
161 drop: (item) => {
162 handleDrop(item, "right");
163 return { droppedInLayoutZone: true };
164 },
165 collect: (monitor) => ({
166 isRightOver: monitor.isOver() && monitor.canDrop(),
167 }),
168 });
169 useEffect(() => {
170 dropTop(topRef);
171 dropLeft(leftRef);
172 dropRight(rightRef);
173 }, [dropLeft, dropRight, dropTop]);
174
175 return (
176 <>
177 {/* Top drop zone */}
178 <div
179 ref={topRef}
180 className={cn(
181 "absolute top-0 right-0 left-0 z-99999 h-12",
182 isTopOver ? "bg-primary/20" : "bg-transparent",
183 "transition-colors duration-200",
184 )}
185 />
186
187 {/* Left drop zone */}
188 <div
189 ref={leftRef}
190 className={cn(
191 "absolute top-16 bottom-0 left-0 z-99999 w-8",
192 isLeftOver ? "bg-primary/20" : "bg-transparent",
193 "transition-colors duration-200",
194 )}
195 />
196
197 {/* Right drop zone */}
198 <div
199 ref={rightRef}
200 className={cn(
201 "absolute top-16 right-0 bottom-0 z-99999 w-8",
202 isRightOver ? "bg-primary/20" : "bg-transparent",
203 "transition-colors duration-200",
204 )}
205 />
206 </>
207 );
208 }
209
209 lines Plain Text