| 1 | /** biome-ignore-all lint/suspicious/noExplicitAny: This use requires any */ |
| 2 | import { |
| 3 | type DragItemNode, |
| 4 | type DropDirection, |
| 5 | type ElementDragItemNode, |
| 6 | } from "@platejs/dnd"; |
| 7 | import { type TElement } from "platejs"; |
| 8 | import { type DropTargetMonitor, type XYCoord } from "react-dnd"; |
| 9 | |
| 10 | export interface GetHoverDirectionOptions { |
| 11 | dragItem: DragItemNode; |
| 12 | |
| 13 | /** Hovering node. */ |
| 14 | element: TElement; |
| 15 | |
| 16 | monitor: DropTargetMonitor; |
| 17 | |
| 18 | /** The node ref of the node being dragged. */ |
| 19 | nodeRef: any; |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * If dragging a node A over another node B: get the direction of node A |
| 24 | * relative to node B based on mouse position. |
| 25 | * |
| 26 | * Always detects all 4 directions (top/bottom/left/right). |
| 27 | * Uses edge zones for left/right detection with fallback to vertical. |
| 28 | */ |
| 29 | export const getHoverDirection = ({ |
| 30 | dragItem, |
| 31 | element, |
| 32 | monitor, |
| 33 | nodeRef, |
| 34 | }: GetHoverDirectionOptions): DropDirection => { |
| 35 | if (!nodeRef.current) return; |
| 36 | |
| 37 | // Don't replace items with themselves |
| 38 | if (element === (dragItem as ElementDragItemNode).element) return; |
| 39 | |
| 40 | // For multiple node drag, don't show drop line if hovering over any selected element |
| 41 | const elementDragItem = dragItem as ElementDragItemNode; |
| 42 | const draggedIds = Array.isArray(elementDragItem.id) |
| 43 | ? elementDragItem.id |
| 44 | : [elementDragItem.id]; |
| 45 | if (draggedIds.includes(element.id as string)) return; |
| 46 | |
| 47 | const HORIZONTAL_THRESHOLD = 40; |
| 48 | |
| 49 | const hoverBoundingRect = nodeRef.current?.getBoundingClientRect(); |
| 50 | if (!hoverBoundingRect) return; |
| 51 | |
| 52 | const clientOffset = monitor.getClientOffset(); |
| 53 | if (!clientOffset) return; |
| 54 | |
| 55 | const hoverClientX = (clientOffset as XYCoord).x - hoverBoundingRect.left; |
| 56 | const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; |
| 57 | |
| 58 | // Check for left edge zone |
| 59 | if (hoverClientX < HORIZONTAL_THRESHOLD) { |
| 60 | return "left"; |
| 61 | } |
| 62 | |
| 63 | // Check for right edge zone |
| 64 | const hoverMiddleX = hoverBoundingRect.width / 2; |
| 65 | if (hoverClientX > hoverMiddleX + HORIZONTAL_THRESHOLD) { |
| 66 | return "right"; |
| 67 | } |
| 68 | |
| 69 | // Default: vertical direction based on mouse Y position |
| 70 | const hoverMiddleY = hoverBoundingRect.height / 2; |
| 71 | return hoverClientY < hoverMiddleY ? "top" : "bottom"; |
| 72 | }; |
| 73 |