import type { Position, Rect } from 'css-box-model'; import { invariant } from '../../../invariant'; import { closest } from '../../position'; import isWithin from '../../is-within'; import { getCorners } from '../../spacing'; import isPartiallyVisibleThroughFrame from '../../visibility/is-partially-visible-through-frame'; import { toDroppableList } from '../../dimension-structures'; import type { Axis, DroppableDimension, DroppableDimensionMap, Viewport, } from '../../../types'; interface GetBestDroppableArgs { isMovingForward: boolean; // the current position of the dragging item pageBorderBoxCenter: Position; // the home of the draggable source: DroppableDimension; // all the droppables in the system droppables: DroppableDimensionMap; viewport: Viewport; } const getKnownActive = (droppable: DroppableDimension): Rect => { const rect: Rect | null = droppable.subject.active; invariant(rect, 'Cannot get clipped area from droppable'); return rect; }; export default ({ isMovingForward, pageBorderBoxCenter, source, droppables, viewport, }: GetBestDroppableArgs): DroppableDimension | null => { const active: Rect | null = source.subject.active; if (!active) { return null; } const axis: Axis = source.axis; const isBetweenSourceClipped = isWithin(active[axis.start], active[axis.end]); const candidates: DroppableDimension[] = toDroppableList(droppables) // Remove the source droppable from the list .filter((droppable: DroppableDimension): boolean => droppable !== source) // Remove any options that are not enabled .filter((droppable: DroppableDimension): boolean => droppable.isEnabled) // Remove any droppables that do not have a visible subject .filter((droppable: DroppableDimension): boolean => Boolean(droppable.subject.active), ) // Remove any that are not visible in the window .filter((droppable: DroppableDimension): boolean => isPartiallyVisibleThroughFrame(viewport.frame)(getKnownActive(droppable)), ) .filter((droppable: DroppableDimension): boolean => { const activeOfTarget: Rect = getKnownActive(droppable); // is the target in front of the source on the cross axis? if (isMovingForward) { return active[axis.crossAxisEnd] < activeOfTarget[axis.crossAxisEnd]; } // is the target behind the source on the cross axis? return activeOfTarget[axis.crossAxisStart] < active[axis.crossAxisStart]; }) // Must have some overlap on the main axis .filter((droppable: DroppableDimension): boolean => { const activeOfTarget: Rect = getKnownActive(droppable); const isBetweenDestinationClipped = isWithin( activeOfTarget[axis.start], activeOfTarget[axis.end], ); return ( isBetweenSourceClipped(activeOfTarget[axis.start]) || isBetweenSourceClipped(activeOfTarget[axis.end]) || isBetweenDestinationClipped(active[axis.start]) || isBetweenDestinationClipped(active[axis.end]) ); }) // Sort on the cross axis .sort((a: DroppableDimension, b: DroppableDimension) => { const first: number = getKnownActive(a)[axis.crossAxisStart]; const second: number = getKnownActive(b)[axis.crossAxisStart]; if (isMovingForward) { return first - second; } return second - first; }) // Find the droppables that have the same cross axis value as the first item .filter( ( droppable: DroppableDimension, index: number, array: DroppableDimension[], ): boolean => getKnownActive(droppable)[axis.crossAxisStart] === getKnownActive(array[0])[axis.crossAxisStart], ); // no possible candidates if (!candidates.length) { return null; } // only one result - all done! if (candidates.length === 1) { return candidates[0]; } // At this point we have a number of candidates that // all have the same axis.crossAxisStart value. // Check to see if the center position is within the size of a Droppable on the main axis const contains: DroppableDimension[] = candidates.filter( (droppable: DroppableDimension) => { const isWithinDroppable = isWithin( getKnownActive(droppable)[axis.start], getKnownActive(droppable)[axis.end], ); return isWithinDroppable(pageBorderBoxCenter[axis.line]); }, ); if (contains.length === 1) { return contains[0]; } // The center point of the draggable falls on the boundary between two droppables if (contains.length > 1) { // sort on the main axis and choose the first return contains.sort( (a: DroppableDimension, b: DroppableDimension): number => getKnownActive(a)[axis.start] - getKnownActive(b)[axis.start], )[0]; } // The center is not contained within any droppable // 1. Find the candidate that has the closest corner // 2. If there is a tie - choose the one that is first on the main axis return candidates.sort( (a: DroppableDimension, b: DroppableDimension): number => { const first = closest(pageBorderBoxCenter, getCorners(getKnownActive(a))); const second = closest( pageBorderBoxCenter, getCorners(getKnownActive(b)), ); // if the distances are not equal - choose the shortest if (first !== second) { return first - second; } // They both have the same distance - // choose the one that is first on the main axis return getKnownActive(a)[axis.start] - getKnownActive(b)[axis.start]; }, )[0]; };