react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
339 lines (278 loc) • 9.66 kB
JavaScript
// @flow
import { type Position } from 'css-box-model';
import invariant from 'tiny-invariant';
import type {
DimensionMarshal,
Callbacks,
GetDraggableDimensionFn,
DroppableCallbacks,
Entries,
DroppableEntry,
DraggableEntry,
StartPublishingResult,
} from './dimension-marshal-types';
import createPublisher, {
type WhileDraggingPublisher,
} from './while-dragging-publisher';
import getInitialPublish from './get-initial-publish';
import type {
DroppableId,
DroppableDescriptor,
DraggableDescriptor,
LiftRequest,
Critical,
} from '../../types';
import { values } from '../../native-with-fallback';
import { warning } from '../../dev-warning';
type Collection = {|
critical: Critical,
|};
const throwIfAddOrRemoveOfWrongType = (
collection: Collection,
descriptor: DraggableDescriptor,
) => {
invariant(
collection.critical.draggable.type === descriptor.type,
`We have detected that you have added a Draggable during a drag.
This is not of the same type as the dragging item
Dragging type: ${collection.critical.draggable.type}.
Added type: ${descriptor.type}
We are not allowing this as you can run into problems if your change
has shifted the positioning of other Droppables, or has changed the size of the page`,
);
};
export default (callbacks: Callbacks) => {
const entries: Entries = {
droppables: {},
draggables: {},
};
let collection: ?Collection = null;
const publisher: WhileDraggingPublisher = createPublisher({
callbacks: {
publish: callbacks.publishWhileDragging,
collectionStarting: callbacks.collectionStarting,
getCritical: (): Critical => {
invariant(
collection,
'Cannot get critical when there is no collection',
);
return collection.critical;
},
},
getEntries: (): Entries => entries,
});
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;
}
throwIfAddOrRemoveOfWrongType(collection, descriptor);
// A Draggable has been added during a collection - need to act!
publisher.add(descriptor);
};
const updateDraggable = (
published: DraggableDescriptor,
descriptor: DraggableDescriptor,
getDimension: GetDraggableDimensionFn,
) => {
const existing: ?DraggableEntry = entries.draggables[published.id];
invariant(
existing,
'Cannot update draggable registration as no published registration was found',
);
// If consumers are not using keys correctly then there can be timing issues
// Note: there will still be a crash if starting a drag during the drop animation
if (existing.descriptor === published) {
// id might have changed so we are removing the old entry
delete entries.draggables[published.id];
} else {
warning(`
Detected incorrect usage of 'key' on '<Draggable draggableId="${published.id}"$ />
Your 'key' should be:
- Unique for each Draggable in a list
- Not be based on the index of the Draggable
Usually you want your 'key' to just be the 'draggableId'
More information: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/draggable.md#keys-for-a-list-of-draggable-
`);
}
// adding new entry
const entry: DraggableEntry = {
descriptor,
getDimension,
};
entries.draggables[descriptor.id] = entry;
// it is fine if these are updated during a drag
// this can happen as the index changes
};
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',
);
throwIfAddOrRemoveOfWrongType(collection, descriptor);
publisher.remove(descriptor);
};
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,
};
invariant(!collection, 'Cannot add 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];
invariant(!collection, 'Cannot add a Droppable during a drag');
};
const updateDroppableIsEnabled = (id: DroppableId, isEnabled: boolean) => {
invariant(
entries.droppables[id],
`Cannot update is enabled flag of 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 updateDroppableIsCombineEnabled = (
id: DroppableId,
isCombineEnabled: boolean,
) => {
invariant(
entries.droppables[id],
`Cannot update isCombineEnabled flag of Droppable ${id} as it is not registered`,
);
// no need to update
if (!collection) {
return;
}
callbacks.updateDroppableIsCombineEnabled({ id, isCombineEnabled });
};
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 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;
values(entries.droppables)
.filter(
(entry: DroppableEntry): boolean => entry.descriptor.type === home.type,
)
.forEach((entry: DroppableEntry) => entry.callbacks.dragStopped());
// Finally - clear our collection
collection = null;
};
const startPublishing = (request: LiftRequest): 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 = {
critical,
};
return getInitialPublish({
critical,
entries,
scrollOptions: request.scrollOptions,
});
};
const marshal: DimensionMarshal = {
// dimension registration
registerDraggable,
updateDraggable,
unregisterDraggable,
registerDroppable,
unregisterDroppable,
// droppable changes
updateDroppableIsEnabled,
updateDroppableIsCombineEnabled,
scrollDroppable,
updateDroppableScroll,
// Entry
startPublishing,
stopPublishing,
};
return marshal;
};