UNPKG

react-beautiful-dnd-next

Version:

Beautiful and accessible drag and drop for lists with React

339 lines (278 loc) 9.66 kB
// @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; };