Skip to content

Commit 2a5b656

Browse files
authored
Merge pull request #71 from abdel-17/add-file-tree-class
Add `FileTree` class
2 parents 502a9b0 + d1ad816 commit 2a5b656

File tree

13 files changed

+1226
-452
lines changed

13 files changed

+1226
-452
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"format:check": "pnpm --parallel --color format:check"
1414
},
1515
"devDependencies": {
16-
"@changesets/cli": "^2.29.3",
16+
"@changesets/cli": "^2.29.4",
1717
"playwright": "^1.52.0",
18-
"prettier-plugin-svelte": "^3.3.3"
18+
"prettier-plugin-svelte": "^3.4.0"
1919
},
2020
"pnpm": {
2121
"onlyBuiltDependencies": [

packages/svelte-file-tree/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# svelte-file-tree
22

3+
## 0.7.0
4+
5+
### Minor Changes
6+
7+
- feat: add `FileTree` class
8+
- feat: add `TreeItemState.type` property (always equal to "item")
9+
- breaking: change the type of `TreeProps.root` to `FileTree`.
10+
- breaking: change the type of any `destination` property from `FolderNode` to `FolderNode | FileTree`.
11+
312
## 0.6.0
413

514
### Patch Changes

packages/svelte-file-tree/package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "svelte-file-tree",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"type": "module",
55
"scripts": {
66
"dev": "svelte-kit sync && svelte-package --watch",
@@ -31,26 +31,26 @@
3131
"types": "./dist/index.d.ts",
3232
"svelte": "./dist/index.js",
3333
"peerDependencies": {
34-
"svelte": "^5.20.0"
34+
"svelte": "^5.31.0"
3535
},
3636
"dependencies": {
37-
"@atlaskit/pragmatic-drag-and-drop": "^1.6.1",
37+
"@atlaskit/pragmatic-drag-and-drop": "^1.7.0",
3838
"esm-env": "^1.2.2"
3939
},
4040
"devDependencies": {
41-
"@sveltejs/kit": "^2.20.8",
41+
"@sveltejs/kit": "^2.21.1",
4242
"@sveltejs/package": "^2.3.11",
4343
"@sveltejs/vite-plugin-svelte": "5.0.3",
44-
"@types/node": "^22.15.17",
45-
"@vitest/browser": "^3.1.3",
44+
"@types/node": "^22.15.29",
45+
"@vitest/browser": "^3.1.4",
4646
"jsdom": "^26.1.0",
4747
"prettier": "^3.5.3",
48-
"prettier-plugin-svelte": "^3.3.3",
48+
"prettier-plugin-svelte": "^3.4.0",
4949
"publint": "^0.3.12",
50-
"svelte": "5.28.2",
51-
"svelte-check": "^4.1.7",
50+
"svelte": "5.33.11",
51+
"svelte-check": "^4.2.1",
5252
"vite": "^6.3.5",
53-
"vitest": "3.1.3",
53+
"vitest": "3.1.4",
5454
"vitest-browser-svelte": "^0.1.0"
5555
},
5656
"repository": {

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<script
22
lang="ts"
3-
generics="TFile extends FileNode = FileNode, TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>"
3+
generics="TFile extends FileNode = FileNode, TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>, TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>"
44
>
55
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
66
import { DEV } from "esm-env";
77
import { SvelteSet } from "svelte/reactivity";
88
import { isControlOrMeta, noop, truePredicate } from "$lib/helpers.js";
99
import {
1010
FileNode,
11+
FileTree,
1112
FolderNode,
1213
TreeItemState,
1314
type DefaultTFolder,
@@ -61,7 +62,7 @@
6162
canRemove = truePredicate,
6263
onRemove = noop,
6364
...rest
64-
}: TreeProps<TFile, TFolder> = $props();
65+
}: TreeProps<TFile, TFolder, TTree> = $props();
6566
6667
const uid = $props.id();
6768
let tabbableId: string | undefined = $state.raw();
@@ -274,7 +275,7 @@
274275
return item.inClipboard;
275276
}
276277
277-
async function copy(clipboardIds: Set<string>, destination: TFolder) {
278+
async function copy(clipboardIds: Set<string>, destination: TFolder | TTree) {
278279
const names = new Set<string>();
279280
for (const child of destination.children) {
280281
names.add(child.name);
@@ -340,7 +341,7 @@
340341
async function move(
341342
movedIds: Set<string>,
342343
isItemMoved: (item: TreeItemState<TFile, TFolder>) => boolean,
343-
destination: TFolder,
344+
destination: TFolder | TTree,
344345
) {
345346
const names = new Set<string>();
346347
for (const child of destination.children) {
@@ -349,7 +350,7 @@
349350
350351
const sources: Array<TreeItemState<TFile, TFolder>> = [];
351352
const sourceIds = new Set<string>();
352-
const sourceOwners = new Set<TFolder>();
353+
const sourceOwners = new Set<TFolder | TTree>();
353354
for (const id of movedIds) {
354355
const current = getItem(id);
355356
if (current === undefined) {
@@ -421,7 +422,7 @@
421422
return true;
422423
}
423424
424-
export async function paste(destination: TFolder) {
425+
export async function paste(destination: TFolder | TTree) {
425426
if (clipboard === undefined) {
426427
return false;
427428
}
@@ -468,7 +469,7 @@
468469
469470
async function _remove(item: TreeItemState<TFile, TFolder>) {
470471
const removed: Array<TreeItemState<TFile, TFolder>> = [];
471-
const removedOwners = new Set<TFolder>();
472+
const removedOwners = new Set<TFolder | TTree>();
472473
for (const id of selectedIds) {
473474
const current = getItem(id);
474475
if (current === undefined) {
@@ -983,7 +984,7 @@
983984
return;
984985
}
985986
986-
let dropDestination: TFolder;
987+
let dropDestination: TFolder | TTree;
987988
const dropTarget = dropTargets[0];
988989
if (dropTarget.element === args.self.element) {
989990
dropDestination = root;

packages/svelte-file-tree/src/lib/components/context.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import type {
55
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
66
import { DEV } from "esm-env";
77
import { getContext, hasContext, setContext } from "svelte";
8-
import type { DefaultTFolder, FileNode, FolderNode, TreeItemState } from "$lib/tree.svelte.js";
8+
import type {
9+
DefaultTFolder,
10+
FileNode,
11+
FileTree,
12+
FolderNode,
13+
TreeItemState,
14+
} from "$lib/tree.svelte.js";
915

1016
export type TreeItemEvent<TEvent extends Event = Event> = TEvent & {
1117
currentTarget: HTMLDivElement;
@@ -14,8 +20,9 @@ export type TreeItemEvent<TEvent extends Event = Event> = TEvent & {
1420
export type TreeContext<
1521
TFile extends FileNode = FileNode,
1622
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
23+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
1724
> = {
18-
root: () => TFolder;
25+
root: () => TTree;
1926
tabbableId: () => string;
2027
getItemElementId: (itemId: string) => string;
2128
onFocusIn: (item: TreeItemState<TFile, TFolder>, event: TreeItemEvent<FocusEvent>) => void;
@@ -32,7 +39,8 @@ const CONTEXT_KEY = Symbol("TreeContext");
3239
export function getTreeContext<
3340
TFile extends FileNode = FileNode,
3441
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
35-
>(): TreeContext<TFile, TFolder> {
42+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
43+
>(): TreeContext<TFile, TFolder, TTree> {
3644
if (DEV && !hasContext(CONTEXT_KEY)) {
3745
throw new Error("No parent <Tree> found");
3846
}
@@ -42,6 +50,7 @@ export function getTreeContext<
4250
export function setTreeContext<
4351
TFile extends FileNode = FileNode,
4452
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
45-
>(context: TreeContext<TFile, TFolder>) {
53+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
54+
>(context: TreeContext<TFile, TFolder, TTree>) {
4655
setContext(CONTEXT_KEY, context);
4756
}

packages/svelte-file-tree/src/lib/components/types.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { SvelteSet } from "svelte/reactivity";
55
import type {
66
DefaultTFolder,
77
FileNode,
8+
FileTree,
89
FolderNode,
910
TreeClipboard,
1011
TreeItemState,
@@ -28,25 +29,28 @@ export type OnClipboardChangeArgs = {
2829
export type OnChildrenChangeArgs<
2930
TFile extends FileNode = FileNode,
3031
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
32+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
3133
> = {
3234
operation: "insert" | "remove";
33-
target: TFolder;
35+
target: TFolder | TTree;
3436
children: Array<TFile | TFolder>;
3537
};
3638

3739
export type OnDropDestinationChangeArgs<
3840
TFile extends FileNode = FileNode,
3941
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
42+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
4043
> = {
41-
dropDestination: TFolder | undefined;
44+
dropDestination: TFolder | TTree | undefined;
4245
};
4346

4447
export type OnResolveNameConflictArgs<
4548
TFile extends FileNode = FileNode,
4649
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
50+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
4751
> = {
4852
operation: "copy" | "move";
49-
destination: TFolder;
53+
destination: TFolder | TTree;
5054
name: string;
5155
};
5256

@@ -63,17 +67,19 @@ export type OnCircularReferenceArgs<
6367
export type OnCopyArgs<
6468
TFile extends FileNode = FileNode,
6569
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
70+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
6671
> = {
6772
sources: Array<TreeItemState<TFile, TFolder>>;
68-
destination: TFolder;
73+
destination: TFolder | TTree;
6974
};
7075

7176
export type OnMoveArgs<
7277
TFile extends FileNode = FileNode,
7378
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
79+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
7480
> = {
7581
sources: Array<TreeItemState<TFile, TFolder>>;
76-
destination: TFolder;
82+
destination: TFolder | TTree;
7783
};
7884

7985
export type OnRemoveArgs<
@@ -86,9 +92,10 @@ export type OnRemoveArgs<
8692
export interface TreeProps<
8793
TFile extends FileNode = FileNode,
8894
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
95+
TTree extends FileTree<TFile | TFolder> = FileTree<TFile | TFolder>,
8996
> extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "role" | "aria-multiselectable"> {
9097
children: Snippet<[args: TreeChildrenSnippetArgs<TFile, TFolder>]>;
91-
root: TFolder;
98+
root: TTree;
9299
defaultSelectedIds?: Iterable<string>;
93100
selectedIds?: SvelteSet<string>;
94101
defaultExpandedIds?: Iterable<string>;
@@ -98,16 +105,16 @@ export interface TreeProps<
98105
ref?: HTMLElement | null;
99106
copyNode?: (node: TFile | TFolder) => TFile | TFolder;
100107
onClipboardChange?: (args: OnClipboardChangeArgs) => void;
101-
onChildrenChange?: (args: OnChildrenChangeArgs<TFile, TFolder>) => void;
102-
onDropDestinationChange?: (args: OnDropDestinationChangeArgs<TFile, TFolder>) => void;
108+
onChildrenChange?: (args: OnChildrenChangeArgs<TFile, TFolder, TTree>) => void;
109+
onDropDestinationChange?: (args: OnDropDestinationChangeArgs<TFile, TFolder, TTree>) => void;
103110
onResolveNameConflict?: (
104-
args: OnResolveNameConflictArgs<TFile, TFolder>,
111+
args: OnResolveNameConflictArgs<TFile, TFolder, TTree>,
105112
) => MaybePromise<NameConflictResolution>;
106113
onCircularReference?: (args: OnCircularReferenceArgs<TFile, TFolder>) => void;
107-
canCopy?: (args: OnCopyArgs<TFile, TFolder>) => MaybePromise<boolean>;
108-
onCopy?: (args: OnCopyArgs<TFile, TFolder>) => void;
109-
canMove?: (args: OnMoveArgs<TFile, TFolder>) => MaybePromise<boolean>;
110-
onMove?: (args: OnMoveArgs<TFile, TFolder>) => void;
114+
canCopy?: (args: OnCopyArgs<TFile, TFolder, TTree>) => MaybePromise<boolean>;
115+
onCopy?: (args: OnCopyArgs<TFile, TFolder, TTree>) => void;
116+
canMove?: (args: OnMoveArgs<TFile, TFolder, TTree>) => MaybePromise<boolean>;
117+
onMove?: (args: OnMoveArgs<TFile, TFolder, TTree>) => void;
111118
canRemove?: (args: OnRemoveArgs<TFile, TFolder>) => MaybePromise<boolean>;
112119
onRemove?: (args: OnRemoveArgs<TFile, TFolder>) => void;
113120
}

packages/svelte-file-tree/src/lib/tree.svelte.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
import type { SvelteSet } from "svelte/reactivity";
22

3+
function getTotalCount(children: Array<FileTreeNode>) {
4+
let result = 0;
5+
for (const child of children) {
6+
result++;
7+
8+
if (child.type === "folder") {
9+
result += child.count;
10+
}
11+
}
12+
return result;
13+
}
14+
15+
export class FileTree<TNode extends FileNode | FolderNode<TNode> = FileTreeNode> {
16+
children: Array<TNode>;
17+
18+
constructor(children: Array<TNode>) {
19+
this.children = $state(children);
20+
}
21+
22+
readonly type = "tree";
23+
24+
readonly count = $derived.by(() => getTotalCount(this.children));
25+
}
26+
327
export type FileNodeProps = {
428
id: string;
529
name: string;
630
};
731

832
export class FileNode {
933
id: string;
10-
name = $state.raw("");
34+
name: string;
1135

1236
constructor(props: FileNodeProps) {
13-
this.id = props.id;
14-
this.name = props.name;
37+
this.id = $state.raw(props.id);
38+
this.name = $state.raw(props.name);
1539
}
1640

1741
readonly type = "file";
@@ -25,28 +49,18 @@ export type FolderNodeProps<TNode extends FileNode | FolderNode<TNode> = FileTre
2549

2650
export class FolderNode<TNode extends FileNode | FolderNode<TNode> = FileTreeNode> {
2751
id: string;
28-
name = $state.raw("");
29-
children: Array<TNode> = $state([]);
52+
name: string;
53+
children: Array<TNode>;
3054

3155
constructor(props: FolderNodeProps<TNode>) {
32-
this.id = props.id;
33-
this.name = props.name;
34-
this.children = props.children;
56+
this.id = $state.raw(props.id);
57+
this.name = $state.raw(props.name);
58+
this.children = $state(props.children);
3559
}
3660

3761
readonly type = "folder";
3862

39-
readonly count = $derived.by(() => {
40-
let result = 0;
41-
for (const child of this.children) {
42-
result++;
43-
44-
if (child.type === "folder") {
45-
result += child.count;
46-
}
47-
}
48-
return result;
49-
});
63+
readonly count = $derived.by(() => getTotalCount(this.children));
5064
}
5165

5266
export type FileTreeNode = FileNode | FolderNode<FileTreeNode>;
@@ -101,6 +115,8 @@ export class TreeItemState<
101115
this.#isItemDisabled = props.isItemDisabled;
102116
}
103117

118+
readonly type = "item";
119+
104120
readonly selected = $derived.by(() => this.#selectedIds().has(this.node.id));
105121

106122
readonly expanded = $derived.by(() => this.#expandedIds().has(this.node.id));

0 commit comments

Comments
 (0)