react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
200 lines (168 loc) • 5.73 kB
JavaScript
// @flow
import invariant from 'tiny-invariant';
import { type Position } from 'css-box-model';
import type {
Axis,
DroppableDimensionMap,
DraggableId,
DroppableDimension,
DraggableDimension,
DraggableDimensionMap,
Viewport,
} from '../../../types';
import { toDraggableMap, toDroppableList } from '../../dimension-structures';
import { patch, add, negate } from '../../position';
import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable';
import offsetDraggable from './offset-draggable';
type Args = {|
droppables: DroppableDimensionMap,
existing: DraggableDimensionMap,
additions: DraggableDimension[],
removals: DraggableId[],
viewport: Viewport,
|};
type Shift = {|
indexChange: number,
offset: Position,
|};
type ShiftMap = {
[id: DraggableId]: Shift,
};
export default ({
existing,
droppables,
additions: addedDraggables,
removals: removedDraggables,
viewport,
}: Args): DraggableDimensionMap => {
const shifted: DraggableDimensionMap = {};
toDroppableList(droppables).forEach((droppable: DroppableDimension) => {
const axis: Axis = droppable.axis;
const original: DraggableDimension[] = getDraggablesInsideDroppable(
droppable.descriptor.id,
existing,
);
const toShift: ShiftMap = {};
const addShift = (id: DraggableId, shift: Shift) => {
const previous: ?Shift = toShift[id];
if (!previous) {
toShift[id] = shift;
return;
}
toShift[id] = {
indexChange: previous.indexChange + shift.indexChange,
offset: add(previous.offset, shift.offset),
};
};
// phase 1: removals
const removals: DraggableDimensionMap = toDraggableMap(
removedDraggables
.map((id: DraggableId): DraggableDimension => {
const item: ?DraggableDimension = existing[id];
invariant(item, `Could not find removed draggable "${id}"`);
return item;
})
// only care about the ones inside of this droppable
.filter(
(draggable: DraggableDimension): boolean =>
draggable.descriptor.droppableId === droppable.descriptor.id,
),
);
const withRemovals: DraggableDimension[] = original.filter(
(item: DraggableDimension, index: number): boolean => {
const isBeingRemoved: boolean = Boolean(removals[item.descriptor.id]);
// Item is not being removed - no need to shift anything
if (!isBeingRemoved) {
return true;
}
// moving backwards by displacement
const offset: Position = negate(
patch(axis.line, item.displaceBy[axis.line]),
);
original.slice(index).forEach((sibling: DraggableDimension) => {
// no point shifting this as it is about to be removed
if (removals[sibling.descriptor.id]) {
return;
}
addShift(sibling.descriptor.id, {
// item is being moved backwards one index
indexChange: -1,
offset,
});
});
// We can now remove the draggable as its shift has been recorded
return false;
},
);
// Phase 2: additions
// We do this on the withRemovals array as the new index coming in already account for removals
const additions: DraggableDimension[] = addedDraggables.filter(
(draggable: DraggableDimension): boolean =>
draggable.descriptor.droppableId === droppable.descriptor.id,
);
// Insert additions into the correct positions
// We can do this because the additions are correctly ordered
const withAdditions: DraggableDimension[] = withRemovals.slice(0);
additions.forEach((item: DraggableDimension) => {
withAdditions.splice(item.descriptor.index, 0, item);
});
const additionMap: DraggableDimensionMap = toDraggableMap(additions);
// Calculate the offset to be applied to shifted items
withAdditions.forEach((item: DraggableDimension, index: number) => {
const wasAdded: boolean = Boolean(additionMap[item.descriptor.id]);
// no shifting required when added
if (!wasAdded) {
return;
}
// need to shift everything after the addition
// moving forward by size
const offset: Position = patch(
axis.line,
item.client.marginBox[axis.size],
);
withAdditions.slice(index).forEach((sibling: DraggableDimension) => {
// no shifting required for newly added items
// - they are already captured in the right spot
if (additionMap[sibling.descriptor.id]) {
return;
}
addShift(sibling.descriptor.id, {
// item is being moved forwards one index
indexChange: 1,
offset,
});
});
});
// Phase 3: shift dimensions
withAdditions.forEach((item: DraggableDimension) => {
if (additionMap[item.descriptor.id]) {
return;
}
const shift: Shift = toShift[item.descriptor.id];
if (!shift) {
return;
}
const moved: DraggableDimension = offsetDraggable({
draggable: item,
offset: shift.offset,
initialWindowScroll: viewport.scroll.initial,
});
const index: number = item.descriptor.index + shift.indexChange;
const updated: DraggableDimension = {
...moved,
descriptor: {
...item.descriptor,
index,
},
};
// Add to big cache
shifted[moved.descriptor.id] = updated;
});
});
const map: DraggableDimensionMap = {
...existing,
// will overwrite existing draggables with shifted values if required
...shifted,
};
return map;
};