react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
406 lines (341 loc) • 11 kB
JavaScript
// @flow
import { type Position } from 'css-box-model';
import invariant from 'tiny-invariant';
import createPublisher, { type Publisher, type Provided } from './publisher';
// TODO: state folder reaching into view
import * as timings from '../../debug/timings';
import type {
DraggableId,
DroppableId,
DroppableDescriptor,
DroppableDimension,
DraggableDimension,
DraggableDescriptor,
DraggableDimensionMap,
DroppableDimensionMap,
DimensionMap,
LiftRequest,
Critical,
} from '../../types';
import type {
DimensionMarshal,
Callbacks,
GetDraggableDimensionFn,
DroppableCallbacks,
Entries,
DroppableEntry,
DraggableEntry,
StartPublishingResult,
Collection,
} from './dimension-marshal-types';
export default (callbacks: Callbacks) => {
const entries: Entries = {
droppables: {},
draggables: {},
};
let collection: ?Collection = null;
const publisher: Publisher = createPublisher({
callbacks: {
publish: callbacks.publish,
collectionStarting: callbacks.collectionStarting,
},
getProvided: (): Provided => {
invariant(
collection,
'Cannot get scroll options when there is no collection',
);
return {
entries,
collection,
};
},
});
const registerDraggable = (
descriptor: DraggableDescriptor,
getDimension: GetDraggableDimensionFn,
) => {
// Not checking if the draggable already exists.
// - This allows for overwriting in particular circumstances.
// Not checking if a parent droppable exists.
// - In React 16 children are mounted before their parents
const entry: DraggableEntry = {
descriptor,
getDimension,
};
entries.draggables[descriptor.id] = entry;
if (!collection) {
return;
}
// Not relevant to the drag
if (collection.critical.draggable.type !== descriptor.type) {
return;
}
// A Draggable has been added during a collection - need to act!
publisher.addDraggable(descriptor.id);
};
const updateDraggable = (
previous: DraggableDescriptor,
descriptor: DraggableDescriptor,
getDimension: GetDraggableDimensionFn,
) => {
invariant(
entries.draggables[previous.id],
'Cannot update draggable registration as no previous registration was found',
);
// id might have changed so we are removing the old entry
delete entries.draggables[previous.id];
// adding new entry
const entry: DraggableEntry = {
descriptor,
getDimension,
};
entries.draggables[descriptor.id] = entry;
};
const unregisterDraggable = (descriptor: DraggableDescriptor) => {
const entry: ?DraggableEntry = entries.draggables[descriptor.id];
invariant(
entry,
`Cannot unregister Draggable with id ${
descriptor.id
} as it is not registered`,
);
// Entry has already been overwritten.
// This can happen when a new Draggable with the same draggableId
// is mounted before the old Draggable has been removed.
if (entry.descriptor !== descriptor) {
return;
}
delete entries.draggables[descriptor.id];
if (!collection) {
return;
}
invariant(
collection.critical.draggable.id !== descriptor.id,
'Cannot remove the dragging item during a drag',
);
// Not relevant to the drag
if (descriptor.type !== collection.critical.draggable.type) {
return;
}
publisher.removeDraggable(descriptor.id);
};
const registerDroppable = (
descriptor: DroppableDescriptor,
droppableCallbacks: DroppableCallbacks,
) => {
const id: DroppableId = descriptor.id;
// Not checking if there is already a droppable published with the same id
// In some situations a Droppable might be published with the same id as
// a Droppable that is about to be unmounted - but has not unpublished yet
entries.droppables[id] = {
descriptor,
callbacks: droppableCallbacks,
};
if (!collection) {
return;
}
// Not relevant to this drag
if (descriptor.type !== collection.critical.droppable.type) {
return;
}
publisher.addDroppable(id);
};
const updateDroppable = (
previous: DroppableDescriptor,
descriptor: DroppableDescriptor,
droppableCallbacks: DroppableCallbacks,
) => {
invariant(
entries.droppables[previous.id],
'Cannot update droppable registration as no previous registration was found',
);
// The id might have changed, so we are removing the old entry
delete entries.droppables[previous.id];
const entry: DroppableEntry = {
descriptor,
callbacks: droppableCallbacks,
};
entries.droppables[descriptor.id] = entry;
if (collection) {
invariant(
false,
'You are not able to update the id or type of a droppable during a drag',
);
}
};
const unregisterDroppable = (descriptor: DroppableDescriptor) => {
const entry: ?DroppableEntry = entries.droppables[descriptor.id];
invariant(
entry,
`Cannot unregister Droppable with id ${
descriptor.id
} as as it is not registered`,
);
// entry has already been overwritten
// in which can we will not remove it
if (entry.descriptor !== descriptor) {
return;
}
// Not checking if this will leave orphan draggables as react
// unmounts parents before it unmounts children:
// https://twitter.com/alexandereardon/status/941514612624703488
delete entries.droppables[descriptor.id];
if (!collection) {
return;
}
invariant(
collection.critical.droppable.id !== descriptor.id,
'Cannot remove the home Droppable during a drag',
);
// Not relevant to the drag
if (collection.critical.droppable.type !== descriptor.type) {
return;
}
publisher.removeDroppable(descriptor.id);
};
const updateDroppableIsEnabled = (id: DroppableId, isEnabled: boolean) => {
invariant(
entries.droppables[id],
`Cannot update the scroll on Droppable ${id} as it is not registered`,
);
// no need to update the application state if a collection is not occurring
if (!collection) {
return;
}
// At this point a non primary droppable dimension might not yet be published
// but may have its enabled state changed. For now we still publish this change
// and let the reducer exit early if it cannot find the dimension in the state.
callbacks.updateDroppableIsEnabled({ id, isEnabled });
};
const updateDroppableScroll = (id: DroppableId, newScroll: Position) => {
invariant(
entries.droppables[id],
`Cannot update the scroll on Droppable ${id} as it is not registered`,
);
// no need to update the application state if a collection is not occurring
if (!collection) {
return;
}
callbacks.updateDroppableScroll({ id, offset: newScroll });
};
const scrollDroppable = (id: DroppableId, change: Position) => {
const entry: ?DroppableEntry = entries.droppables[id];
invariant(entry, `Cannot scroll Droppable ${id} as it is not registered`);
if (!collection) {
return;
}
entry.callbacks.scroll(change);
};
const getInitialPublish = (args: Collection): StartPublishingResult => {
const { critical, scrollOptions, initialWindowScroll: windowScroll } = args;
const timingKey: string = 'Initial collection from DOM';
timings.start(timingKey);
const home: DroppableDescriptor = critical.droppable;
const droppables: DroppableDimensionMap = Object.keys(entries.droppables)
.map((id: DroppableId): DroppableEntry => entries.droppables[id])
// Exclude things of the wrong type
.filter(
(entry: DroppableEntry): boolean => entry.descriptor.type === home.type,
)
.map(
(entry: DroppableEntry): DroppableDimension =>
entry.callbacks.getDimensionAndWatchScroll(
windowScroll,
scrollOptions,
),
)
.reduce(
(previous: DroppableDimensionMap, dimension: DroppableDimension) => {
previous[dimension.descriptor.id] = dimension;
return previous;
},
{},
);
const draggables: DraggableDimensionMap = Object.keys(entries.draggables)
.map((id: DraggableId): DraggableEntry => entries.draggables[id])
.filter(
(entry: DraggableEntry): boolean =>
entry.descriptor.type === critical.draggable.type,
)
.map(
(entry: DraggableEntry): DraggableDimension =>
entry.getDimension(windowScroll),
)
.reduce(
(previous: DraggableDimensionMap, dimension: DraggableDimension) => {
previous[dimension.descriptor.id] = dimension;
return previous;
},
{},
);
timings.finish(timingKey);
const dimensions: DimensionMap = { draggables, droppables };
const result: StartPublishingResult = {
dimensions,
critical,
};
return result;
};
const stopPublishing = () => {
// This function can be called defensively
if (!collection) {
return;
}
// Stop any pending dom collections or publish
publisher.stop();
// Tell all droppables to stop watching scroll
// all good if they where not already listening
const home: DroppableDescriptor = collection.critical.droppable;
Object.keys(entries.droppables)
.filter(
(id: DroppableId): boolean =>
entries.droppables[id].descriptor.type === home.type,
)
.forEach((id: DroppableId) =>
entries.droppables[id].callbacks.unwatchScroll(),
);
// Finally - clear our collection
collection = null;
};
const startPublishing = (
request: LiftRequest,
windowScroll: Position,
): StartPublishingResult => {
invariant(
!collection,
'Cannot start capturing critical dimensions as there is already a collection',
);
const entry: ?DraggableEntry = entries.draggables[request.draggableId];
invariant(entry, 'Cannot find critical draggable entry');
const home: ?DroppableEntry =
entries.droppables[entry.descriptor.droppableId];
invariant(home, 'Cannot find critical droppable entry');
const critical: Critical = {
draggable: entry.descriptor,
droppable: home.descriptor,
};
collection = {
scrollOptions: request.scrollOptions,
critical,
initialWindowScroll: windowScroll,
};
return getInitialPublish(collection);
};
const marshal: DimensionMarshal = {
// dimension registration
registerDraggable,
updateDraggable,
unregisterDraggable,
registerDroppable,
updateDroppable,
unregisterDroppable,
// droppable changes
updateDroppableIsEnabled,
scrollDroppable,
updateDroppableScroll,
// Entry
startPublishing,
stopPublishing,
};
return marshal;
};