@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.
Files changed (36) hide show
  1. package/README.md +227 -43
  2. package/dist/components/Node.svelte +160 -108
  3. package/dist/components/Node.svelte.d.ts +5 -22
  4. package/dist/components/RenderCoordinator.svelte.d.ts +29 -0
  5. package/dist/components/RenderCoordinator.svelte.js +115 -0
  6. package/dist/components/Tree.svelte +450 -52
  7. package/dist/components/Tree.svelte.d.ts +81 -6
  8. package/dist/constants.generated.d.ts +6 -0
  9. package/dist/constants.generated.js +8 -0
  10. package/dist/global-api.d.ts +35 -0
  11. package/dist/global-api.js +36 -0
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.js +5 -0
  14. package/dist/logger.d.ts +56 -0
  15. package/dist/logger.js +159 -0
  16. package/dist/ltree/indexer.d.ts +0 -1
  17. package/dist/ltree/indexer.js +23 -19
  18. package/dist/ltree/ltree.svelte.js +109 -34
  19. package/dist/ltree/types.d.ts +3 -0
  20. package/dist/perf-logger.d.ts +70 -0
  21. package/dist/perf-logger.js +196 -0
  22. package/dist/styles/main.scss +9 -0
  23. package/dist/styles.css +7 -0
  24. package/dist/styles.css.map +1 -1
  25. package/dist/vendor/loglevel/index.d.ts +2 -0
  26. package/dist/vendor/loglevel/index.js +9 -0
  27. package/dist/vendor/loglevel/loglevel-esm.d.ts +2 -0
  28. package/dist/vendor/loglevel/loglevel-esm.js +349 -0
  29. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +7 -0
  30. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.js +132 -0
  31. package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +2 -0
  32. package/dist/vendor/loglevel/loglevel-plugin-prefix.js +149 -0
  33. package/dist/vendor/loglevel/loglevel.js +357 -0
  34. package/dist/vendor/loglevel/prefix.d.ts +2 -0
  35. package/dist/vendor/loglevel/prefix.js +9 -0
  36. 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**: Uses LTree data structure for efficient hierarchical data management
12
- - **Drag & Drop**: Built-in drag and drop support with validation and visual feedback
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
- const sourceData = [
220
- { path: '1', name: 'Item 1', isDraggable: true },
221
- { path: '2', name: 'Item 2', isDraggable: true }
222
- ];
223
-
224
- const targetData = [
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
- function onDrop(dropNode, draggedNode, event) {
234
- console.log(`Dropped ${draggedNode.data.name} onto ${dropNode.data.name}`);
235
- // Handle the drop logic here
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
- <div class="row">
240
- <div class="col-6">
241
- <Tree
242
- data={sourceData}
243
- idMember="path"
244
- pathMember="path"
245
- onNodeDragStart={onDragStart}
246
- />
247
- </div>
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
- | `onNodeDrop` | `(dropNode, draggedNode, event) => void` | `undefined` | Drop event handler |
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
- #### onNodeDrop(dropNode, draggedNode, event)
724
- Triggered when a node is dropped onto another node.
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, ensuring results match visible tree structure
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.