@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
158 lines (134 loc) • 4.61 kB
text/typescript
import type { Position, Rect } from 'css-box-model';
import type {
DroppableDimension,
DroppableDimensionMap,
DroppableId,
DraggableDimension,
Axis,
} from '../types';
import { toDroppableList } from './dimension-structures';
import isPositionInFrame from './visibility/is-position-in-frame';
import { distance, patch } from './position';
import isWithin from './is-within';
// https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
// https://silentmatt.com/rectangle-intersection/
function getHasOverlap(first: Rect, second: Rect): boolean {
return (
first.left < second.right &&
first.right > second.left &&
first.top < second.bottom &&
first.bottom > second.top
);
}
interface Args {
pageBorderBox: Rect;
draggable: DraggableDimension;
droppables: DroppableDimensionMap;
}
interface WithDistance {
distance: number;
id: DroppableId;
}
interface GetFurthestArgs {
pageBorderBox: Rect;
draggable: DraggableDimension;
candidates: DroppableDimension[];
}
function getFurthestAway({
pageBorderBox,
draggable,
candidates,
}: GetFurthestArgs): DroppableId | null {
// We are not comparing the center of the home list with the target list as it would
// give preference to giant lists
// We are measuring the distance from where the draggable started
// to where it is *hitting* the candidate
// Note: The hit point might technically not be in the bounds of the candidate
const startCenter: Position = draggable.page.borderBox.center;
const sorted: WithDistance[] = candidates
.map((candidate: DroppableDimension): WithDistance => {
const axis: Axis = candidate.axis;
const target: Position = patch(
candidate.axis.line,
// use the current center of the dragging item on the main axis
pageBorderBox.center[axis.line],
// use the center of the list on the cross axis
candidate.page.borderBox.center[axis.crossAxisLine],
);
return {
id: candidate.descriptor.id,
distance: distance(startCenter, target),
};
})
// largest value will be first
.sort((a: WithDistance, b: WithDistance) => b.distance - a.distance);
// just being safe
return sorted[0] ? sorted[0].id : null;
}
export default function getDroppableOver({
pageBorderBox,
draggable,
droppables,
}: Args): DroppableId | null {
// We know at this point that some overlap has to exist
const candidates: DroppableDimension[] = toDroppableList(droppables).filter(
(item: DroppableDimension): boolean => {
// Cannot be a candidate when disabled
if (!item.isEnabled) {
return false;
}
// Cannot be a candidate when there is no visible area
const active: Rect | null = item.subject.active;
if (!active) {
return false;
}
// Cannot be a candidate when dragging item is not over the droppable at all
if (!getHasOverlap(pageBorderBox, active)) {
return false;
}
// 1. Candidate if the center position is over a droppable
if (isPositionInFrame(active)(pageBorderBox.center)) {
return true;
}
// 2. Candidate if an edge is over the cross axis half way point
// 3. Candidate if dragging item is totally over droppable on cross axis
const axis: Axis = item.axis;
const childCenter: number = active.center[axis.crossAxisLine];
const crossAxisStart: number = pageBorderBox[axis.crossAxisStart];
const crossAxisEnd: number = pageBorderBox[axis.crossAxisEnd];
const isContained = isWithin(
active[axis.crossAxisStart],
active[axis.crossAxisEnd],
);
const isStartContained: boolean = isContained(crossAxisStart);
const isEndContained: boolean = isContained(crossAxisEnd);
// Dragging item is totally covering the active area
if (!isStartContained && !isEndContained) {
return true;
}
/**
* edges must go beyond the center line in order to avoid
* cases were both conditions are satisfied.
*/
if (isStartContained) {
return crossAxisStart < childCenter;
}
return crossAxisEnd > childCenter;
},
);
if (!candidates.length) {
return null;
}
// Only one candidate - use that!
if (candidates.length === 1) {
return candidates[0].descriptor.id;
}
// Multiple options returned
// Should only occur with really large items
// Going to use fallback: distance from home
return getFurthestAway({
pageBorderBox,
draggable,
candidates,
});
}