From 0c7bf85897992dc48c3cf2f1deeaa896995bfcc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claud=C3=A9ric=20Demers?= Date: Fri, 20 Sep 2024 20:17:43 -0400 Subject: [PATCH] Allow optimistic sorting across sortable groups --- .changeset/optimistic-sort-across-group.md | 5 + .../src/sortable/OptimisticSortingPlugin.ts | 150 ++++++++++++------ packages/dom/src/sortable/sortable.ts | 3 +- 3 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 .changeset/optimistic-sort-across-group.md diff --git a/.changeset/optimistic-sort-across-group.md b/.changeset/optimistic-sort-across-group.md new file mode 100644 index 00000000..de24e830 --- /dev/null +++ b/.changeset/optimistic-sort-across-group.md @@ -0,0 +1,5 @@ +--- +'@dnd-kit/dom': patch +--- + +Allow the `OptimisticSortingPlugin` to sort elements across different groups. diff --git a/packages/dom/src/sortable/OptimisticSortingPlugin.ts b/packages/dom/src/sortable/OptimisticSortingPlugin.ts index f321c816..8931c999 100644 --- a/packages/dom/src/sortable/OptimisticSortingPlugin.ts +++ b/packages/dom/src/sortable/OptimisticSortingPlugin.ts @@ -1,36 +1,43 @@ -import {Plugin, UniqueIdentifier} from '@dnd-kit/abstract'; +import {Plugin, type UniqueIdentifier} from '@dnd-kit/abstract'; import type {DragDropManager} from '@dnd-kit/dom'; -import {arrayMove} from '@dnd-kit/helpers'; +import {move} from '@dnd-kit/helpers'; import {isSortable} from './utilities.ts'; import {Sortable, SortableDroppable} from './sortable.ts'; import {batch} from '@dnd-kit/state'; +const defaultGroup = '__default__'; + export class OptimisticSortingPlugin extends Plugin { constructor(manager: DragDropManager) { super(manager); - const getSortableInstances = (group: UniqueIdentifier | undefined) => { - const sortableInstances = new Map(); + const getSortableInstances = () => { + const sortableInstances = new Map< + UniqueIdentifier | undefined, + Set + >(); for (const droppable of manager.registry.droppables) { if (droppable instanceof SortableDroppable) { const {sortable} = droppable; + const {group} = sortable; - if (sortable.group !== group) { - continue; - } + let instances = sortableInstances.get(group); - if (sortableInstances.has(sortable.index)) { - throw new Error( - `Duplicate sortable index found for same sortable group: ${sortable.droppable.id} and ${sortableInstances.get(sortable.index)?.droppable.id} have the same index (${sortable.index}). Make sure each sortable item has a unique index. Use the \`group\` attribute to separate sortables into different groups.` - ); + if (!instances) { + instances = new Set(); + sortableInstances.set(group, instances); } - sortableInstances.set(sortable.index, sortable); + instances.add(sortable); } } + for (const [group, instances] of sortableInstances) { + sortableInstances.set(group, new Set(sort(instances))); + } + return sortableInstances; }; @@ -52,34 +59,28 @@ export class OptimisticSortingPlugin extends Plugin { return; } - if (source.sortable.group !== target.sortable.group) { - return; - } + const instances = getSortableInstances(); + const sameGroup = source.sortable.group === target.sortable.group; + const sourceInstances = instances.get(source.sortable.group); + const targetInstances = sameGroup + ? sourceInstances + : instances.get(target.sortable.group); - const sortableInstances = getSortableInstances(source.sortable.group); + if (!sourceInstances || !targetInstances) return; // Wait for the renderer to handle the event before attempting to optimistically update manager.renderer.rendering.then(() => { - for (const [index, sortable] of sortableInstances.entries()) { - if (sortable.index !== index) { - // At least one index was changed so we should abort optimistic updates - return; + for (const [group, sortableInstances] of instances.entries()) { + const entries = Array.from(sortableInstances).entries(); + + for (const [index, sortable] of entries) { + if (sortable.index !== index || sortable.group !== group) { + // At least one index or group was changed so we should abort optimistic updates + return; + } } } - const orderedSortables = Array.from( - sortableInstances.values() - ).sort((a, b) => a.index - b.index); - - const sourceIndex = orderedSortables.indexOf(source.sortable); - const targetIndex = orderedSortables.indexOf(target.sortable); - - const newOrder = arrayMove( - orderedSortables, - sourceIndex, - targetIndex - ); - const sourceElement = source.sortable.element; const targetElement = target.sortable.element; @@ -87,13 +88,42 @@ export class OptimisticSortingPlugin extends Plugin { return; } + const orderedSourceSortables = sort(sourceInstances); + const orderedTargetSortables = sameGroup + ? orderedSourceSortables + : sort(targetInstances); + const sourceGroup = source.sortable.group ?? defaultGroup; + const targetGroup = target.sortable.group ?? defaultGroup; + const state = { + [sourceGroup]: orderedSourceSortables, + [targetGroup]: orderedTargetSortables, + }; + const newState = move(state, source, target); + const sourceIndex = newState[targetGroup].indexOf(source.sortable); + const targetIndex = newState[targetGroup].indexOf(target.sortable); + reorder(sourceElement, sourceIndex, targetElement, targetIndex); + manager.collisionObserver.disable(); + batch(() => { - for (const [index, sortable] of newOrder.entries()) { + for (const [index, sortable] of newState[sourceGroup].entries()) { sortable.index = index; } + + if (!sameGroup) { + for (const [index, sortable] of newState[ + targetGroup + ].entries()) { + sortable.group = target.sortable.group; + sortable.index = index; + } + } }); + + manager.actions + .setDropTarget(source.id) + .then(() => manager.collisionObserver.enable()); }); }); }), @@ -109,29 +139,38 @@ export class OptimisticSortingPlugin extends Plugin { return; } - if (source.sortable.initialIndex === source.sortable.index) { + if ( + source.sortable.initialIndex === source.sortable.index && + source.sortable.initialGroup === source.sortable.group + ) { return; } queueMicrotask(() => { - const sortableInstances = getSortableInstances(source.sortable.group); + const instances = getSortableInstances(); + const initialGroupInstances = instances.get( + source.sortable.initialGroup + ); + + if (!initialGroupInstances) return; // Wait for the renderer to handle the event before attempting to optimistically update manager.renderer.rendering.then(() => { - for (const [index, sortable] of sortableInstances.entries()) { - if (sortable.index !== index) { - // At least one index was changed so we should abort optimistic updates - return; + for (const [group, sortableInstances] of instances.entries()) { + const entries = Array.from(sortableInstances).entries(); + + for (const [index, sortable] of entries) { + if (sortable.index !== index || sortable.group !== group) { + // At least one index or group was changed so we should abort optimistic updates + return; + } } } - const orderedSortables = Array.from( - sortableInstances.values() - ).sort((a, b) => a.index - b.index); - + const initialGroup = sort(initialGroupInstances); const sourceElement = source.sortable.element; const targetElement = - orderedSortables[source.sortable.initialIndex]?.element; + initialGroup[source.sortable.initialIndex]?.element; if (!targetElement || !sourceElement) { return; @@ -139,14 +178,19 @@ export class OptimisticSortingPlugin extends Plugin { reorder( sourceElement, - source.sortable.index, + source.sortable.initialIndex, targetElement, source.sortable.initialIndex ); batch(() => { - for (const sortable of orderedSortables.values()) { - sortable.index = sortable.initialIndex; + for (const [_, sortableInstances] of instances.entries()) { + const entries = Array.from(sortableInstances).values(); + + for (const sortable of entries) { + sortable.index = sortable.initialIndex; + sortable.group = sortable.initialGroup; + } } }); }); @@ -168,7 +212,15 @@ function reorder( targetElement: Element, targetIndex: number ) { - const position = targetIndex < sourceIndex ? 'beforebegin' : 'afterend'; + const position = targetIndex < sourceIndex ? 'afterend' : 'beforebegin'; targetElement.insertAdjacentElement(position, sourceElement); } + +function sortByIndex(a: Sortable, b: Sortable) { + return a.index - b.index; +} + +function sort(instances: Set) { + return Array.from(instances).sort(sortByIndex); +} diff --git a/packages/dom/src/sortable/sortable.ts b/packages/dom/src/sortable/sortable.ts index 8701ddc9..d459e78d 100644 --- a/packages/dom/src/sortable/sortable.ts +++ b/packages/dom/src/sortable/sortable.ts @@ -94,8 +94,8 @@ export class Sortable { public accessor index: number; previousIndex: number; - initialIndex: number; + initialGroup: UniqueIdentifier | undefined; @reactive public accessor group: UniqueIdentifier | undefined; @@ -125,6 +125,7 @@ export class Sortable { () => this.manager?.monitor.addEventListener('dragstart', () => { this.initialIndex = this.index; + this.initialGroup = this.group; this.previousIndex = this.index; }), () => {