@keenmate/svelte-treeview 4.8.0 → 5.0.0-rc02

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +106 -117
  2. package/ai/INDEX.txt +310 -0
  3. package/ai/advanced-patterns.txt +506 -0
  4. package/ai/basic-setup.txt +336 -0
  5. package/ai/context-menu.txt +349 -0
  6. package/ai/data-handling.txt +390 -0
  7. package/ai/drag-drop.txt +397 -0
  8. package/ai/events-callbacks.txt +382 -0
  9. package/ai/import-patterns.txt +271 -0
  10. package/ai/performance.txt +349 -0
  11. package/ai/search-features.txt +359 -0
  12. package/ai/styling-theming.txt +354 -0
  13. package/ai/tree-editing.txt +423 -0
  14. package/ai/typescript-types.txt +357 -0
  15. package/dist/components/Node.svelte +47 -40
  16. package/dist/components/Node.svelte.d.ts +1 -1
  17. package/dist/components/Tree.svelte +384 -1479
  18. package/dist/components/Tree.svelte.d.ts +30 -28
  19. package/dist/components/TreeProvider.svelte +28 -0
  20. package/dist/components/TreeProvider.svelte.d.ts +28 -0
  21. package/dist/constants.generated.d.ts +1 -1
  22. package/dist/constants.generated.js +1 -1
  23. package/dist/core/TreeController.svelte.d.ts +353 -0
  24. package/dist/core/TreeController.svelte.js +1503 -0
  25. package/dist/core/createTreeController.d.ts +9 -0
  26. package/dist/core/createTreeController.js +11 -0
  27. package/dist/global-api.d.ts +1 -1
  28. package/dist/global-api.js +5 -5
  29. package/dist/index.d.ts +10 -6
  30. package/dist/index.js +7 -3
  31. package/dist/logger.d.ts +7 -6
  32. package/dist/logger.js +0 -2
  33. package/dist/ltree/indexer.js +2 -4
  34. package/dist/ltree/ltree-node.svelte.d.ts +2 -1
  35. package/dist/ltree/ltree-node.svelte.js +1 -0
  36. package/dist/ltree/ltree.svelte.d.ts +1 -1
  37. package/dist/ltree/ltree.svelte.js +168 -175
  38. package/dist/ltree/types.d.ts +12 -8
  39. package/dist/perf-logger.d.ts +2 -1
  40. package/dist/perf-logger.js +0 -2
  41. package/dist/styles/main.scss +78 -78
  42. package/dist/styles.css +41 -41
  43. package/dist/styles.css.map +1 -1
  44. package/dist/vendor/loglevel/index.d.ts +55 -2
  45. package/dist/vendor/loglevel/prefix.d.ts +23 -2
  46. package/package.json +96 -95
  47. package/dist/ltree/ltree-demo.d.ts +0 -2
  48. package/dist/ltree/ltree-demo.js +0 -90
  49. package/dist/vendor/loglevel/loglevel-esm.d.ts +0 -2
  50. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +0 -7
  51. package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +0 -2
@@ -0,0 +1,506 @@
1
+ ADVANCED PATTERNS
2
+ =================
3
+
4
+ Complex use cases and integration patterns for the tree component.
5
+
6
+ EXTERNAL UPDATE API
7
+ -------------------
8
+ Update props from vanilla JavaScript:
9
+
10
+ // Get reference to tree
11
+ const treeElement = document.querySelector('#my-tree');
12
+
13
+ // Update multiple props
14
+ treeElement.update({
15
+ searchText: 'query',
16
+ expandLevel: 3,
17
+ data: newData
18
+ });
19
+
20
+ // Update single prop
21
+ treeElement.update({ searchText: '' });
22
+
23
+ All props updatable except snippets:
24
+ - data, searchText, selectedNode, expandLevel
25
+ - Members: idMember, pathMember, displayValueMember
26
+ - Callbacks: sortCallback, onNodeClicked, etc.
27
+ - Visual: bodyClass, selectedNodeClass, etc.
28
+
29
+ LAZY LOADING CHILDREN
30
+ ---------------------
31
+ Load children on demand:
32
+
33
+ <script>
34
+ let data = $state.raw([
35
+ { id: '1', path: '1', name: 'Folder', hasChildren: true }
36
+ ]);
37
+
38
+ async function handleClick(node) {
39
+ if (node.data.hasChildren && !node.hasChildren) {
40
+ // Fetch children from server
41
+ const children = await fetchChildren(node.path);
42
+
43
+ // Add to data
44
+ data = [...data, ...children];
45
+
46
+ // Expand the node
47
+ treeRef.expandNodes(node.path);
48
+ }
49
+ }
50
+ </script>
51
+
52
+ <Tree
53
+ {data}
54
+ hasChildrenMember="hasChildren"
55
+ onNodeClicked={handleClick}
56
+ />
57
+
58
+ VIRTUALIZED TREE (EXTERNAL)
59
+ ---------------------------
60
+ For extremely large trees (50,000+ nodes), implement external virtualization:
61
+
62
+ <script>
63
+ import { VirtualList } from 'svelte-virtual-list';
64
+
65
+ let flatNodes = $derived(getFlatVisibleNodes(treeData));
66
+
67
+ function getFlatVisibleNodes(data) {
68
+ // Flatten tree respecting expanded state
69
+ const result = [];
70
+ function traverse(nodes, level) {
71
+ for (const node of nodes) {
72
+ result.push({ ...node, level });
73
+ if (node.isExpanded && node.children) {
74
+ traverse(node.children, level + 1);
75
+ }
76
+ }
77
+ }
78
+ traverse(data, 0);
79
+ return result;
80
+ }
81
+ </script>
82
+
83
+ <VirtualList items={flatNodes} let:item>
84
+ <div style="padding-left: {item.level * 20}px">
85
+ {item.name}
86
+ </div>
87
+ </VirtualList>
88
+
89
+ MULTI-SELECT MODE
90
+ -----------------
91
+ Implement checkbox-based multi-selection:
92
+
93
+ <script>
94
+ let selectedPaths = $state(new Set());
95
+
96
+ function toggleSelection(node) {
97
+ const newSet = new Set(selectedPaths);
98
+ if (newSet.has(node.path)) {
99
+ newSet.delete(node.path);
100
+ } else {
101
+ newSet.add(node.path);
102
+ }
103
+ selectedPaths = newSet;
104
+ }
105
+
106
+ function isSelected(path) {
107
+ return selectedPaths.has(path);
108
+ }
109
+ </script>
110
+
111
+ <Tree {data}>
112
+ {#snippet nodeTemplate(node)}
113
+ <label>
114
+ <input
115
+ type="checkbox"
116
+ checked={isSelected(node.path)}
117
+ onchange={() => toggleSelection(node)}
118
+ />
119
+ {node.data.name}
120
+ </label>
121
+ {/snippet}
122
+ </Tree>
123
+
124
+ KEYBOARD NAVIGATION
125
+ -------------------
126
+ Add keyboard support:
127
+
128
+ <script>
129
+ let selectedNode = $state(null);
130
+ let treeRef;
131
+
132
+ function handleKeydown(e) {
133
+ if (!selectedNode) return;
134
+
135
+ switch (e.key) {
136
+ case 'ArrowDown':
137
+ selectNext();
138
+ break;
139
+ case 'ArrowUp':
140
+ selectPrevious();
141
+ break;
142
+ case 'ArrowRight':
143
+ treeRef.expandNodes(selectedNode.path);
144
+ break;
145
+ case 'ArrowLeft':
146
+ treeRef.collapseNodes(selectedNode.path);
147
+ break;
148
+ case 'Enter':
149
+ activateNode(selectedNode);
150
+ break;
151
+ }
152
+ }
153
+
154
+ function selectNext() {
155
+ const flat = treeRef.ltree.flatTreeNodes;
156
+ const idx = flat.findIndex(n => n.path === selectedNode.path);
157
+ if (idx < flat.length - 1) {
158
+ selectedNode = flat[idx + 1];
159
+ treeRef.scrollToPath(selectedNode.path);
160
+ }
161
+ }
162
+ </script>
163
+
164
+ <div onkeydown={handleKeydown} tabindex="0">
165
+ <Tree bind:this={treeRef} bind:selectedNode />
166
+ </div>
167
+
168
+ UNDO/REDO SYSTEM
169
+ ----------------
170
+ Track operations for undo:
171
+
172
+ <script>
173
+ let history = $state([]);
174
+ let historyIndex = $state(-1);
175
+
176
+ function execute(operation, undoOperation) {
177
+ operation();
178
+
179
+ // Clear redo stack
180
+ history = history.slice(0, historyIndex + 1);
181
+ history = [...history, { redo: operation, undo: undoOperation }];
182
+ historyIndex = history.length - 1;
183
+ }
184
+
185
+ function undo() {
186
+ if (historyIndex >= 0) {
187
+ history[historyIndex].undo();
188
+ historyIndex--;
189
+ }
190
+ }
191
+
192
+ function redo() {
193
+ if (historyIndex < history.length - 1) {
194
+ historyIndex++;
195
+ history[historyIndex].redo();
196
+ }
197
+ }
198
+
199
+ // Example: tracked add
200
+ function addNode(parentPath, nodeData) {
201
+ let addedPath;
202
+
203
+ execute(
204
+ () => {
205
+ const result = treeRef.addNode(parentPath, nodeData);
206
+ addedPath = result.node?.path;
207
+ },
208
+ () => {
209
+ if (addedPath) treeRef.removeNode(addedPath);
210
+ }
211
+ );
212
+ }
213
+ </script>
214
+
215
+ REAL-TIME SYNC
216
+ --------------
217
+ Sync with WebSocket/server:
218
+
219
+ <script>
220
+ import { onMount } from 'svelte';
221
+
222
+ let data = $state.raw([]);
223
+
224
+ onMount(() => {
225
+ const ws = new WebSocket('ws://server/tree');
226
+
227
+ ws.onmessage = (event) => {
228
+ const message = JSON.parse(event.data);
229
+
230
+ switch (message.type) {
231
+ case 'add':
232
+ data = [...data, message.node];
233
+ break;
234
+ case 'remove':
235
+ data = data.filter(d => !d.path.startsWith(message.path));
236
+ break;
237
+ case 'update':
238
+ data = data.map(d =>
239
+ d.path === message.path ? { ...d, ...message.changes } : d
240
+ );
241
+ break;
242
+ case 'full':
243
+ data = message.nodes;
244
+ break;
245
+ }
246
+ };
247
+
248
+ return () => ws.close();
249
+ });
250
+ </script>
251
+
252
+ FILTERED EXTERNAL VIEW
253
+ ----------------------
254
+ Keep full data but show filtered view:
255
+
256
+ <script>
257
+ let allData = $state.raw([]);
258
+ let filter = $state('all');
259
+
260
+ let filteredData = $derived(() => {
261
+ if (filter === 'all') return allData;
262
+ return allData.filter(item => item.type === filter);
263
+ });
264
+ </script>
265
+
266
+ <select bind:value={filter}>
267
+ <option value="all">All</option>
268
+ <option value="folder">Folders</option>
269
+ <option value="file">Files</option>
270
+ </select>
271
+
272
+ <Tree data={filteredData} ... />
273
+
274
+ Note: This loses hierarchy for filtered items. Better to use internal search.
275
+
276
+ DRAG BETWEEN MULTIPLE TREES
277
+ ---------------------------
278
+ <script>
279
+ let sourceData = $state.raw([...]);
280
+ let targetData = $state.raw([...]);
281
+
282
+ function handleSourceDrop(dropNode, draggedNode, position, event, operation) {
283
+ // Item dropped back to source
284
+ if (draggedNode.data.sourceTree === 'source') {
285
+ // Handle internal move
286
+ }
287
+ }
288
+
289
+ function handleTargetDrop(dropNode, draggedNode, position, event, operation) {
290
+ // Item dropped to target from source
291
+ if (draggedNode.data.sourceTree === 'source') {
292
+ // Remove from source
293
+ sourceData = sourceData.filter(d => d.path !== draggedNode.path);
294
+
295
+ // Add to target
296
+ const newItem = {
297
+ ...draggedNode.data,
298
+ path: calculateNewPath(dropNode, position),
299
+ sourceTree: 'target'
300
+ };
301
+ targetData = [...targetData, newItem];
302
+ }
303
+ }
304
+ </script>
305
+
306
+ <Tree
307
+ data={sourceData}
308
+ treeId="source"
309
+ dragDropMode="cross"
310
+ onNodeDrop={handleSourceDrop}
311
+ />
312
+
313
+ <Tree
314
+ data={targetData}
315
+ treeId="target"
316
+ dragDropMode="both"
317
+ onNodeDrop={handleTargetDrop}
318
+ />
319
+
320
+ FORM INTEGRATION
321
+ ----------------
322
+ Use tree as form input:
323
+
324
+ <script>
325
+ let selectedPaths = $state([]);
326
+
327
+ function handleSubmit(e) {
328
+ const formData = new FormData(e.target);
329
+ formData.set('selectedNodes', JSON.stringify(selectedPaths));
330
+ // Submit form
331
+ }
332
+ </script>
333
+
334
+ <form onsubmit={handleSubmit}>
335
+ <input type="hidden" name="selectedNodes" value={JSON.stringify(selectedPaths)} />
336
+
337
+ <Tree {data}>
338
+ {#snippet nodeTemplate(node)}
339
+ <label>
340
+ <input
341
+ type="checkbox"
342
+ checked={selectedPaths.includes(node.path)}
343
+ onchange={(e) => {
344
+ if (e.target.checked) {
345
+ selectedPaths = [...selectedPaths, node.path];
346
+ } else {
347
+ selectedPaths = selectedPaths.filter(p => p !== node.path);
348
+ }
349
+ }}
350
+ />
351
+ {node.data.name}
352
+ </label>
353
+ {/snippet}
354
+ </Tree>
355
+
356
+ <button type="submit">Submit</button>
357
+ </form>
358
+
359
+ BREADCRUMB NAVIGATION
360
+ ---------------------
361
+ <script>
362
+ let selectedNode = $state(null);
363
+
364
+ let breadcrumbs = $derived(() => {
365
+ if (!selectedNode) return [];
366
+
367
+ const crumbs = [];
368
+ let currentPath = selectedNode.path;
369
+
370
+ while (currentPath) {
371
+ const node = treeRef.getNodeByPath(currentPath);
372
+ if (node) crumbs.unshift(node);
373
+ currentPath = node?.parentPath;
374
+ }
375
+
376
+ return crumbs;
377
+ });
378
+
379
+ function navigateTo(node) {
380
+ selectedNode = node;
381
+ treeRef.scrollToPath(node.path, { expand: true, highlight: true });
382
+ }
383
+ </script>
384
+
385
+ <nav class="breadcrumbs">
386
+ {#each breadcrumbs as crumb, i}
387
+ {#if i > 0} / {/if}
388
+ <button onclick={() => navigateTo(crumb)}>
389
+ {crumb.data?.name}
390
+ </button>
391
+ {/each}
392
+ </nav>
393
+
394
+ <Tree bind:this={treeRef} bind:selectedNode />
395
+
396
+ PERMISSION-BASED ACTIONS
397
+ ------------------------
398
+ <script>
399
+ const permissions = {
400
+ canEdit: true,
401
+ canDelete: false,
402
+ canCreate: true
403
+ };
404
+
405
+ function createContextMenu(node, closeMenuCallback) {
406
+ const items = [];
407
+
408
+ items.push({
409
+ icon: '📂',
410
+ title: 'Open',
411
+ callback: () => { openItem(node); closeMenuCallback(); }
412
+ });
413
+
414
+ if (permissions.canEdit) {
415
+ items.push({
416
+ icon: '✏️',
417
+ title: 'Edit',
418
+ callback: () => editItem(node)
419
+ });
420
+ }
421
+
422
+ if (permissions.canCreate && node.data.type === 'folder') {
423
+ items.push({
424
+ icon: '➕',
425
+ title: 'New Item',
426
+ callback: () => createItem(node)
427
+ });
428
+ }
429
+
430
+ if (permissions.canDelete) {
431
+ items.push({ isDivider: true });
432
+ items.push({
433
+ icon: '🗑️',
434
+ title: 'Delete',
435
+ callback: () => deleteItem(node)
436
+ });
437
+ }
438
+
439
+ return items;
440
+ }
441
+ </script>
442
+
443
+ TREE DIFF/COMPARE
444
+ -----------------
445
+ Compare two tree states:
446
+
447
+ <script>
448
+ function diffTrees(oldData, newData) {
449
+ const oldPaths = new Set(oldData.map(d => d.path));
450
+ const newPaths = new Set(newData.map(d => d.path));
451
+
452
+ const added = newData.filter(d => !oldPaths.has(d.path));
453
+ const removed = oldData.filter(d => !newPaths.has(d.path));
454
+ const modified = newData.filter(d => {
455
+ const old = oldData.find(o => o.path === d.path);
456
+ return old && JSON.stringify(old) !== JSON.stringify(d);
457
+ });
458
+
459
+ return { added, removed, modified };
460
+ }
461
+ </script>
462
+
463
+ SSR CONSIDERATIONS
464
+ ------------------
465
+ Tree works with SSR (SvelteKit):
466
+
467
+ // +page.svelte
468
+ <script>
469
+ import { Tree } from '@keenmate/svelte-treeview';
470
+ import { browser } from '$app/environment';
471
+
472
+ export let data; // From +page.server.ts
473
+ </script>
474
+
475
+ {#if browser}
476
+ <Tree data={data.items} ... />
477
+ {:else}
478
+ <div class="tree-placeholder">Loading tree...</div>
479
+ {/if}
480
+
481
+ Or with onMount:
482
+
483
+ <script>
484
+ import { onMount } from 'svelte';
485
+
486
+ let mounted = $state(false);
487
+ onMount(() => { mounted = true; });
488
+ </script>
489
+
490
+ {#if mounted}
491
+ <Tree ... />
492
+ {/if}
493
+
494
+ BEST PRACTICES
495
+ --------------
496
+ ✅ Use $state.raw() for large datasets
497
+ ✅ Debounce expensive operations
498
+ ✅ Handle async operations properly
499
+ ✅ Implement undo for destructive actions
500
+ ✅ Use WebSocket for real-time updates
501
+ ✅ Consider SSR implications
502
+
503
+ ❌ Don't block UI with sync operations
504
+ ❌ Don't forget error handling
505
+ ❌ Don't implement virtual scroll unless needed (50K+ nodes)
506
+ ❌ Don't mutate data directly