import type { CollisionDetection } from '@dnd-kit/core' import type { GridRowId } from '../../types' import { DROPPABLE_TYPE_ROW_GROUP } from '../../constants' import type { Store } from '../../state' /** * This function tries to find a visible, previous sibling for the target that * excludes any items currently being dragged. */ function findValidInsertId( prevInsertId: GridRowId | null, draggedRowIds: Set, level: number, store: Store ) { if (prevInsertId != null && draggedRowIds.has(prevInsertId)) { const state = store.getState() const rowIds = store.selectors.selectRowIds(state) let index = rowIds.indexOf(prevInsertId) - 1 // Look for a previous sibling that isn't being dragged. // Only evaluate rows with the same level. If a parent // is encountered, it should return null while (index >= 0) { const rowId = rowIds[index] const rowLevel = Math.max( 0, store.selectors.selectRowLevel(state, rowId) ) if (rowLevel < level) { // We found a parent, but no better matches return null } else if (level === rowLevel && !draggedRowIds.has(rowId)) { // We found the correct row return rowId } index-- } return null } return prevInsertId } export type CollisionDetails = { /** * The new parent of the dropped rows. Null means root level. */ parentId: null | GridRowId /** * The previous row in the rendered grid, ignoring hierarchy. * Populated only when operation is 'between' */ prevId: null | GridRowId /** * The new, previous sibling of the dropped rows. Null means the dropped * item will be first when operation is "between" */ prevInsertId: null | GridRowId /** * The new level of the dropped rows */ level: number /** * Will this operation put the dropped rows between other rows or over * a parent or the root grid. */ operation: 'between' | 'over' } export const collisionFactory = (store: Store) => { const gridRowIntersection: CollisionDetection = ({ collisionRect, droppableRects, }) => { const rect = droppableRects.get(DROPPABLE_TYPE_ROW_GROUP) if (!rect) { return [] } const state = store.getState() const draggedRowIds = store.selectors.selectDraggingRowIds(state) const middleOfDraggingOverlay = collisionRect.top + collisionRect.height / 2 const { maxTop, maxBottom, collisions: collisionAreas, } = store.selectors.selectCollisionAreas(state) const limitedOverlayY = Math.min( rect.top + maxBottom, Math.max(rect.top + maxTop, middleOfDraggingOverlay) ) const area = collisionAreas.find( (col) => rect.top + col.top <= limitedOverlayY && rect.top + col.bottom >= limitedOverlayY && (col.left === 0 || rect.left + col.left < collisionRect.left) ) if (!area) { return [] } const { parentId, level, prevId, prevInsertId, operation } = area const prevInsertIdAdjusted = findValidInsertId( prevInsertId, draggedRowIds, level, store ) return [ { id: DROPPABLE_TYPE_ROW_GROUP, data: { parentId, prevId, prevInsertId: prevInsertIdAdjusted, level, operation, } satisfies CollisionDetails, }, ] } return gridRowIntersection }