UNPKG

@deepdub/react-arborist

Version:
212 lines (189 loc) 5.21 kB
import { XYCoord } from "react-dnd"; import { NodeApi } from "../interfaces/node-api"; import { bound, indexOf, isClosed, isItem, isOpenWithEmptyChildren, } from "../utils"; import { DropResult } from "./drop-hook"; function measureHover( el: HTMLElement, offset: XYCoord, hitAreaHeight?: number, disableReorder?: boolean ) { const rect = el.getBoundingClientRect(); const x = offset.x - Math.round(rect.x); const y = offset.y - Math.round(rect.y); const height = rect.height; const pad = hitAreaHeight === undefined ? height / 4 : (height - hitAreaHeight) / 2; if (disableReorder) { return { x, inTopHalf: false, inBottomHalf: false, inMiddle: true, atTop: false, atBottom: false, }; } const inTopHalf = y < height / 2; const inBottomHalf = !inTopHalf; const inMiddle = y >= pad && y <= height - pad; const atTop = !inMiddle && inTopHalf; const atBottom = !inMiddle && inBottomHalf; return { x, inTopHalf, inBottomHalf, inMiddle, atTop, atBottom }; } type HoverData = ReturnType<typeof measureHover>; function getNodesAroundCursor( node: NodeApi | null, prev: NodeApi | null, next: NodeApi | null, hover: HoverData ): [NodeApi | null, NodeApi | null] { if (!node) { // We're hovering over the empty part of the list, not over an item, // Put the cursor below the last item which is "prev" return [prev, null]; } if (node.isInternal) { if (hover.atTop) { return [prev, node]; } else if (hover.inMiddle) { return [node, node]; } else { return [node, next]; } } else { if (hover.inTopHalf) { return [prev, node]; } else { return [node, next]; } } } type Args = { element: HTMLElement; offset: XYCoord; indent: number; hitAreaHeight?: number; disableReorder?: boolean; node: NodeApi | null; prevNode: NodeApi | null; nextNode: NodeApi | null; }; export type ComputedDrop = { drop: DropResult | null; cursor: Cursor | null; }; function dropAt( parentId: string | undefined, index: number | null ): DropResult { return { parentId: parentId || null, index }; } function lineCursor(index: number, level: number) { return { type: "line" as "line", index, level, }; } function noCursor() { return { type: "none" as "none", }; } function highlightCursor(id: string) { return { type: "highlight" as "highlight", id, }; } function walkUpFrom(node: NodeApi, level: number) { let drop = node; while (drop.parent && drop.level > level) { drop = drop.parent; } const parentId = drop.parent?.id || null; const index = indexOf(drop) + 1; return { parentId, index }; } export type LineCursor = ReturnType<typeof lineCursor>; export type NoCursor = ReturnType<typeof noCursor>; export type HighlightCursor = ReturnType<typeof highlightCursor>; export type Cursor = LineCursor | NoCursor | HighlightCursor; /** * This is the most complex, tricky function in the whole repo. */ export function computeDrop(args: Args): ComputedDrop { const hover = measureHover( args.element, args.offset, args.hitAreaHeight, args.disableReorder ); const indent = args.indent; const hoverLevel = Math.round(Math.max(0, hover.x - indent) / indent); const { node, nextNode, prevNode } = args; const [above, below] = getNodesAroundCursor(node, prevNode, nextNode, hover); /* Hovering over the middle of a folder */ if (node && node.isInternal && hover.inMiddle) { return { drop: dropAt(node.id, null), cursor: highlightCursor(node.id), }; } /* * Now we only need to care about the node above the cursor * ----------- ------- */ /* There is no node above the cursor line */ if (!above) { return { drop: dropAt(below?.parent?.id, 0), cursor: lineCursor(0, 0), }; } /* The node above the cursor line is an item */ if (isItem(above)) { const level = bound(hoverLevel, below?.level || 0, above.level); return { drop: walkUpFrom(above, level), cursor: lineCursor(above.rowIndex! + 1, level), }; } /* The node above the cursor line is a closed folder */ if (isClosed(above)) { const level = bound(hoverLevel, below?.level || 0, above.level); return { drop: walkUpFrom(above, level), cursor: lineCursor(above.rowIndex! + 1, level), }; } /* The node above the cursor line is an open folder with no children */ if (isOpenWithEmptyChildren(above)) { const level = bound(hoverLevel, 0, above.level + 1); if (level > above.level) { /* Will be the first child of the empty folder */ return { drop: dropAt(above.id, 0), cursor: lineCursor(above.rowIndex! + 1, level), }; } else { /* Will be a sibling or grandsibling of the empty folder */ return { drop: walkUpFrom(above, level), cursor: lineCursor(above.rowIndex! + 1, level), }; } } /* The node above the cursor is a an open folder with children */ return { drop: dropAt(above?.id, 0), cursor: lineCursor(above.rowIndex! + 1, above.level + 1), }; }