Skip to content

Commit 8dfcebe

Browse files
author
abdel-17
committed
implement reversible dnd
1 parent aacf3d4 commit 8dfcebe

File tree

7 files changed

+377
-98
lines changed

7 files changed

+377
-98
lines changed

packages/svelte-file-tree/src/lib/components/Tree/Tree.svelte

Lines changed: 282 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,43 @@
11
<script lang="ts">
22
import { Ref } from "$lib/internal/ref.js";
3-
import type { FileOrFolder } from "$lib/tree.svelte.js";
3+
import type { FileOrFolder, FolderNode } from "$lib/tree.svelte.js";
4+
import { flushSync } from "svelte";
45
import TreeItemProvider from "./TreeItemProvider.svelte";
56
import { type TreeContext, setTreeContext } from "./context.svelte.js";
6-
import { DraggedState, TabbableState } from "./state.svelte.js";
7+
import { DraggedIdState, TabbableIdState } from "./state.svelte.js";
78
import type { TreeProps } from "./types.js";
89
910
const { tree, item, onTreeChange, onTreeChangeError, ...attributes }: TreeProps = $props();
1011
const treeRef = new Ref(() => tree);
1112
12-
const lookup = new Map<string, TreeContext.Item>();
13-
const tabbable = new TabbableState(treeRef);
14-
const dragged = new DraggedState();
13+
const lookup = new Map<string, TreeProps.Item>();
14+
const tabbableId = new TabbableIdState(treeRef);
15+
const draggedId = new DraggedIdState();
1516
16-
function onSetItem(item: TreeContext.Item): void {
17+
function onSetItem(item: TreeProps.Item): void {
1718
lookup.set(item.node.id, item);
1819
}
1920
2021
function onDeleteItem(id: string): void {
21-
tree.selectedIds.delete(id);
22-
tree.expandedIds.delete(id);
23-
tree.clipboard.delete(id);
2422
lookup.delete(id);
2523
26-
if (tabbable.id === id) {
27-
tabbable.clear();
24+
if (tabbableId.current === id) {
25+
tabbableId.clear();
2826
}
2927
30-
if (dragged.id === id) {
31-
dragged.clear();
28+
if (draggedId.current === id) {
29+
draggedId.clear();
3230
}
3331
}
3432
35-
function getChildren(parent: TreeProps.ItemParent | undefined): FileOrFolder[] {
36-
if (parent === undefined) {
33+
function getChildren(item: TreeProps.Item<FolderNode> | undefined): FileOrFolder[] {
34+
if (item === undefined) {
3735
return tree.children;
3836
}
39-
return parent.node.children;
37+
return item.node.children;
4038
}
4139
42-
function getNextItem({ node, index, parent }: TreeContext.Item): TreeContext.Item | undefined {
40+
function getNextItem({ node, index, parent }: TreeProps.Item): TreeProps.Item | undefined {
4341
if (node.type === "folder" && node.expanded && node.children.length !== 0) {
4442
return {
4543
node: node.children[0],
@@ -68,11 +66,7 @@
6866
}
6967
}
7068
71-
function getPreviousItem({
72-
node,
73-
index,
74-
parent,
75-
}: TreeContext.Item): TreeContext.Item | undefined {
69+
function getPreviousItem({ node, index, parent }: TreeProps.Item): TreeProps.Item | undefined {
7670
if (index === 0) {
7771
return parent;
7872
}
@@ -90,7 +84,7 @@
9084
}
9185
9286
function selectUntil({ node, element }: TreeContext.SelectUntilArgs): void {
93-
let lastSelected: TreeContext.Item | undefined;
87+
let lastSelected: TreeProps.Item | undefined;
9488
for (const id of tree.selectedIds) {
9589
const current = lookup.get(id);
9690
if (current !== undefined) {
@@ -99,7 +93,7 @@
9993
}
10094
10195
if (lastSelected === undefined) {
102-
let current: TreeContext.Item | undefined = {
96+
let current: TreeProps.Item | undefined = {
10397
node: tree.children[0],
10498
index: 0,
10599
parent: undefined,
@@ -122,7 +116,7 @@
122116
const following = positionBitmask & Node.DOCUMENT_POSITION_FOLLOWING;
123117
const navigate = following ? getNextItem : getPreviousItem;
124118
125-
let current: TreeContext.Item | undefined = lastSelected;
119+
let current: TreeProps.Item | undefined = lastSelected;
126120
while (current.node !== node) {
127121
current = navigate(current);
128122
if (current === undefined) {
@@ -148,7 +142,6 @@
148142
type: "rename:conflict",
149143
node,
150144
name,
151-
conflicting: sibling,
152145
});
153146
return false;
154147
}
@@ -158,27 +151,283 @@
158151
node.name = name;
159152
onTreeChange?.({
160153
type: "rename",
161-
node,
162-
oldName,
163-
newName: name,
154+
change: {
155+
id: node.id,
156+
oldName,
157+
newName: name,
158+
},
159+
rollback() {
160+
node.name = oldName;
161+
},
164162
});
165163
return true;
166164
}
167165
166+
function dropDragged({
167+
draggedId,
168+
node,
169+
index,
170+
parent,
171+
position,
172+
}: TreeContext.DropDraggedArgs): void {
173+
const dragged = lookup.get(draggedId);
174+
if (dragged === undefined) {
175+
return;
176+
}
177+
178+
const draggedParentId = dragged.parent?.node.id;
179+
const draggedSiblings = getChildren(dragged.parent);
180+
181+
const changes: TreeProps.ReorderChange[] = [];
182+
let parentChanged: boolean;
183+
switch (position) {
184+
case "before":
185+
case "after": {
186+
const parentId = parent?.node.id;
187+
parentChanged = draggedParentId !== parentId;
188+
if (parentChanged) {
189+
draggedSiblings.splice(dragged.index, 1);
190+
191+
for (let i = dragged.index; i < draggedSiblings.length; i++) {
192+
changes.push({
193+
id: draggedSiblings[i].id,
194+
oldParentId: draggedParentId,
195+
oldIndex: i + 1,
196+
newParentId: draggedParentId,
197+
newIndex: i,
198+
});
199+
}
200+
201+
const siblings = getChildren(parent);
202+
const newIndex = position === "before" ? index : index + 1;
203+
siblings.splice(newIndex, 0, dragged.node);
204+
205+
changes.push({
206+
id: draggedId,
207+
oldParentId: draggedParentId,
208+
oldIndex: dragged.index,
209+
newParentId: parentId,
210+
newIndex,
211+
});
212+
213+
for (let i = newIndex + 1; i < siblings.length; i++) {
214+
changes.push({
215+
id: siblings[i].id,
216+
oldParentId: parentId,
217+
oldIndex: i - 1,
218+
newParentId: parentId,
219+
newIndex: i,
220+
});
221+
}
222+
223+
onDropDragged(dragged, parentChanged);
224+
onTreeChange?.({
225+
type: "reorder",
226+
changes,
227+
rollback() {
228+
siblings.splice(newIndex, 1);
229+
draggedSiblings.splice(dragged.index, 0, dragged.node);
230+
231+
flushSync();
232+
dragged.node.element?.focus();
233+
},
234+
});
235+
} else {
236+
// It's more efficient to reorder the items in-place.
237+
let newIndex: number;
238+
if (dragged.index < index) {
239+
// d z
240+
// 1 2 <> 3 4 5
241+
// 1 3 2 <> 4 5
242+
// 1 3 4 2 5
243+
newIndex = position === "before" ? index - 1 : index;
244+
for (let i = dragged.index; i < newIndex; i++) {
245+
const current = draggedSiblings[i];
246+
const next = draggedSiblings[i + 1];
247+
draggedSiblings[i] = next;
248+
draggedSiblings[i + 1] = current;
249+
250+
changes.push({
251+
id: next.id,
252+
oldParentId: draggedParentId,
253+
oldIndex: i + 1,
254+
newParentId: draggedParentId,
255+
newIndex: i,
256+
});
257+
}
258+
} else {
259+
// z d
260+
// 1 2 3 <> 4 5
261+
// 1 2 <> 4 3 5
262+
// 1 4 2 3 5
263+
newIndex = position === "before" ? index : index + 1;
264+
for (let i = dragged.index; i > newIndex; i--) {
265+
const current = draggedSiblings[i];
266+
const previous = draggedSiblings[i - 1];
267+
draggedSiblings[i] = previous;
268+
draggedSiblings[i - 1] = current;
269+
270+
changes.push({
271+
id: previous.id,
272+
oldParentId: draggedParentId,
273+
oldIndex: i - 1,
274+
newParentId: draggedParentId,
275+
newIndex: i,
276+
});
277+
}
278+
}
279+
280+
if (newIndex === dragged.index) {
281+
return;
282+
}
283+
284+
changes.push({
285+
id: draggedId,
286+
oldParentId: draggedParentId,
287+
oldIndex: dragged.index,
288+
newParentId: draggedParentId,
289+
newIndex,
290+
});
291+
292+
onDropDragged(dragged, parentChanged);
293+
onTreeChange?.({
294+
type: "reorder",
295+
changes,
296+
rollback() {
297+
if (newIndex < dragged.index) {
298+
for (let i = newIndex; i < dragged.index; i++) {
299+
const current = draggedSiblings[i];
300+
const next = draggedSiblings[i + 1];
301+
draggedSiblings[i] = next;
302+
draggedSiblings[i + 1] = current;
303+
}
304+
} else {
305+
for (let i = newIndex; i > dragged.index; i--) {
306+
const current = draggedSiblings[i];
307+
const previous = draggedSiblings[i - 1];
308+
draggedSiblings[i] = previous;
309+
draggedSiblings[i - 1] = current;
310+
}
311+
}
312+
},
313+
});
314+
}
315+
break;
316+
}
317+
case "inside": {
318+
if (node.type === "file") {
319+
throw new Error("Cannot drop an item inside a file");
320+
}
321+
322+
parentChanged = draggedParentId !== node.id;
323+
if (parentChanged) {
324+
draggedSiblings.splice(dragged.index, 1);
325+
326+
for (let i = dragged.index; i < draggedSiblings.length; i++) {
327+
changes.push({
328+
id: draggedSiblings[i].id,
329+
oldParentId: draggedParentId,
330+
oldIndex: i + 1,
331+
newParentId: draggedParentId,
332+
newIndex: i,
333+
});
334+
}
335+
336+
const newLength = node.children.push(dragged.node);
337+
const newIndex = newLength - 1;
338+
339+
changes.push({
340+
id: draggedId,
341+
oldParentId: draggedParentId,
342+
oldIndex: dragged.index,
343+
newParentId: node.id,
344+
newIndex,
345+
});
346+
347+
onDropDragged(dragged, parentChanged);
348+
onTreeChange?.({
349+
type: "reorder",
350+
changes,
351+
rollback() {
352+
node.children.pop();
353+
draggedSiblings.splice(dragged.index, 0, dragged.node);
354+
},
355+
});
356+
357+
node.expand();
358+
} else {
359+
const lastIndex = draggedSiblings.length - 1;
360+
if (dragged.index === lastIndex) {
361+
return;
362+
}
363+
364+
// It's more efficient to reorder the items in-place.
365+
for (let i = dragged.index; i < lastIndex; i++) {
366+
const current = draggedSiblings[i];
367+
const next = draggedSiblings[i + 1];
368+
draggedSiblings[i] = next;
369+
draggedSiblings[i + 1] = current;
370+
371+
changes.push({
372+
id: next.id,
373+
oldParentId: draggedParentId,
374+
oldIndex: i + 1,
375+
newParentId: draggedParentId,
376+
newIndex: i,
377+
});
378+
}
379+
380+
changes.push({
381+
id: draggedId,
382+
oldParentId: draggedParentId,
383+
oldIndex: dragged.index,
384+
newParentId: draggedParentId,
385+
newIndex: lastIndex,
386+
});
387+
388+
onDropDragged(dragged, parentChanged);
389+
onTreeChange?.({
390+
type: "reorder",
391+
changes,
392+
rollback() {
393+
for (let i = lastIndex; i > dragged.index; i--) {
394+
const current = draggedSiblings[i];
395+
const previous = draggedSiblings[i - 1];
396+
draggedSiblings[i] = previous;
397+
draggedSiblings[i - 1] = current;
398+
}
399+
},
400+
});
401+
}
402+
break;
403+
}
404+
}
405+
}
406+
407+
function onDropDragged(dragged: TreeProps.Item, parentChanged: boolean): void {
408+
tree.selectedIds.clear();
409+
dragged.node.select();
410+
411+
if (parentChanged) {
412+
flushSync();
413+
dragged.node.element?.focus();
414+
}
415+
}
416+
168417
setTreeContext({
169418
tree: treeRef,
170-
lookup,
171-
tabbable,
172-
dragged,
419+
tabbableId,
420+
draggedId,
173421
getChildren,
174422
getNextItem,
175423
getPreviousItem,
176424
selectUntil,
177425
renameItem,
426+
dropDragged,
178427
});
179428
</script>
180429

181-
{#snippet items(nodes = tree.children, depth = 0, parent?: TreeProps.ItemParent)}
430+
{#snippet items(nodes = tree.children, depth = 0, parent?: TreeProps.Item<FolderNode>)}
182431
{#each nodes as node, index (node.id)}
183432
<TreeItemProvider {node} {index} {depth} {parent} {onSetItem} {onDeleteItem}>
184433
{@render item({ node, index, depth, parent })}

0 commit comments

Comments
 (0)