@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
131 lines (111 loc) • 3.25 kB
text/typescript
import { expand, getRect } from 'css-box-model';
import type { Rect, Spacing } from 'css-box-model';
import type {
DraggableId,
Displacement,
DraggableDimension,
DroppableDimension,
DisplacementGroups,
DisplacedBy,
} from '../types';
import { isPartiallyVisible } from './visibility/is-visible';
interface Args {
afterDragging: DraggableDimension[];
destination: DroppableDimension;
displacedBy: DisplacedBy;
last: DisplacementGroups | null;
viewport: Rect;
forceShouldAnimate?: boolean;
}
const getShouldAnimate = (
id: DraggableId,
last?: DisplacementGroups | null,
forceShouldAnimate?: boolean | null,
) => {
// Use a forced value if provided
if (typeof forceShouldAnimate === 'boolean') {
return forceShouldAnimate;
}
// nothing to gauge animation from
if (!last) {
return true;
}
const { invisible, visible } = last;
// it was previously invisible - no animation
if (invisible[id]) {
return false;
}
const previous: Displacement | null = visible[id];
return previous ? previous.shouldAnimate : true;
};
// Note: it is also an optimisation to not render the displacement on
// items when they are not longer visible.
// This prevents a lot of .render() calls when leaving / entering a list
function getTarget(
draggable: DraggableDimension,
displacedBy: DisplacedBy,
): Rect {
const marginBox: Rect = draggable.page.marginBox;
// ## Visibility overscanning
// We are expanding rather than offsetting the marginBox.
// In some cases we want
// - the target based on the starting position (such as when dropping outside of any list)
// - the target based on the items position without starting displacement (such as when moving inside a list)
// To keep things simple we just expand the whole area for this check
// The worst case is some minor redundant offscreen movements
const expandBy: Spacing = {
// pull backwards into viewport
top: displacedBy.point.y,
right: 0,
bottom: 0,
// pull backwards into viewport
left: displacedBy.point.x,
};
return getRect(expand(marginBox, expandBy));
}
export default function getDisplacementGroups({
afterDragging,
destination,
displacedBy,
viewport,
forceShouldAnimate,
last,
}: Args): DisplacementGroups {
return afterDragging.reduce(
function process(
groups: DisplacementGroups,
draggable: DraggableDimension,
): DisplacementGroups {
const target: Rect = getTarget(draggable, displacedBy);
const id: DraggableId = draggable.descriptor.id;
groups.all.push(id);
const isVisible: boolean = isPartiallyVisible({
target,
destination,
viewport,
withDroppableDisplacement: true,
});
if (!isVisible) {
groups.invisible[draggable.descriptor.id] = true;
return groups;
}
// item is visible
const shouldAnimate: boolean = getShouldAnimate(
id,
last,
forceShouldAnimate,
);
const displacement: Displacement = {
draggableId: id,
shouldAnimate,
};
groups.visible[id] = displacement;
return groups;
},
{
all: [],
visible: {},
invisible: {},
},
);
}