@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
175 lines (151 loc) • 4.47 kB
text/typescript
import type { Position } from 'css-box-model';
import { invariant } from '../../invariant';
import type {
Axis,
DroppableDimension,
DraggableDimension,
DraggableDimensionMap,
Scrollable,
DroppableSubject,
PlaceholderInSubject,
} from '../../types';
import getDraggablesInsideDroppable from '../get-draggables-inside-droppable';
import { add, patch } from '../position';
import getSubject from './util/get-subject';
import isHomeOf from './is-home-of';
import getDisplacedBy from '../get-displaced-by';
const getRequiredGrowthForPlaceholder = (
droppable: DroppableDimension,
placeholderSize: Position,
draggables: DraggableDimensionMap,
): Position | null => {
const axis: Axis = droppable.axis;
// A virtual list will most likely not contain all of the Draggables
// so counting them does not help.
if (droppable.descriptor.mode === 'virtual') {
return patch(axis.line, placeholderSize[axis.line]);
}
// TODO: consider margin collapsing?
// Using contentBox as that is where the Draggables will sit
const availableSpace: number = droppable.subject.page.contentBox[axis.size];
const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable(
droppable.descriptor.id,
draggables,
);
const spaceUsed: number = insideDroppable.reduce(
(sum: number, dimension: DraggableDimension): number =>
sum + dimension.client.marginBox[axis.size],
0,
);
const requiredSpace: number = spaceUsed + placeholderSize[axis.line];
const needsToGrowBy: number = requiredSpace - availableSpace;
// nothing to do here
if (needsToGrowBy <= 0) {
return null;
}
return patch(axis.line, needsToGrowBy);
};
const withMaxScroll = (frame: Scrollable, max: Position): Scrollable => ({
...frame,
scroll: {
...frame.scroll,
max,
},
});
export const addPlaceholder = (
droppable: DroppableDimension,
draggable: DraggableDimension,
draggables: DraggableDimensionMap,
): DroppableDimension => {
const frame: Scrollable | null = droppable.frame;
invariant(
!isHomeOf(draggable, droppable),
'Should not add placeholder space to home list',
);
invariant(
!droppable.subject.withPlaceholder,
'Cannot add placeholder size to a subject when it already has one',
);
const placeholderSize: Position = getDisplacedBy(
droppable.axis,
draggable.displaceBy,
).point;
const requiredGrowth: Position | null = getRequiredGrowthForPlaceholder(
droppable,
placeholderSize,
draggables,
);
const added: PlaceholderInSubject = {
placeholderSize,
increasedBy: requiredGrowth,
oldFrameMaxScroll: droppable.frame ? droppable.frame.scroll.max : null,
};
if (!frame) {
const subject: DroppableSubject = getSubject({
page: droppable.subject.page,
withPlaceholder: added,
axis: droppable.axis,
frame: droppable.frame,
});
return {
...droppable,
subject,
};
}
const maxScroll: Position = requiredGrowth
? add(frame.scroll.max, requiredGrowth)
: frame.scroll.max;
const newFrame: Scrollable = withMaxScroll(frame, maxScroll);
const subject: DroppableSubject = getSubject({
page: droppable.subject.page,
withPlaceholder: added,
axis: droppable.axis,
frame: newFrame,
});
return {
...droppable,
subject,
frame: newFrame,
};
};
export const removePlaceholder = (
droppable: DroppableDimension,
): DroppableDimension => {
const added: PlaceholderInSubject | null = droppable.subject.withPlaceholder;
invariant(
added,
'Cannot remove placeholder form subject when there was none',
);
const frame: Scrollable | null = droppable.frame;
if (!frame) {
const subject: DroppableSubject = getSubject({
page: droppable.subject.page,
axis: droppable.axis,
frame: null,
// cleared
withPlaceholder: null,
});
return {
...droppable,
subject,
};
}
const oldMaxScroll: Position | null = added.oldFrameMaxScroll;
invariant(
oldMaxScroll,
'Expected droppable with frame to have old max frame scroll when removing placeholder',
);
const newFrame: Scrollable = withMaxScroll(frame, oldMaxScroll);
const subject: DroppableSubject = getSubject({
page: droppable.subject.page,
axis: droppable.axis,
frame: newFrame,
// cleared
withPlaceholder: null,
});
return {
...droppable,
subject,
frame: newFrame,
};
};