import type { GridRowDragMode, GridRowId, GridRowMeta, GridTreeIndentSize, } from '../types' import type { Collection } from './types' import { buildParentLookup } from './parent-lookup' import { getFullHeight } from './full-height' import { MARGIN_FOR_SHIFTING } from '../constants' import type { CollisionDetails } from '../functional/grid-drag-controller/collision' import { calculateIndent } from '../utils/indent' /** * This is used to track vertical (top/bottom) and left position * of specific collision areas. These x/y positions are combined * with CollisionDetails. */ type CollisionArea = { left: number top: number bottom: number } & CollisionDetails const OVERLAP = 6 // This number gets doubled in between items (6 above seam, 6 below) const EXTRA_BOUNDS = 50 // This is used for hierarchical mode const isRowParent = (rowMeta?: GridRowMeta): boolean => rowMeta?.type === 'group' || rowMeta?.type === 'tree' type CalculateHitAreasParams = { isTreeGrid: boolean rowIds: GridRowId[] rowHeight: number treeIndentSize: GridTreeIndentSize draggingRowIds: Set previewColumnOffset: number mode: GridRowDragMode canConvertLeaf: boolean levelMap?: Map rowCollection: Collection } export const calculateHitAreas = ({ isTreeGrid, rowIds, rowHeight, draggingRowIds, levelMap, mode, canConvertLeaf, previewColumnOffset, rowCollection, treeIndentSize, }: CalculateHitAreasParams) => { const halfRowHeight = rowHeight / 2 const rowIdSet = new Set(rowIds) const startingLeft = previewColumnOffset + (isTreeGrid ? calculateIndent(0, treeIndentSize, true) + MARGIN_FOR_SHIFTING : 0) const canDropAtIndex = mode === 'default' const canDropOnParent = mode === 'default' || mode === 'parent' const { parentLookup, getAllParents } = buildParentLookup(rowCollection) const getDragDetailsForRow = (rowId: null | GridRowId) => { if (rowId == null) { return { level: 0, parentId: null, canDropOn: false, dragging: false, } } const rowIsLoaded = !!rowCollection.entities.get(rowId) const rowMeta = rowCollection.meta.get(rowId) const isBeingDragged = draggingRowIds.has(rowId) const isParent = isRowParent(rowMeta) const canDropOn = !!rowIsLoaded && !isBeingDragged && (canConvertLeaf || (canDropOnParent && isParent)) return { level: isTreeGrid && levelMap ? (levelMap.get(rowId) ?? 0) : 0, parentId: parentLookup.get(rowId) ?? null, canDropOn, dragging: isBeingDragged, } } const collisions: CollisionArea[] = [] let prevId: null | GridRowId = null let top = 0 function addCollisionsForPair( prevId: null | GridRowId, nextId: null | GridRowId ) { const prevInfo = getDragDetailsForRow(prevId) const nextInfo = getDragDetailsForRow(nextId) if (canDropAtIndex) { const levelLoop: { level: number parentId: null | GridRowId prevId: null | GridRowId }[] = [] if (nextInfo.level >= prevInfo.level) { // When moving on to a sibling, or stepping into a parent, only one seam is needed levelLoop.push({ level: nextInfo.level, parentId: nextInfo.parentId, prevId: nextInfo.level === prevInfo.level ? prevId : null, }) } else { // When moving back up, depending on the difference in levels, there may be multiple // parents it could land in const parents = getAllParents(prevId) let adjustedPrevId: null | GridRowId = prevId let parentId: null | GridRowId = parents.shift() ?? null let levelCounter = prevInfo.level while (levelCounter >= nextInfo.level) { levelLoop.push({ level: levelCounter, parentId: parentId, prevId: adjustedPrevId, }) levelCounter-- adjustedPrevId = parentId parentId = parents.shift() ?? null } } for (const loop of levelLoop) { collisions.push({ left: isTreeGrid && levelLoop.length > 1 && loop !== levelLoop[levelLoop.length - 1] ? startingLeft + calculateIndent(loop.level, treeIndentSize) : 0, top: top - (prevInfo.canDropOn ? OVERLAP : halfRowHeight), bottom: top + (nextInfo.canDropOn ? OVERLAP : halfRowHeight), parentId: loop.parentId, level: loop.level, prevId, prevInsertId: loop.prevId, operation: 'between', }) } } if (nextId != null && nextInfo.canDropOn) { if (canDropAtIndex) { collisions.push({ left: 0, top: top + OVERLAP, bottom: top + rowHeight - OVERLAP, parentId: nextId, level: nextInfo.level, prevId, prevInsertId: null, operation: 'over', }) } else { const height = getFullHeight( nextId, rowIdSet, rowCollection.meta ) collisions.push({ left: previewColumnOffset + calculateIndent(nextInfo.level, treeIndentSize, true), top: top, bottom: top + height * rowHeight, parentId: nextId, level: nextInfo.level, prevId, prevInsertId: null, operation: 'over', }) } } } for (const nextId of rowIds) { addCollisionsForPair(prevId, nextId) prevId = nextId top += rowHeight } addCollisionsForPair(prevId, null) collisions.sort((colA, colB) => { const levelComp = colB.level - colA.level if (levelComp === 0) { return colA.top - colB.top } return levelComp }) let { maxTop, maxBottom } = collisions.reduce<{ maxTop: number maxBottom: number }>( (memo, collision) => { if (collision.top < memo.maxTop) { memo.maxTop = collision.top } if (collision.bottom > memo.maxBottom) { memo.maxBottom = collision.bottom } return memo }, { maxTop: 0, maxBottom: 0 } ) if (!canDropAtIndex) { maxTop = -EXTRA_BOUNDS maxBottom = rowIds.length * rowHeight + EXTRA_BOUNDS collisions.push({ operation: 'over', top: maxTop, bottom: maxBottom, left: 0, level: 0, parentId: null, prevId: null, prevInsertId: null, }) } return { collisions, maxTop, maxBottom, } }