Skip to content

Commit 8ebcdf8

Browse files
committed
feat: add virtualization
1 parent bba01e7 commit 8ebcdf8

File tree

20 files changed

+1241
-671
lines changed

20 files changed

+1241
-671
lines changed

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

Lines changed: 173 additions & 186 deletions
Large diffs are not rendered by default.

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

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import { DragData } from "./data.js";
1515
import type { TreeItemProps } from "./types.js";
1616
17-
const context = getTreeContext<TFile, TFolder, TTree>();
17+
const treeContext = getTreeContext<TFile, TFolder, TTree>();
1818
1919
let {
2020
children,
@@ -32,17 +32,17 @@
3232
3333
const handleFocusIn: EventHandler<FocusEvent, HTMLDivElement> = (event) => {
3434
onfocusin?.(event);
35-
context.onFocusIn(item, event);
35+
treeContext.onFocusIn(item, event);
3636
};
3737
3838
const handleKeyDown: EventHandler<KeyboardEvent, HTMLDivElement> = (event) => {
3939
onkeydown?.(event);
40-
context.onKeyDown(item, event);
40+
treeContext.onKeyDown(item, event);
4141
};
4242
4343
const handleClick: EventHandler<MouseEvent, HTMLDivElement> = (event) => {
4444
onclick?.(event);
45-
context.onClick(item, event);
45+
treeContext.onClick(item, event);
4646
};
4747
4848
function getItem() {
@@ -57,9 +57,9 @@
5757
return draggable({
5858
element: ref!,
5959
getInitialData: getDragData,
60-
canDrag: (args) => context.canDrag(item, args),
60+
canDrag: (args) => treeContext.canDrag(item, args),
6161
onDragStart: (args) => {
62-
context.onDragStart(item, args);
62+
treeContext.onDragStart(item, args);
6363
},
6464
});
6565
});
@@ -68,7 +68,7 @@
6868
return dropTargetForElements({
6969
element: ref!,
7070
getData: getDragData,
71-
canDrop: (args) => context.canDropElement(item, args),
71+
canDrop: (args) => treeContext.canDropElement(item, args),
7272
onDragEnter: (args) => {
7373
const dragData = args.source.data;
7474
if (!(dragData instanceof DragData)) {
@@ -80,7 +80,7 @@
8080
type: "item",
8181
input: args.location.current.input,
8282
source,
83-
destination: context.getDropDestination(item),
83+
destination: treeContext.getDropDestination(item),
8484
});
8585
},
8686
onDragLeave: (args) => {
@@ -94,7 +94,7 @@
9494
type: "item",
9595
input: args.location.current.input,
9696
source,
97-
destination: context.getDropDestination(item),
97+
destination: treeContext.getDropDestination(item),
9898
});
9999
},
100100
onDrag: (args) => {
@@ -108,7 +108,7 @@
108108
type: "item",
109109
input: args.location.current.input,
110110
source,
111-
destination: context.getDropDestination(item),
111+
destination: treeContext.getDropDestination(item),
112112
});
113113
},
114114
onDrop: (args) => {
@@ -122,7 +122,7 @@
122122
type: "item",
123123
input: args.location.current.input,
124124
source,
125-
destination: context.getDropDestination(item),
125+
destination: treeContext.getDropDestination(item),
126126
});
127127
},
128128
});
@@ -132,62 +132,63 @@
132132
return dropTargetForExternal({
133133
element: ref!,
134134
getData: getDragData,
135-
canDrop: (args) => context.canDropExternal(item, args),
135+
canDrop: (args) => treeContext.canDropExternal(item, args),
136136
onDragEnter: (args) => {
137137
onDragEnter({
138138
type: "external",
139139
input: args.location.current.input,
140140
items: args.source.items,
141-
destination: context.getDropDestination(item),
141+
destination: treeContext.getDropDestination(item),
142142
});
143143
},
144144
onDragLeave: (args) => {
145145
onDragLeave({
146146
type: "external",
147147
input: args.location.current.input,
148148
items: args.source.items,
149-
destination: context.getDropDestination(item),
149+
destination: treeContext.getDropDestination(item),
150150
});
151151
},
152152
onDrag: (args) => {
153153
onDrag({
154154
type: "external",
155155
input: args.location.current.input,
156156
items: args.source.items,
157-
destination: context.getDropDestination(item),
157+
destination: treeContext.getDropDestination(item),
158158
});
159159
},
160160
onDrop: (args) => {
161161
onDrop({
162162
type: "external",
163163
input: args.location.current.input,
164164
items: args.source.items,
165-
destination: context.getDropDestination(item),
165+
destination: treeContext.getDropDestination(item),
166166
});
167167
},
168168
});
169169
});
170170
171171
$effect(() => {
172172
return () => {
173-
context.onDestroyItem(item);
173+
treeContext.onDestroyItem(item);
174174
};
175175
});
176176
</script>
177177
178178
<div
179179
{...rest}
180180
bind:this={ref}
181-
id={context.getItemElementId(item.node.id)}
181+
id={treeContext.getItemElementId(item.node.id)}
182182
role="treeitem"
183183
aria-selected={item.selected}
184184
aria-expanded={item.node.type === "folder" && item.node.children.length !== 0
185185
? item.expanded
186186
: undefined}
187187
aria-level={item.depth + 1}
188188
aria-posinset={item.index + 1}
189-
aria-setsize={item.parent?.node.children.length ?? context.root().children.length}
190-
tabindex={context.tabbableId() === item.node.id ? 0 : -1}
189+
aria-setsize={item.parent?.node.children.length ?? treeContext.root().children.length}
190+
aria-disabled={item.disabled}
191+
tabindex={treeContext.tabbableId() === item.node.id ? 0 : -1}
191192
onfocusin={handleFocusIn}
192193
onkeydown={handleKeyDown}
193194
onclick={handleClick}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<script lang="ts" module>
2+
import {
3+
elementScroll,
4+
observeElementOffset,
5+
observeElementRect,
6+
Virtualizer,
7+
type ScrollToOptions,
8+
type VirtualItem,
9+
type VirtualizerOptions,
10+
} from "@tanstack/virtual-core";
11+
import { getContext, setContext } from "svelte";
12+
import { FileNode, FolderNode, TreeItemState, type DefaultTFolder } from "$lib/tree.svelte.js";
13+
import type { VirtualListProps } from "./types.js";
14+
15+
export type VirtualListContext<
16+
TFile extends FileNode,
17+
TFolder extends FolderNode<TFile | TFolder>,
18+
> = {
19+
scrollToIndex: (index: number, options?: ScrollToOptions) => void;
20+
setItems: (value: Array<TreeItemState<TFile, TFolder>>) => void;
21+
};
22+
23+
const CONTEXT_KEY = Symbol("VirtualListContext");
24+
25+
export function getVirtualListContextIfExists<
26+
TFile extends FileNode,
27+
TFolder extends FolderNode<TFile | TFolder>,
28+
>(): VirtualListContext<TFile, TFolder> | undefined {
29+
return getContext(CONTEXT_KEY);
30+
}
31+
</script>
32+
33+
<script
34+
lang="ts"
35+
generics="TFile extends FileNode = FileNode, TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>"
36+
>
37+
let {
38+
children,
39+
estimateSize,
40+
overscan = 1,
41+
paddingStart,
42+
paddingEnd,
43+
scrollPaddingStart,
44+
scrollPaddingEnd,
45+
scrollMargin,
46+
gap,
47+
ref = $bindable(null),
48+
...rest
49+
}: VirtualListProps = $props();
50+
51+
let treeSize = $state.raw(0);
52+
let virtualItems: Array<VirtualItem> = $state.raw([]);
53+
let items: Array<TreeItemState<TFile, TFolder>> = $state.raw([]);
54+
55+
const options: VirtualizerOptions<HTMLElement, HTMLElement> = {
56+
count: 0,
57+
overscan,
58+
paddingStart,
59+
paddingEnd,
60+
scrollPaddingStart,
61+
scrollPaddingEnd,
62+
scrollMargin,
63+
gap,
64+
getScrollElement: () => ref,
65+
getItemKey: (index) => items[index]!.node.id,
66+
estimateSize: (index) => {
67+
const item = items[index]!;
68+
if (!item.visible) {
69+
return 0;
70+
}
71+
return estimateSize(item, index);
72+
},
73+
onChange: (instance) => {
74+
instance._willUpdate();
75+
treeSize = instance.getTotalSize();
76+
virtualItems = instance.getVirtualItems();
77+
},
78+
scrollToFn: elementScroll,
79+
observeElementOffset,
80+
observeElementRect,
81+
};
82+
const virtualizer = new Virtualizer(options);
83+
84+
export function getTreeSize() {
85+
return treeSize;
86+
}
87+
88+
export function getVirtualItems() {
89+
return virtualItems;
90+
}
91+
92+
export const { scrollToIndex, scrollToOffset, scrollBy } = virtualizer;
93+
94+
const context: VirtualListContext<TFile, TFolder> = {
95+
scrollToIndex,
96+
setItems: (value) => {
97+
items = value;
98+
},
99+
};
100+
setContext(CONTEXT_KEY, context);
101+
102+
$effect(() => {
103+
const cleanup = virtualizer._didMount();
104+
virtualizer._willUpdate();
105+
return cleanup;
106+
});
107+
108+
$effect(() => {
109+
options.count = items.length;
110+
options.overscan = overscan;
111+
options.paddingStart = paddingStart;
112+
options.paddingEnd = paddingEnd;
113+
options.scrollPaddingStart = scrollPaddingStart;
114+
options.scrollPaddingEnd = scrollPaddingEnd;
115+
options.scrollMargin = scrollMargin;
116+
options.gap = gap;
117+
virtualizer.setOptions(options);
118+
virtualizer.measure();
119+
});
120+
</script>
121+
122+
<div {...rest} bind:this={ref}>
123+
{@render children({ treeSize, virtualItems })}
124+
</div>

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Input as DragInput } from "@atlaskit/pragmatic-drag-and-drop/types";
2+
import type { ScrollToOptions, VirtualItem } from "@tanstack/virtual-core";
23
import type { Snippet } from "svelte";
34
import type { HTMLAttributes } from "svelte/elements";
45
import type { SvelteSet } from "svelte/reactivity";
@@ -138,6 +139,7 @@ export interface TreeProps<
138139
pasteOperation?: PasteOperation;
139140
ref?: HTMLDivElement | null;
140141
isItemDisabled?: (node: TFile | TFolder) => boolean;
142+
isItemHidden?: (node: TFile | TFolder) => boolean;
141143
copyNode?: (node: TFile | TFolder) => TFile | TFolder;
142144
onResolveNameConflict?: (
143145
args: OnResolveNameConflictArgs<TFile, TFolder, TTree>,
@@ -157,11 +159,6 @@ export interface TreeProps<
157159
onDrop?: (args: DragEventArgs<TFile, TFolder, TTree>) => void;
158160
}
159161

160-
export type TreeCopyToClipboardMethodOptions = {
161-
pasteOperation?: PasteOperation;
162-
batched?: boolean;
163-
};
164-
165162
export type TreeRemoveMethodOptions = {
166163
batched?: boolean;
167164
};
@@ -179,6 +176,7 @@ export interface TreeItemProps<
179176
| "aria-level"
180177
| "aria-posinset"
181178
| "aria-setsize"
179+
| "aria-disabled"
182180
| "tabindex"
183181
> {
184182
children: Snippet;
@@ -189,3 +187,26 @@ export interface TreeItemProps<
189187
onDrag?: (args: DragEventArgs<TFile, TFolder, TTree>) => void;
190188
onDrop?: (args: DragEventArgs<TFile, TFolder, TTree>) => void;
191189
}
190+
191+
export type VirtualListChildrenSnippetArgs = {
192+
treeSize: number;
193+
virtualItems: Array<VirtualItem>;
194+
};
195+
196+
export interface VirtualListProps<
197+
TFile extends FileNode = FileNode,
198+
TFolder extends FolderNode<TFile | TFolder> = DefaultTFolder<TFile>,
199+
> extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
200+
children: Snippet<[args: VirtualListChildrenSnippetArgs]>;
201+
estimateSize: (item: TreeItemState<TFile, TFolder>, index: number) => number;
202+
overscan?: number;
203+
paddingStart?: number;
204+
paddingEnd?: number;
205+
scrollPaddingStart?: number;
206+
scrollPaddingEnd?: number;
207+
scrollMargin?: number;
208+
gap?: number;
209+
ref?: HTMLDivElement | null;
210+
}
211+
212+
export type VirtualListScrollToOptions = ScrollToOptions;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as Tree } from "./components/Tree.svelte";
22
export { default as TreeItem } from "./components/TreeItem.svelte";
3+
export { default as VirtualList } from "./components/VirtualList.svelte";
34
export type * from "./components/types.js";
45
export * from "./tree.svelte.js";

0 commit comments

Comments
 (0)