UNPKG

@headless-tree/core

Version:

The definitive tree component for the Web

269 lines (235 loc) 7.3 kB
import { ItemInstance, TreeInstance } from "../../types/core"; import { DragTarget } from "./types"; export enum ItemDropCategory { Item, ExpandedFolder, LastInGroup, } export enum PlacementType { ReorderAbove, ReorderBelow, MakeChild, Reparent, } export type TargetPlacement = | { type: | PlacementType.ReorderAbove | PlacementType.ReorderBelow | PlacementType.MakeChild; } | { type: PlacementType.Reparent; reparentLevel: number; }; export const isOrderedDragTarget = <T>(dragTarget: DragTarget<T>) => "childIndex" in dragTarget; export const canDrop = ( dataTransfer: DataTransfer | null, target: DragTarget<any>, tree: TreeInstance<any>, ) => { const draggedItems = tree.getState().dnd?.draggedItems; const config = tree.getConfig(); if (draggedItems && !(config.canDrop?.(draggedItems, target) ?? true)) { return false; } if ( draggedItems && draggedItems.some( (draggedItem) => target.item.getId() === draggedItem.getId() || target.item.isDescendentOf(draggedItem.getId()), ) ) { return false; } if ( !draggedItems && dataTransfer && config.canDropForeignDragObject && !config.canDropForeignDragObject(dataTransfer, target) ) { return false; } return true; }; export const getItemDropCategory = (item: ItemInstance<any>) => { if (item.isExpanded()) { return ItemDropCategory.ExpandedFolder; } const parent = item.getParent(); if (parent && item.getIndexInParent() === item.getItemMeta().setSize - 1) { return ItemDropCategory.LastInGroup; } return ItemDropCategory.Item; }; export const getInsertionIndex = <T>( children: ItemInstance<T>[], childIndex: number, draggedItems: ItemInstance<T>[] | undefined, ) => { const numberOfDragItemsBeforeTarget = children .slice(0, childIndex) .reduce( (counter, child) => child && draggedItems?.some((i) => i.getId() === child.getId()) ? ++counter : counter, 0, ) ?? 0; return childIndex - numberOfDragItemsBeforeTarget; }; export const getTargetPlacement = ( e: any, item: ItemInstance<any>, tree: TreeInstance<any>, canMakeChild: boolean, ): TargetPlacement => { const config = tree.getConfig(); if (!config.canReorder) { return canMakeChild ? { type: PlacementType.MakeChild } : { type: PlacementType.ReorderBelow }; } const bb = item.getElement()?.getBoundingClientRect(); const topPercent = bb ? (e.clientY - bb.top) / bb.height : 0.5; const leftPixels = bb ? e.clientX - bb.left : 0; const targetDropCategory = getItemDropCategory(item); const reorderAreaPercentage = !canMakeChild ? 0.5 : config.reorderAreaPercentage ?? 0.3; const indent = config.indent ?? 20; const makeChildType = canMakeChild ? PlacementType.MakeChild : PlacementType.ReorderBelow; if (targetDropCategory === ItemDropCategory.ExpandedFolder) { if (topPercent < reorderAreaPercentage) { return { type: PlacementType.ReorderAbove }; } return { type: makeChildType }; } if (targetDropCategory === ItemDropCategory.LastInGroup) { if (leftPixels < item.getItemMeta().level * indent) { if (topPercent < 0.5) { return { type: PlacementType.ReorderAbove }; } const minLevel = item.getItemBelow()?.getItemMeta().level ?? 0; return { type: PlacementType.Reparent, reparentLevel: Math.max(minLevel, Math.floor(leftPixels / indent)), }; } // if not at left of item area, treat as if it was a normal item } // targetDropCategory === ItemDropCategory.Item if (topPercent < reorderAreaPercentage) { return { type: PlacementType.ReorderAbove }; } if (topPercent > 1 - reorderAreaPercentage) { return { type: PlacementType.ReorderBelow }; } return { type: makeChildType }; }; export const getDragCode = ( item: ItemInstance<any>, placement: TargetPlacement, ) => { return [ item.getId(), placement.type, placement.type === PlacementType.Reparent ? placement.reparentLevel : 0, ].join("__"); }; const getNthParent = ( item: ItemInstance<any>, n: number, ): ItemInstance<any> => { if (n === item.getItemMeta().level) { return item; } return getNthParent(item.getParent()!, n); }; /** @param item refers to the bottom-most item of the container, at which bottom is being reparented on (e.g. root-1-2-6) */ export const getReparentTarget = <T>( item: ItemInstance<T>, reparentLevel: number, draggedItems: ItemInstance<T>[] | undefined, ) => { const itemMeta = item.getItemMeta(); const reparentedTarget = getNthParent(item, reparentLevel - 1); const targetItemAbove = getNthParent(item, reparentLevel); // .getItemBelow()!; const targetIndex = targetItemAbove.getIndexInParent() + 1; return { item: reparentedTarget, childIndex: targetIndex, insertionIndex: getInsertionIndex( reparentedTarget.getChildren(), targetIndex, draggedItems, ), dragLineIndex: itemMeta.index + 1, dragLineLevel: reparentLevel, }; }; export const getDragTarget = ( e: any, item: ItemInstance<any>, tree: TreeInstance<any>, canReorder = tree.getConfig().canReorder, ): DragTarget<any> => { const draggedItems = tree.getState().dnd?.draggedItems; const itemMeta = item.getItemMeta(); const parent = item.getParent(); const itemTarget: DragTarget<any> = { item }; const parentTarget: DragTarget<any> | null = parent ? { item: parent } : null; const canBecomeSibling = parentTarget && canDrop(e.dataTransfer, parentTarget, tree); const canMakeChild = canDrop(e.dataTransfer, itemTarget, tree); const placement = getTargetPlacement(e, item, tree, canMakeChild); if ( !canReorder && parent && canBecomeSibling && placement.type !== PlacementType.MakeChild ) { if (draggedItems?.some((item) => item.isDescendentOf(parent.getId()))) { // dropping on itself should be illegal, return item, canDrop will then return false return itemTarget; } return parentTarget; } if (!canReorder && parent && !canBecomeSibling) { // TODO! this breaks in story DND/Can Drop. Maybe move this logic into a composable DragTargetStrategy[] ? return getDragTarget(e, parent, tree, false); } if (!parent) { // Shouldn't happen, but if dropped "next" to root item, just drop it inside return itemTarget; } if (placement.type === PlacementType.MakeChild) { return itemTarget; } if (!canBecomeSibling) { return getDragTarget(e, parent, tree, false); } if (placement.type === PlacementType.Reparent) { return getReparentTarget(item, placement.reparentLevel, draggedItems); } const maybeAddOneForBelow = placement.type === PlacementType.ReorderAbove ? 0 : 1; const childIndex = item.getIndexInParent() + maybeAddOneForBelow; return { item: parent, dragLineIndex: itemMeta.index + maybeAddOneForBelow, dragLineLevel: itemMeta.level, childIndex, // TODO performance could be improved by computing this only when dragcode changed insertionIndex: getInsertionIndex( parent.getChildren(), childIndex, draggedItems, ), }; };