react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
229 lines (191 loc) • 6.47 kB
JavaScript
// @flow
import invariant from 'tiny-invariant';
import {
offset,
withScroll,
type BoxModel,
type Position,
} from 'css-box-model';
import type {
Axis,
DimensionMap,
Publish,
DraggableId,
DroppableId,
DraggableDimension,
DroppableDimension,
DraggableDimensionMap,
DroppableDimensionMap,
} from '../../types';
import { toDroppableMap, toDraggableMap } from '../dimension-structures';
import { patch } from '../position';
import * as timings from '../../debug/timings';
type Args = {|
existing: DimensionMap,
publish: Publish,
windowScroll: Position,
|};
type Partitioned = {|
inNewDroppable: DraggableDimension[],
inExistingDroppable: DraggableDimension[],
|};
type Record = {|
index: number,
// the size of the dimension on the main axis
size: number,
|};
// type RecordMap = {
// [id: DraggableId]: Record
// }
type Entry = {|
additions: Record[],
removals: Record[],
|};
type ChangeSet = {
[id: DroppableId]: Entry,
};
const getTotal = (records: Record[]): number =>
records.reduce((total: number, record: Record) => total + record.size, 0);
const withEntry = (set: ChangeSet, droppableId: DroppableId): Entry => {
if (!set[droppableId]) {
set[droppableId] = {
additions: [],
removals: [],
};
}
return set[droppableId];
};
const getRecord = (draggable: DraggableDimension, home: DroppableDimension) => {
const axis: Axis = home.axis;
const size: number = draggable.client.marginBox[axis.size];
const record: Record = {
index: draggable.descriptor.index,
size,
};
return record;
};
const timingKey: string = 'Dynamic dimension change processing (just math)';
export default ({ existing, publish, windowScroll }: Args): DimensionMap => {
timings.start(timingKey);
const addedDroppables: DroppableDimensionMap = toDroppableMap(
publish.additions.droppables,
);
const addedDraggables: DraggableDimensionMap = toDraggableMap(
publish.additions.draggables,
);
const partitioned: Partitioned = publish.additions.draggables.reduce(
(previous: Partitioned, draggable: DraggableDimension) => {
const droppableId: DroppableId = draggable.descriptor.droppableId;
const isInNewDroppable: boolean = Boolean(addedDroppables[droppableId]);
if (isInNewDroppable) {
previous.inNewDroppable.push(draggable);
} else {
previous.inExistingDroppable.push(draggable);
}
return previous;
},
{
inNewDroppable: [],
inExistingDroppable: [],
},
);
// TODO: can just exit early here
if (!partitioned.inExistingDroppable.length) {
// console.log('no updates to existing droppables, can just move on');
}
const set: ChangeSet = {};
// Draggable additions
partitioned.inExistingDroppable.forEach((draggable: DraggableDimension) => {
const droppableId: DroppableId = draggable.descriptor.droppableId;
const home: ?DroppableDimension = existing.droppables[droppableId];
invariant(
home,
`Cannot find home Droppable for added Draggable ${
draggable.descriptor.id
}`,
);
withEntry(set, droppableId).additions.push(getRecord(draggable, home));
});
// Draggable removals
publish.removals.draggables.forEach((id: DraggableId) => {
// Pull draggable dimension from existing dimensions
const draggable: ?DraggableDimension = existing.draggables[id];
invariant(draggable, `Cannot find Draggable ${id}`);
const droppableId: DroppableId = draggable.descriptor.droppableId;
const home: ?DroppableDimension = existing.droppables[droppableId];
invariant(home, `Cannot find home Droppable for added Draggable ${id}`);
withEntry(set, droppableId).removals.push(getRecord(draggable, home));
});
// ## Adjust draggables based on changes
const shifted: DraggableDimension[] = Object.keys(existing.draggables).map(
(id: DraggableId): DraggableDimension => {
const draggable: DraggableDimension = existing.draggables[id];
const droppableId: DroppableId = draggable.descriptor.droppableId;
const entry: ?Entry = set[droppableId];
// No additions or removals to the Droppable
// Can just return the draggable
if (!entry) {
return draggable;
}
const startIndex: number = draggable.descriptor.index;
// Were there any additions before the draggable?
const additions: Record[] = entry.additions.filter(
(record: Record) => record.index <= startIndex,
);
// Were there any removals before the droppable?
const removals: Record[] = entry.removals.filter(
(record: Record) => record.index <= startIndex,
);
// No changes before the draggable - no shifting required
if (!additions.length && !removals.length) {
return draggable;
}
const droppable: DroppableDimension = existing.droppables[droppableId];
const additionSize: number = getTotal(additions);
const removalSize: number = getTotal(removals);
const deltaShift: number = additionSize - removalSize;
// console.log('DELTA SHIFT', deltaShift);
const change: Position = patch(droppable.axis.line, deltaShift);
const client: BoxModel = offset(draggable.client, change);
// TODO: should this be different?
const page: BoxModel = withScroll(client, windowScroll);
const indexChange: number = additions.length - removals.length;
// console.log('INDEX SHIFT', indexChange);
const index: number = startIndex + indexChange;
const moved: DraggableDimension = {
...draggable,
descriptor: {
...draggable.descriptor,
index,
},
placeholder: {
...draggable.placeholder,
client,
},
client,
page,
};
return moved;
},
);
// Let's add our shifted draggables to our dimension map
const dimensions: DimensionMap = {
draggables: {
...toDraggableMap(shifted),
...addedDraggables,
},
droppables: {
...existing.droppables,
...addedDroppables,
},
};
// We also need to remove the Draggables and Droppables from this new map
publish.removals.draggables.forEach((id: DraggableId) => {
delete dimensions.draggables[id];
});
publish.removals.droppables.forEach((id: DroppableId) => {
delete dimensions.droppables[id];
});
timings.finish(timingKey);
return dimensions;
};