Skip to content

Commit

Permalink
Allow optimistic sorting across sortable groups
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed Sep 21, 2024
1 parent 62a8118 commit 140c318
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/optimistic-sort-across-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dnd-kit/dom': patch
---

Allow the `OptimisticSortingPlugin` to sort elements across different groups.
149 changes: 100 additions & 49 deletions packages/dom/src/sortable/OptimisticSortingPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
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 Default = '__Default__';

export class OptimisticSortingPlugin extends Plugin<DragDropManager> {
constructor(manager: DragDropManager) {
super(manager);

const getSortableInstances = (group: UniqueIdentifier | undefined) => {
const sortableInstances = new Map<number, Sortable>();
const getSortableInstances = () => {
const sortableInstances = new Map<UniqueIdentifier, Set<Sortable>>();

for (const droppable of manager.registry.droppables) {
if (droppable instanceof SortableDroppable) {
const {sortable} = droppable;
const {group = Default} = 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;
};

Expand All @@ -52,48 +56,73 @@ export class OptimisticSortingPlugin extends Plugin<DragDropManager> {
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 ?? Default
);
const targetInstances = sameGroup
? sourceInstances
: instances.get(target.sortable.group ?? Default);

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;

if (!targetElement || !sourceElement) {
return;
}

const orderedSourceSortables = sort(sourceInstances);
const orderedTargetSortables = sameGroup
? orderedSourceSortables
: sort(targetInstances);
const sourceGroup = source.sortable.group ?? Default;
const targetGroup = target.sortable.group ?? Default;
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());
});
});
}),
Expand All @@ -109,44 +138,58 @@ export class OptimisticSortingPlugin extends Plugin<DragDropManager> {
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 ?? Default
);

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;
}

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;
}
}
});
});
Expand All @@ -168,7 +211,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<Sortable>) {
return Array.from(instances).sort(sortByIndex);
}
3 changes: 2 additions & 1 deletion packages/dom/src/sortable/sortable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ export class Sortable<T extends Data = Data> {
public accessor index: number;

previousIndex: number;

initialIndex: number;
initialGroup: UniqueIdentifier | undefined;

@reactive
public accessor group: UniqueIdentifier | undefined;
Expand Down Expand Up @@ -125,6 +125,7 @@ export class Sortable<T extends Data = Data> {
() =>
this.manager?.monitor.addEventListener('dragstart', () => {
this.initialIndex = this.index;
this.initialGroup = this.group;
this.previousIndex = this.index;
}),
() => {
Expand Down

0 comments on commit 140c318

Please sign in to comment.