返回 presentation-ai
useDragNode.ts
根目录 / src / components / notebook / presentation / editor / dnd / hooks / useDragNode.ts
1 import {
2 DndPlugin,
3 type DragItemNode,
4 type ElementDragItemNode,
5 } from "@platejs/dnd";
6 import { type TElement } from "platejs";
7 import { type PlateEditor } from "platejs/react";
8 import React from "react";
9 import {
10 useDrag,
11 type ConnectDragPreview,
12 type ConnectDragSource,
13 type DragSourceHookSpec,
14 } from "react-dnd";
15
16 import { dropNodeAtFreeformTarget } from "../transforms/onDropNode";
17 import { startFreeformDrag, stopFreeformDrag } from "../utils/freeformDrop";
18
19 export interface UseDragNodeOptions extends DragSourceHookSpec<
20 DragItemNode,
21 unknown,
22 { isDragging: boolean }
23 > {
24 disableFreeformDrag?: boolean;
25 element: TElement;
26 }
27
28 function isElementDragItem(
29 dragItem: DragItemNode | undefined,
30 ): dragItem is ElementDragItemNode {
31 return Boolean(dragItem && "id" in dragItem && "element" in dragItem);
32 }
33
34 /**
35 * `useDrag` hook to drag a node from the editor. `item` with `id` is required.
36 *
37 * On drag start:
38 * - Set `isDragging` to true
39 * - Add `dragging` class to `body`
40 *
41 * On drag end:
42 * - Set `isDragging` to false
43 * - Remove `dragging` class from `body`
44 */
45 export const useDragNode = (
46 editor: PlateEditor,
47 {
48 disableFreeformDrag = false,
49 element: staleElement,
50 item,
51 ...options
52 }: UseDragNodeOptions,
53 ): [
54 { isAboutToDrag: boolean; isDragging: boolean },
55 ConnectDragSource,
56 ConnectDragPreview,
57 ] => {
58 const elementId = staleElement.id as string;
59 const [isAboutToDrag, setIsAboutToDrag] = React.useState(false);
60
61 const [collected, dragRef, preview] = useDrag<
62 DragItemNode,
63 unknown,
64 { isDragging: boolean }
65 >(
66 () => ({
67 canDrag: () => {
68 setIsAboutToDrag(true);
69 return true;
70 },
71 collect: (monitor) => ({
72 isDragging: monitor.isDragging(),
73 }),
74 end: (dragItem, monitor) => {
75 if (
76 !disableFreeformDrag &&
77 !monitor.didDrop() &&
78 isElementDragItem(dragItem)
79 ) {
80 dropNodeAtFreeformTarget(editor, dragItem);
81 }
82
83 stopFreeformDrag(editor);
84 editor.setOption(DndPlugin, "isDragging", false);
85 document.body.classList.remove("dragging");
86 setIsAboutToDrag(false);
87 },
88 item(monitor) {
89 editor.setOption(DndPlugin, "isDragging", true);
90 editor.setOption(DndPlugin, "_isOver", true);
91 document.body.classList.add("dragging");
92
93 // Check if multiple nodes are selected
94 const currentDraggingId = editor.getOption(DndPlugin, "draggingId");
95 let id: string[] | string;
96
97 if (
98 Array.isArray(currentDraggingId) &&
99 currentDraggingId.length > 1 &&
100 currentDraggingId.includes(elementId)
101 ) {
102 // Multiple selection including current element
103 id = Array.from(currentDraggingId);
104 } else {
105 // Single element drag
106 id = elementId;
107 editor.setOption(DndPlugin, "draggingId", elementId);
108 }
109
110 // Get the fresh element from the editor, or fall back to the stale element
111 // This handles external/synthetic elements (like RootImage) that don't exist in the editor
112 const nodeEntry = editor.api.node({ id: elementId, at: [] });
113 const element =
114 (nodeEntry?.[0] as TElement | undefined) ?? staleElement;
115
116 const optionItem = typeof item === "function" ? item(monitor) : item;
117 const extraItem =
118 optionItem && typeof optionItem === "object" ? optionItem : {};
119 const dragNodeItem: ElementDragItemNode = {
120 ...extraItem,
121 id,
122 editorId: editor.id,
123 element,
124 };
125
126 if (!disableFreeformDrag) {
127 startFreeformDrag(editor, dragNodeItem);
128 }
129
130 return dragNodeItem;
131 },
132 ...options,
133 }),
134 [disableFreeformDrag, editor, elementId],
135 );
136
137 // Reset isAboutToDrag when drag is cancelled (e.g., ESC key)
138 React.useEffect(() => {
139 if (!collected.isDragging && isAboutToDrag) {
140 setIsAboutToDrag(false);
141 }
142 }, [collected.isDragging, isAboutToDrag]);
143
144 return [{ ...collected, isAboutToDrag }, dragRef, preview];
145 };
146
146 lines TYPESCRIPT