@keenmate/svelte-treeview 4.5.0-rc01 ā 4.6.0
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.
- package/README.md +227 -43
- package/dist/components/Node.svelte +160 -108
- package/dist/components/Node.svelte.d.ts +5 -22
- package/dist/components/RenderCoordinator.svelte.d.ts +29 -0
- package/dist/components/RenderCoordinator.svelte.js +115 -0
- package/dist/components/Tree.svelte +450 -52
- package/dist/components/Tree.svelte.d.ts +81 -6
- package/dist/constants.generated.d.ts +6 -0
- package/dist/constants.generated.js +8 -0
- package/dist/global-api.d.ts +35 -0
- package/dist/global-api.js +36 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +56 -0
- package/dist/logger.js +159 -0
- package/dist/ltree/indexer.d.ts +0 -1
- package/dist/ltree/indexer.js +23 -19
- package/dist/ltree/ltree.svelte.js +109 -34
- package/dist/ltree/types.d.ts +3 -0
- package/dist/perf-logger.d.ts +70 -0
- package/dist/perf-logger.js +196 -0
- package/dist/styles/main.scss +9 -0
- package/dist/styles.css +7 -0
- package/dist/styles.css.map +1 -1
- package/dist/vendor/loglevel/index.d.ts +2 -0
- package/dist/vendor/loglevel/index.js +9 -0
- package/dist/vendor/loglevel/loglevel-esm.d.ts +2 -0
- package/dist/vendor/loglevel/loglevel-esm.js +349 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +7 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.js +132 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +2 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix.js +149 -0
- package/dist/vendor/loglevel/loglevel.js +357 -0
- package/dist/vendor/loglevel/prefix.d.ts +2 -0
- package/dist/vendor/loglevel/prefix.js +9 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ A high-performance, feature-rich hierarchical tree view component for Svelte 5 w
|
|
|
8
8
|
## š Features
|
|
9
9
|
|
|
10
10
|
- **Svelte 5 Native**: Built specifically for Svelte 5 with full support for runes and modern Svelte patterns
|
|
11
|
-
- **High Performance**:
|
|
12
|
-
- **Drag & Drop**: Built-in drag and drop
|
|
11
|
+
- **High Performance**: Flat rendering mode with progressive loading for 5000+ nodes
|
|
12
|
+
- **Drag & Drop**: Built-in drag and drop with position control (above/below/child), touch support, and async validation
|
|
13
|
+
- **Tree Editing**: Built-in methods for add, move, remove operations with automatic path management
|
|
13
14
|
- **Search & Filter**: Integrated FlexSearch for fast, full-text search capabilities
|
|
14
15
|
- **Flexible Data Sources**: Works with any hierarchical data structure
|
|
15
16
|
- **Context Menus**: Dynamic right-click menus with callback-based generation, icons, disabled states
|
|
@@ -60,6 +61,62 @@ If using Vite, Webpack, or similar, you can import the SCSS:
|
|
|
60
61
|
import '@keenmate/svelte-treeview/styles.scss';
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
## ā ļø Performance Warning: Use `$state.raw()` for Large Datasets
|
|
65
|
+
|
|
66
|
+
> [!WARNING]
|
|
67
|
+
> **When passing large arrays (1000+ items) to the Tree component, use `$state.raw()` instead of `$state()` to avoid severe performance issues.**
|
|
68
|
+
|
|
69
|
+
Svelte 5's `$state()` creates deep proxies for all nested objects. With thousands of items, this causes massive overhead during tree operations.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// ā SLOW - Each item becomes a Proxy (5000x slower with large datasets)
|
|
73
|
+
let treeData = $state<TreeNode[]>([])
|
|
74
|
+
|
|
75
|
+
// ā
FAST - Items remain plain objects
|
|
76
|
+
let treeData = $state.raw<TreeNode[]>([])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Symptoms of this issue:**
|
|
80
|
+
- Tree takes 15-90+ seconds to render with thousands of items
|
|
81
|
+
- Console shows `[Violation] 'message' handler took XXXXms`
|
|
82
|
+
- Same data loads instantly in isolated test
|
|
83
|
+
|
|
84
|
+
The array itself remains reactive - only individual items lose deep reactivity (which Tree doesn't need).
|
|
85
|
+
|
|
86
|
+
## š¢ New in v4.6: Progressive Flat Rendering
|
|
87
|
+
|
|
88
|
+
> [!NOTE]
|
|
89
|
+
> **The tree now uses progressive flat rendering by default for significantly improved performance.**
|
|
90
|
+
|
|
91
|
+
**What this means:**
|
|
92
|
+
- The tree renders immediately with the first batch of nodes (~200 by default)
|
|
93
|
+
- Remaining nodes are rendered progressively in subsequent frames
|
|
94
|
+
- For large trees (5000+ nodes), you'll see nodes appear over ~100-500ms instead of a single long freeze
|
|
95
|
+
- The UI remains responsive during rendering
|
|
96
|
+
|
|
97
|
+
**Configuration options:**
|
|
98
|
+
```svelte
|
|
99
|
+
<Tree
|
|
100
|
+
{data}
|
|
101
|
+
useFlatRendering={true} <!-- Default: true (flat mode) -->
|
|
102
|
+
progressiveRender={true} <!-- Default: true (batched rendering) -->
|
|
103
|
+
initialBatchSize={20} <!-- First batch size (default: 20) -->
|
|
104
|
+
maxBatchSize={500} <!-- Maximum batch size cap (default: 500) -->
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Exponential batching:** The first batch renders 20 nodes instantly, then doubles each frame (20 ā 40 ā 80 ā 160 ā 320 ā 500...) for optimal perceived performance.
|
|
109
|
+
|
|
110
|
+
**To use the legacy recursive rendering:**
|
|
111
|
+
```svelte
|
|
112
|
+
<Tree
|
|
113
|
+
{data}
|
|
114
|
+
useFlatRendering={false} <!-- Uses recursive Node components -->
|
|
115
|
+
/>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Recursive mode may be preferred for very small trees or when you need the `{#key changeTracker}` behavior that recreates all nodes on any change.
|
|
119
|
+
|
|
63
120
|
## šÆ Quick Start
|
|
64
121
|
|
|
65
122
|
```svelte
|
|
@@ -215,49 +272,124 @@ For complete FlexSearch documentation, visit: [FlexSearch Options](https://githu
|
|
|
215
272
|
```svelte
|
|
216
273
|
<script lang="ts">
|
|
217
274
|
import { Tree } from '@keenmate/svelte-treeview';
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{ path: 'zone1', name: 'Drop Zone 1' },
|
|
226
|
-
{ path: 'zone2', name: 'Drop Zone 2' }
|
|
275
|
+
|
|
276
|
+
let treeRef: Tree<MyNode>;
|
|
277
|
+
|
|
278
|
+
const data = [
|
|
279
|
+
{ path: '1', name: 'Folder 1', isDraggable: true },
|
|
280
|
+
{ path: '1.1', name: 'Item 1', isDraggable: true },
|
|
281
|
+
{ path: '2', name: 'Folder 2', isDraggable: true }
|
|
227
282
|
];
|
|
228
|
-
|
|
283
|
+
|
|
229
284
|
function onDragStart(node, event) {
|
|
230
285
|
console.log('Dragging:', node.data.name);
|
|
231
286
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
287
|
+
|
|
288
|
+
// Same-tree moves are auto-handled - this callback is for notification/custom logic
|
|
289
|
+
function onDrop(dropNode, draggedNode, position, event, operation) {
|
|
290
|
+
console.log(`Dropped ${draggedNode.data.name} ${position} ${dropNode?.data.name}`);
|
|
291
|
+
// position is 'above', 'below', or 'child'
|
|
292
|
+
// operation is 'move' or 'copy' (Ctrl+drag)
|
|
236
293
|
}
|
|
237
294
|
</script>
|
|
238
295
|
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<div class="col-6">
|
|
250
|
-
<Tree
|
|
251
|
-
data={targetData}
|
|
252
|
-
idMember="path"
|
|
253
|
-
pathMember="path"
|
|
254
|
-
dragOverNodeClass="ltree-dragover-highlight"
|
|
255
|
-
onNodeDrop={onDrop}
|
|
256
|
-
/>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
296
|
+
<Tree
|
|
297
|
+
bind:this={treeRef}
|
|
298
|
+
{data}
|
|
299
|
+
idMember="path"
|
|
300
|
+
pathMember="path"
|
|
301
|
+
orderMember="sortOrder"
|
|
302
|
+
dragOverNodeClass="ltree-dragover-highlight"
|
|
303
|
+
onNodeDragStart={onDragStart}
|
|
304
|
+
onNodeDrop={onDrop}
|
|
305
|
+
/>
|
|
259
306
|
```
|
|
260
307
|
|
|
308
|
+
#### Drop Position Control
|
|
309
|
+
|
|
310
|
+
When using `dropZoneMode="floating"` (default), users can choose where to drop:
|
|
311
|
+
- **Above**: Insert as sibling before the target node
|
|
312
|
+
- **Below**: Insert as sibling after the target node
|
|
313
|
+
- **Child**: Insert as child of the target node
|
|
314
|
+
|
|
315
|
+
#### Async Drop Validation
|
|
316
|
+
|
|
317
|
+
Use `beforeDropCallback` to validate or modify drops, including async operations like confirmation dialogs:
|
|
318
|
+
|
|
319
|
+
```svelte
|
|
320
|
+
<script lang="ts">
|
|
321
|
+
async function beforeDrop(dropNode, draggedNode, position, event, operation) {
|
|
322
|
+
// Cancel specific drops
|
|
323
|
+
if (draggedNode.data.locked) {
|
|
324
|
+
return false; // Cancel the drop
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Show confirmation dialog (async)
|
|
328
|
+
if (position === 'child' && !dropNode.data.isFolder) {
|
|
329
|
+
const confirmed = await showConfirmDialog('Drop as sibling instead?');
|
|
330
|
+
if (!confirmed) return false;
|
|
331
|
+
return { position: 'below' }; // Override position
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Proceed normally
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
</script>
|
|
338
|
+
|
|
339
|
+
<Tree
|
|
340
|
+
{data}
|
|
341
|
+
beforeDropCallback={beforeDrop}
|
|
342
|
+
onNodeDrop={onDrop}
|
|
343
|
+
/>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Tree Editing
|
|
347
|
+
|
|
348
|
+
The tree provides built-in methods for programmatic editing:
|
|
349
|
+
|
|
350
|
+
```svelte
|
|
351
|
+
<script lang="ts">
|
|
352
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
353
|
+
|
|
354
|
+
let treeRef: Tree<MyNode>;
|
|
355
|
+
|
|
356
|
+
// Add a new node
|
|
357
|
+
function addChild() {
|
|
358
|
+
const result = treeRef.addNode(
|
|
359
|
+
selectedNode?.path || '', // parent path (empty = root)
|
|
360
|
+
{ id: Date.now(), path: '', name: 'New Item', sortOrder: 100 }
|
|
361
|
+
);
|
|
362
|
+
if (result.success) {
|
|
363
|
+
console.log('Added:', result.node);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Move a node
|
|
368
|
+
function moveUp() {
|
|
369
|
+
const siblings = treeRef.getSiblings(selectedNode.path);
|
|
370
|
+
const index = siblings.findIndex(s => s.path === selectedNode.path);
|
|
371
|
+
if (index > 0) {
|
|
372
|
+
treeRef.moveNode(selectedNode.path, siblings[index - 1].path, 'above');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Remove a node
|
|
377
|
+
function remove() {
|
|
378
|
+
treeRef.removeNode(selectedNode.path);
|
|
379
|
+
}
|
|
380
|
+
</script>
|
|
381
|
+
|
|
382
|
+
<Tree
|
|
383
|
+
bind:this={treeRef}
|
|
384
|
+
{data}
|
|
385
|
+
idMember="id"
|
|
386
|
+
pathMember="path"
|
|
387
|
+
orderMember="sortOrder"
|
|
388
|
+
/>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Note**: When using `orderMember`, the tree automatically calculates sort order values when moving nodes with 'above' or 'below' positions.
|
|
392
|
+
|
|
261
393
|
### With Context Menus
|
|
262
394
|
|
|
263
395
|
The tree supports context menus with two approaches: callback-based (recommended) and snippet-based.
|
|
@@ -509,6 +641,10 @@ Without both requirements, no search indexing will occur.
|
|
|
509
641
|
|------|------|---------|-------------|
|
|
510
642
|
| `expandLevel` | `number \| null` | `2` | Automatically expand nodes up to this level |
|
|
511
643
|
| `shouldToggleOnNodeClick` | `boolean` | `true` | Toggle expansion on node click |
|
|
644
|
+
| `useFlatRendering` | `boolean` | `true` | Use flat rendering mode (faster for large trees) |
|
|
645
|
+
| `progressiveRender` | `boolean` | `true` | Progressively render nodes in batches |
|
|
646
|
+
| `renderBatchSize` | `number` | `50` | Number of nodes to render per batch |
|
|
647
|
+
| `orderMember` | `string \| null` | `null` | Property name for sort order (enables above/below positioning in drag-drop) |
|
|
512
648
|
| `indexerBatchSize` | `number \| null` | `25` | Number of nodes to process per batch during search indexing |
|
|
513
649
|
| `indexerTimeout` | `number \| null` | `50` | Maximum time (ms) to wait for idle callback before forcing indexing |
|
|
514
650
|
| `shouldDisplayDebugInformation` | `boolean` | `false` | Show debug information panel with tree statistics and enable console debug logging for tree operations and async search indexing |
|
|
@@ -520,7 +656,8 @@ Without both requirements, no search indexing will occur.
|
|
|
520
656
|
| `onNodeClicked` | `(node) => void` | `undefined` | Node click event handler |
|
|
521
657
|
| `onNodeDragStart` | `(node, event) => void` | `undefined` | Drag start event handler |
|
|
522
658
|
| `onNodeDragOver` | `(node, event) => void` | `undefined` | Drag over event handler |
|
|
523
|
-
| `
|
|
659
|
+
| `beforeDropCallback` | `(dropNode, draggedNode, position, event, operation) => boolean \| { position?, operation? } \| Promise<...>` | `undefined` | Async-capable callback to validate/modify drops before they happen |
|
|
660
|
+
| `onNodeDrop` | `(dropNode, draggedNode, position, event, operation) => void` | `undefined` | Drop event handler. Position is 'above', 'below', or 'child'. Operation is 'move' or 'copy' |
|
|
524
661
|
|
|
525
662
|
#### Visual Styling Properties
|
|
526
663
|
| Prop | Type | Default | Description |
|
|
@@ -555,6 +692,12 @@ Without both requirements, no search indexing will occur.
|
|
|
555
692
|
| `searchNodes` | `searchText: string \| null \| undefined, searchOptions?: SearchOptions` | Search nodes using internal search index and return matching nodes with optional FlexSearch options |
|
|
556
693
|
| `scrollToPath` | `path: string, options?: ScrollToPathOptions` | Scroll to and highlight a specific node |
|
|
557
694
|
| `update` | `updates: Partial<Props>` | Programmatically update component props from external JavaScript |
|
|
695
|
+
| `addNode` | `parentPath: string, data: T, pathSegment?: string` | Add a new node under the specified parent |
|
|
696
|
+
| `moveNode` | `sourcePath: string, targetPath: string, position: 'above' \| 'below' \| 'child'` | Move a node to a new location |
|
|
697
|
+
| `removeNode` | `path: string, includeDescendants?: boolean` | Remove a node (and optionally its descendants) |
|
|
698
|
+
| `getNodeByPath` | `path: string` | Get a node by its path |
|
|
699
|
+
| `getChildren` | `parentPath: string` | Get direct children of a node |
|
|
700
|
+
| `getSiblings` | `path: string` | Get siblings of a node (including itself) |
|
|
558
701
|
|
|
559
702
|
#### ScrollToPath Options
|
|
560
703
|
|
|
@@ -720,8 +863,17 @@ Triggered when drag operation starts.
|
|
|
720
863
|
#### onNodeDragOver(node, event)
|
|
721
864
|
Triggered when dragging over a potential drop target.
|
|
722
865
|
|
|
723
|
-
####
|
|
724
|
-
|
|
866
|
+
#### beforeDropCallback(dropNode, draggedNode, position, event, operation)
|
|
867
|
+
Called before a drop is processed. Can be async for showing dialogs.
|
|
868
|
+
- Return `false` to cancel the drop
|
|
869
|
+
- Return `{ position: 'above'|'below'|'child' }` to override position
|
|
870
|
+
- Return `{ operation: 'move'|'copy' }` to override operation
|
|
871
|
+
- Return `true` or `undefined` to proceed normally
|
|
872
|
+
|
|
873
|
+
#### onNodeDrop(dropNode, draggedNode, position, event, operation)
|
|
874
|
+
Triggered when a node is dropped. For same-tree moves, the tree auto-handles the move and this callback is for notification.
|
|
875
|
+
- `position`: 'above', 'below', or 'child'
|
|
876
|
+
- `operation`: 'move' or 'copy' (Ctrl+drag)
|
|
725
877
|
|
|
726
878
|
### Slots
|
|
727
879
|
|
|
@@ -895,14 +1047,46 @@ interface InsertArrayResult<T> {
|
|
|
895
1047
|
|
|
896
1048
|
The component is optimized for large datasets:
|
|
897
1049
|
|
|
1050
|
+
- **Flat Rendering Mode**: Single `{#each}` loop instead of recursive components (default, ~12x faster initial render)
|
|
1051
|
+
- **Progressive Rendering**: Batched rendering prevents UI freeze during initial load
|
|
1052
|
+
- **Context-Based Callbacks**: Stable function references eliminate unnecessary re-renders
|
|
898
1053
|
- **LTree**: Efficient hierarchical data structure
|
|
899
1054
|
- **Async Search Indexing**: Uses `requestIdleCallback` for non-blocking search index building
|
|
900
|
-
- **Accurate Search Results**: Search index only includes successfully inserted nodes
|
|
901
|
-
- **Consistent Visual Hierarchy**: Optimized CSS-based indentation prevents exponential spacing growth
|
|
902
|
-
- **Virtual Scrolling**: (Coming soon)
|
|
903
|
-
- **Lazy Loading**: (Coming soon)
|
|
1055
|
+
- **Accurate Search Results**: Search index only includes successfully inserted nodes
|
|
904
1056
|
- **Search Indexing**: Uses FlexSearch for fast search operations
|
|
905
1057
|
|
|
1058
|
+
### Performance Benchmarks (5500 nodes)
|
|
1059
|
+
|
|
1060
|
+
| Operation | Time |
|
|
1061
|
+
|-----------|------|
|
|
1062
|
+
| Initial render | ~25ms |
|
|
1063
|
+
| Expand/collapse | ~100-150ms |
|
|
1064
|
+
| Search filtering | <50ms |
|
|
1065
|
+
|
|
1066
|
+
### v4.5+ Performance Improvements
|
|
1067
|
+
|
|
1068
|
+
**Flat Rendering Mode** (default) - Renders all visible nodes in a single loop:
|
|
1069
|
+
```svelte
|
|
1070
|
+
<Tree
|
|
1071
|
+
{data}
|
|
1072
|
+
useFlatRendering={true}
|
|
1073
|
+
progressiveRender={true}
|
|
1074
|
+
/>
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
**Optimized `insertArray` algorithm** - Fixed O(n²) bottleneck. Now loads 17,000+ nodes in under 100ms.
|
|
1078
|
+
|
|
1079
|
+
**Performance Logging** - Built-in performance measurement for debugging:
|
|
1080
|
+
```typescript
|
|
1081
|
+
import { enablePerfLogging } from '@keenmate/svelte-treeview';
|
|
1082
|
+
enablePerfLogging();
|
|
1083
|
+
|
|
1084
|
+
// Or from browser console:
|
|
1085
|
+
window.components['svelte-treeview'].perf.enable()
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Important**: See the [$state.raw() warning](#%EF%B8%8F-performance-warning-use-stateraw-for-large-datasets) above - using `$state()` instead of `$state.raw()` for tree data can cause 5,000x slowdown!
|
|
1089
|
+
|
|
906
1090
|
## š¤ Contributing
|
|
907
1091
|
|
|
908
1092
|
We welcome contributions! Please see our contributing guidelines for details.
|