import { invariant } from '../../invariant'; import type { TypeId, DraggableId, DroppableId } from '../../types'; import type { Registry, DraggableAPI, DroppableAPI, DraggableEntry, DroppableEntry, RegistryEvent, Subscriber, Unsubscribe, DraggableEntryMap, DroppableEntryMap, } from './registry-types'; interface EntryMap { draggables: DraggableEntryMap; droppables: DroppableEntryMap; } export default function createRegistry(): Registry { const entries: EntryMap = { draggables: {}, droppables: {}, }; const subscribers: Subscriber[] = []; function subscribe(cb: Subscriber): Unsubscribe { subscribers.push(cb); return function unsubscribe(): void { const index: number = subscribers.indexOf(cb); // might have been removed by a clean if (index === -1) { return; } subscribers.splice(index, 1); }; } function notify(event: RegistryEvent) { if (subscribers.length) { subscribers.forEach((cb) => cb(event)); } } function findDraggableById(id: DraggableId): DraggableEntry | null { return entries.draggables[id] || null; } function getDraggableById(id: DraggableId): DraggableEntry { const entry: DraggableEntry | null = findDraggableById(id); invariant(entry, `Cannot find draggable entry with id [${id}]`); return entry; } const draggableAPI: DraggableAPI = { register: (entry: DraggableEntry) => { entries.draggables[entry.descriptor.id] = entry; notify({ type: 'ADDITION', value: entry }); }, update: (entry: DraggableEntry, last: DraggableEntry) => { const current: DraggableEntry | null = entries.draggables[last.descriptor.id]; // item already removed if (!current) { return; } // id already used for another mount if (current.uniqueId !== entry.uniqueId) { return; } // We are safe to delete the old entry and add a new one delete entries.draggables[last.descriptor.id]; entries.draggables[entry.descriptor.id] = entry; }, unregister: (entry: DraggableEntry) => { const draggableId: DraggableId = entry.descriptor.id; const current: DraggableEntry | null = findDraggableById(draggableId); // can occur if cleaned before unregistration if (!current) { return; } // outdated uniqueId if (entry.uniqueId !== current.uniqueId) { return; } delete entries.draggables[draggableId]; // make sure the entry exists before removal if (entries.droppables[entry.descriptor.droppableId]) { notify({ type: 'REMOVAL', value: entry }); } }, getById: getDraggableById, findById: findDraggableById, exists: (id: DraggableId): boolean => Boolean(findDraggableById(id)), getAllByType: (type: TypeId): DraggableEntry[] => Object.values(entries.draggables).filter( (entry: DraggableEntry): boolean => entry.descriptor.type === type, ), }; function findDroppableById(id: DroppableId): DroppableEntry | null { return entries.droppables[id] || null; } function getDroppableById(id: DroppableId): DroppableEntry { const entry: DroppableEntry | null = findDroppableById(id); invariant(entry, `Cannot find droppable entry with id [${id}]`); return entry; } const droppableAPI: DroppableAPI = { register: (entry: DroppableEntry) => { entries.droppables[entry.descriptor.id] = entry; }, unregister: (entry: DroppableEntry) => { const current: DroppableEntry | null = findDroppableById( entry.descriptor.id, ); // can occur if cleaned before an unregistry if (!current) { return; } // already changed if (entry.uniqueId !== current.uniqueId) { return; } delete entries.droppables[entry.descriptor.id]; }, getById: getDroppableById, findById: findDroppableById, exists: (id: DroppableId): boolean => Boolean(findDroppableById(id)), getAllByType: (type: TypeId): DroppableEntry[] => Object.values(entries.droppables).filter( (entry: DroppableEntry): boolean => entry.descriptor.type === type, ), }; function clean(): void { // kill entries entries.draggables = {}; entries.droppables = {}; // remove all subscribers subscribers.length = 0; } return { draggable: draggableAPI, droppable: droppableAPI, subscribe, clean, }; }