@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
168 lines (141 loc) • 4.52 kB
text/typescript
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,
};
}