返回 presentation-ai
useEditorSurfaceDrop.ts
根目录 / src / components / notebook / presentation / editor / dnd / hooks / useEditorSurfaceDrop.ts
1 import {
2 DndPlugin,
3 DRAG_ITEM_BLOCK,
4 type DragItemNode,
5 type DropLineDirection,
6 type ElementDragItemNode,
7 } from "@platejs/dnd";
8 import { PathApi, type Path, type TElement } from "platejs";
9 import { type PlateEditor } from "platejs/react";
10 import React from "react";
11 import { useDrop, type DropTargetMonitor } from "react-dnd";
12
13 import { applyDropPathResult } from "../transforms/onDropNode";
14 import { canDropAtPath, type DropPathResult } from "../utils/getDropPath";
15
16 type EditorSurfacePlacement = "top" | "bottom";
17
18 type EditorElementDragItem = DragItemNode & {
19 element: TElement;
20 id: string | string[];
21 };
22
23 type EditorSurfaceDropResult = {
24 droppedOnEditorSurface: true;
25 };
26
27 type SurfaceDropTarget = {
28 id: string;
29 line: DropLineDirection;
30 };
31
32 function isEditorElementDragItem(
33 dragItem: DragItemNode | undefined,
34 ): dragItem is EditorElementDragItem {
35 return Boolean(
36 dragItem &&
37 "id" in dragItem &&
38 "element" in dragItem &&
39 typeof dragItem.element === "object" &&
40 dragItem.element !== null,
41 );
42 }
43
44 function getDragIds(dragItem: EditorElementDragItem): string[] {
45 return Array.isArray(dragItem.id) ? dragItem.id : [dragItem.id];
46 }
47
48 function getPrimaryDragPath(
49 editor: PlateEditor,
50 dragItem: EditorElementDragItem,
51 ): Path | undefined {
52 const primaryDragId = getDragIds(dragItem).find(
53 (id) => typeof id === "string",
54 );
55
56 if (!primaryDragId) return undefined;
57
58 const entry = editor.api.node({ id: primaryDragId, at: [] });
59
60 return entry?.[1];
61 }
62
63 function toElementDragItemNode(
64 editor: PlateEditor,
65 dragItem: EditorElementDragItem,
66 ): ElementDragItemNode {
67 return "editorId" in dragItem
68 ? dragItem
69 : {
70 ...dragItem,
71 editorId: editor.id,
72 };
73 }
74
75 function getEditorSurfacePlacement(
76 root: HTMLElement | null,
77 monitor: DropTargetMonitor<DragItemNode, unknown>,
78 ): EditorSurfacePlacement | undefined {
79 const clientOffset = monitor.getClientOffset();
80
81 if (!root || !clientOffset) return undefined;
82
83 const rect = root.getBoundingClientRect();
84
85 return clientOffset.y < rect.top + rect.height / 2 ? "top" : "bottom";
86 }
87
88 function getEditorEdgeDropPathResult(
89 editor: PlateEditor,
90 dragItem: EditorElementDragItem,
91 placement: EditorSurfacePlacement,
92 ): DropPathResult | undefined {
93 const childCount = editor.children.length;
94 const dragPath = getPrimaryDragPath(editor, dragItem);
95
96 if (childCount === 0) {
97 return {
98 createColumns: false,
99 direction: placement,
100 dragPath,
101 hoveredPath: [0],
102 isExternalNode: !dragPath,
103 to: [0],
104 };
105 }
106
107 if (placement === "top") {
108 const firstPath: Path = [0];
109
110 if (dragPath && PathApi.equals(dragPath, firstPath)) return undefined;
111
112 return {
113 createColumns: false,
114 direction: "top",
115 dragPath,
116 hoveredPath: firstPath,
117 isExternalNode: !dragPath,
118 to: firstPath,
119 };
120 }
121
122 const lastPath: Path = [childCount - 1];
123
124 if (dragPath && PathApi.equals(dragPath, lastPath)) return undefined;
125
126 const to =
127 dragPath &&
128 PathApi.isBefore(dragPath, lastPath) &&
129 PathApi.isSibling(dragPath, lastPath)
130 ? lastPath
131 : PathApi.next(lastPath);
132
133 return {
134 createColumns: false,
135 direction: "bottom",
136 dragPath,
137 hoveredPath: lastPath,
138 isExternalNode: !dragPath,
139 to,
140 };
141 }
142
143 function getSurfaceDropTarget(
144 editor: PlateEditor,
145 placement: EditorSurfacePlacement,
146 ): SurfaceDropTarget | null {
147 const childCount = editor.children.length;
148
149 if (childCount === 0) return null;
150
151 const targetPath: Path = placement === "top" ? [0] : [childCount - 1];
152 const target = editor.api.node({ at: targetPath })?.[0];
153 const targetId =
154 typeof target === "object" &&
155 target !== null &&
156 "id" in target &&
157 typeof target.id === "string"
158 ? target.id
159 : undefined;
160
161 if (!targetId) return null;
162
163 return {
164 id: targetId,
165 line: placement,
166 };
167 }
168
169 function useSurfaceDropTargetState(editor: PlateEditor) {
170 const activeSurfaceTargetRef = React.useRef<SurfaceDropTarget | null>(null);
171
172 const clearSurfaceDropTarget = React.useCallback(() => {
173 const activeTarget = activeSurfaceTargetRef.current;
174
175 if (!activeTarget) return;
176
177 const current = editor.getOptions(DndPlugin).dropTarget;
178
179 if (
180 current?.id === activeTarget.id &&
181 current?.line === activeTarget.line
182 ) {
183 editor.setOption(DndPlugin, "dropTarget", { id: null, line: "" });
184 }
185
186 activeSurfaceTargetRef.current = null;
187 }, [editor]);
188
189 const setSurfaceDropTarget = React.useCallback(
190 (target: SurfaceDropTarget | null) => {
191 if (!target) {
192 clearSurfaceDropTarget();
193 return;
194 }
195
196 const current = editor.getOptions(DndPlugin).dropTarget;
197
198 activeSurfaceTargetRef.current = target;
199
200 if (current?.id === target.id && current?.line === target.line) {
201 return;
202 }
203
204 editor.setOption(DndPlugin, "dropTarget", target);
205 },
206 [clearSurfaceDropTarget, editor],
207 );
208
209 return {
210 clearSurfaceDropTarget,
211 setSurfaceDropTarget,
212 };
213 }
214
215 export function useEditorSurfaceDrop({
216 disabled,
217 editor,
218 }: {
219 disabled: boolean;
220 editor: PlateEditor;
221 }): {
222 isEditorSurfaceDropActive: boolean;
223 setEditorSurfaceDropRef: React.RefCallback<HTMLDivElement>;
224 } {
225 const rootRef = React.useRef<HTMLDivElement | null>(null);
226 const { clearSurfaceDropTarget, setSurfaceDropTarget } =
227 useSurfaceDropTargetState(editor);
228
229 const [{ canDrop, isOver }, drop] = useDrop<
230 DragItemNode,
231 EditorSurfaceDropResult | undefined,
232 { canDrop: boolean; isOver: boolean }
233 >(
234 () => ({
235 accept: [DRAG_ITEM_BLOCK],
236 canDrop: (dragItem) => !disabled && isEditorElementDragItem(dragItem),
237 collect: (monitor) => ({
238 canDrop: monitor.canDrop(),
239 isOver: monitor.isOver({ shallow: true }),
240 }),
241 drop: (dragItem, monitor) => {
242 if (
243 monitor.didDrop() ||
244 disabled ||
245 !isEditorElementDragItem(dragItem)
246 ) {
247 return undefined;
248 }
249
250 const placement = getEditorSurfacePlacement(rootRef.current, monitor);
251
252 if (!placement) return undefined;
253
254 const result = getEditorEdgeDropPathResult(editor, dragItem, placement);
255
256 if (!result || !canDropAtPath(editor, dragItem, result)) {
257 return undefined;
258 }
259
260 applyDropPathResult(editor, {
261 dragItem: toElementDragItemNode(editor, dragItem),
262 result,
263 });
264 clearSurfaceDropTarget();
265
266 return { droppedOnEditorSurface: true };
267 },
268 hover: (dragItem, monitor) => {
269 if (
270 disabled ||
271 !monitor.isOver({ shallow: true }) ||
272 !isEditorElementDragItem(dragItem)
273 ) {
274 return;
275 }
276
277 const placement = getEditorSurfacePlacement(rootRef.current, monitor);
278
279 if (!placement) {
280 clearSurfaceDropTarget();
281 return;
282 }
283
284 setSurfaceDropTarget(getSurfaceDropTarget(editor, placement));
285 },
286 }),
287 [clearSurfaceDropTarget, disabled, editor, setSurfaceDropTarget],
288 );
289
290 React.useEffect(() => {
291 if (!isOver || !canDrop) {
292 clearSurfaceDropTarget();
293 }
294 }, [canDrop, clearSurfaceDropTarget, isOver]);
295
296 React.useEffect(() => clearSurfaceDropTarget, [clearSurfaceDropTarget]);
297
298 const setEditorSurfaceDropRef = React.useCallback(
299 (node: HTMLDivElement | null) => {
300 rootRef.current = node;
301 drop(node);
302 },
303 [drop],
304 );
305
306 return {
307 isEditorSurfaceDropActive: isOver && canDrop,
308 setEditorSurfaceDropRef,
309 };
310 }
311
311 lines TYPESCRIPT