返回 presentation-ai
onHoverNode.ts
1 import {
2 DndPlugin,
3 type DragItemNode,
4 type ElementDragItemNode,
5 } from "@platejs/dnd";
6 import { NodeApi, PathApi, type Path, type TElement } from "platejs";
7 import { type PlateEditor } from "platejs/react";
8 import { type DropTargetMonitor } from "react-dnd";
9
10 import { type UseDropNodeOptions } from "../hooks/useDropNode";
11 import { getActiveFreeformDropTarget } from "../utils/freeformDrop";
12 import { getDropPath } from "../utils/getDropPath";
13 import { getHoverDirection } from "../utils/getHoverDirection";
14
15 /**
16 * Callback called when dragging a node and hovering nodes.
17 * Updates the dropTarget to show the drop line indicator.
18 */
19 export const onHoverNode = (
20 editor: PlateEditor,
21 {
22 canDropNode,
23 dragItem,
24 element,
25 monitor,
26 nodeRef,
27 }: {
28 dragItem: DragItemNode;
29 monitor: DropTargetMonitor;
30 } & Pick<UseDropNodeOptions, "canDropNode" | "element" | "nodeRef">,
31 ) => {
32 const { dropTarget } = editor.getOptions(DndPlugin);
33 const currentId = dropTarget?.id ?? null;
34 const currentLine = dropTarget?.line ?? "";
35 const freeformTarget = getActiveFreeformDropTarget(editor);
36
37 if (freeformTarget) {
38 if (editor.api.isExpanded()) {
39 editor.tf.focus();
40 editor.tf.collapse();
41 }
42
43 return;
44 }
45
46 // Check if the drop would actually move the node
47 const result = getDropPath(editor, {
48 canDropNode,
49 dragItem,
50 element,
51 monitor,
52 nodeRef,
53 });
54
55 // If getDropPath returns undefined, try to bubble up to a root-level parent
56 if (!result) {
57 // Get the hover direction first
58 const direction = getHoverDirection({
59 dragItem,
60 element,
61 monitor,
62 nodeRef,
63 });
64
65 // Only bubble up for left/right directions when dragging a root element over nested content
66 if (direction === "left" || direction === "right") {
67 const dragItemNode = dragItem as ElementDragItemNode;
68 const dragPath = dragItemNode.element
69 ? editor.api.findPath(dragItemNode.element)
70 : undefined;
71
72 // If dragging a root-level element
73 if (dragPath && dragPath.length === 1) {
74 const hoveredPath = editor.api.findPath(element);
75
76 // If hovering over a nested element (not root level)
77 if (hoveredPath && hoveredPath.length > 1) {
78 // Find the root-level parent
79 const rootPath: Path = [hoveredPath[0] as number];
80 const rootElement = NodeApi.get(editor, rootPath) as
81 | TElement
82 | undefined;
83
84 if (
85 rootElement &&
86 rootElement.id !== (dragItemNode.element?.id as string)
87 ) {
88 // Show dropline on the root-level parent
89 const newDropTarget = {
90 id: rootElement.id as string,
91 line: direction,
92 };
93
94 if (
95 newDropTarget.id !== currentId ||
96 newDropTarget.line !== currentLine
97 ) {
98 editor.setOption(DndPlugin, "dropTarget", newDropTarget);
99 }
100 return;
101 }
102 }
103 }
104 }
105
106 // No valid drop target found, clear the dropline
107 if (currentId || currentLine) {
108 editor.setOption(DndPlugin, "dropTarget", { id: null, line: "" });
109 }
110 return;
111 }
112
113 const { direction } = result;
114 const newDropTarget = { id: element.id as string, line: direction };
115
116 if (newDropTarget.id !== currentId || newDropTarget.line !== currentLine) {
117 // For top positioning, adjust to show line at bottom of previous element
118 // This makes the visual indicator appear between elements
119 if (newDropTarget.line === "top") {
120 const previousPath = PathApi.previous(editor.api.findPath(element)!);
121
122 if (!previousPath) {
123 return editor.setOption(DndPlugin, "dropTarget", newDropTarget);
124 }
125
126 const prevNode = NodeApi.get(editor, previousPath!);
127
128 editor.setOption(DndPlugin, "dropTarget", {
129 id: prevNode?.id as string,
130 line: "bottom",
131 });
132
133 return;
134 }
135
136 // For left/right, show dropline on the actual hovered element
137 // (no redirection needed - left shows left line, right shows right line)
138 editor.setOption(DndPlugin, "dropTarget", newDropTarget);
139 }
140
141 // Collapse selection if expanded during drag
142 if (direction && editor.api.isExpanded()) {
143 editor.tf.focus();
144 editor.tf.collapse();
145 }
146 };
147
147 lines TYPESCRIPT