| 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 |