@keenmate/svelte-treeview 4.5.0 → 4.7.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 CHANGED
@@ -2,14 +2,61 @@
2
2
 
3
3
  A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
4
4
 
5
- > [!TIP]
6
- > **v4.5.0 Performance Boost** - Optimized tree building algorithm now loads 17,000+ nodes in under 100ms (previously 85+ seconds). See [Performance](#-performance) for details.
5
+ ## šŸ“¢ New in v4.7: Per-Node Drop Position Restrictions
6
+
7
+ > [!NOTE]
8
+ > **You can now restrict which drop positions (above/below/child) are allowed per node.**
9
+
10
+ Use `getAllowedDropPositionsCallback` for dynamic logic or `allowedDropPositionsMember` for server data:
11
+ ```typescript
12
+ // Files can only have siblings, trash only accepts children
13
+ function getAllowedDropPositions(node) {
14
+ if (node.data?.type === 'file') return ['above', 'below'];
15
+ if (node.data?.type === 'trash') return ['child'];
16
+ return undefined; // all positions allowed (default)
17
+ }
18
+ ```
19
+
20
+ ## šŸ“¢ v4.6: Progressive Flat Rendering
21
+
22
+ > [!NOTE]
23
+ > **The tree now uses progressive flat rendering by default for significantly improved performance.**
24
+
25
+ **What this means:**
26
+ - The tree renders immediately with the first batch of nodes (~20 by default)
27
+ - Remaining nodes are rendered progressively in subsequent frames
28
+ - For large trees (5000+ nodes), you'll see nodes appear over ~100-500ms instead of a single long freeze
29
+ - The UI remains responsive during rendering
30
+
31
+ **Configuration options:**
32
+ ```svelte
33
+ <Tree
34
+ {data}
35
+ useFlatRendering={true} <!-- Default: true (flat mode) -->
36
+ progressiveRender={true} <!-- Default: true (batched rendering) -->
37
+ initialBatchSize={20} <!-- First batch size (default: 20) -->
38
+ maxBatchSize={500} <!-- Maximum batch size cap (default: 500) -->
39
+ />
40
+ ```
41
+
42
+ **Exponential batching:** The first batch renders 20 nodes instantly, then doubles each frame (20 → 40 → 80 → 160 → 320 → 500...) for optimal perceived performance.
43
+
44
+ **To use the legacy recursive rendering:**
45
+ ```svelte
46
+ <Tree
47
+ {data}
48
+ useFlatRendering={false} <!-- Uses recursive Node components -->
49
+ />
50
+ ```
51
+
52
+ Recursive mode may be preferred for very small trees or when you need the `{#key changeTracker}` behavior that recreates all nodes on any change.
7
53
 
8
54
  ## šŸš€ Features
9
55
 
10
56
  - **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
57
+ - **High Performance**: Flat rendering mode with progressive loading for 5000+ nodes
58
+ - **Drag & Drop**: Built-in drag and drop with position control (above/below/child), touch support, and async validation
59
+ - **Tree Editing**: Built-in methods for add, move, remove operations with automatic path management
13
60
  - **Search & Filter**: Integrated FlexSearch for fast, full-text search capabilities
14
61
  - **Flexible Data Sources**: Works with any hierarchical data structure
15
62
  - **Context Menus**: Dynamic right-click menus with callback-based generation, icons, disabled states
@@ -237,49 +284,163 @@ For complete FlexSearch documentation, visit: [FlexSearch Options](https://githu
237
284
  ```svelte
238
285
  <script lang="ts">
239
286
  import { Tree } from '@keenmate/svelte-treeview';
240
-
241
- const sourceData = [
242
- { path: '1', name: 'Item 1', isDraggable: true },
243
- { path: '2', name: 'Item 2', isDraggable: true }
244
- ];
245
-
246
- const targetData = [
247
- { path: 'zone1', name: 'Drop Zone 1' },
248
- { path: 'zone2', name: 'Drop Zone 2' }
287
+
288
+ let treeRef: Tree<MyNode>;
289
+
290
+ const data = [
291
+ { path: '1', name: 'Folder 1', isDraggable: true },
292
+ { path: '1.1', name: 'Item 1', isDraggable: true },
293
+ { path: '2', name: 'Folder 2', isDraggable: true }
249
294
  ];
250
-
295
+
251
296
  function onDragStart(node, event) {
252
297
  console.log('Dragging:', node.data.name);
253
298
  }
254
-
255
- function onDrop(dropNode, draggedNode, event) {
256
- console.log(`Dropped ${draggedNode.data.name} onto ${dropNode.data.name}`);
257
- // Handle the drop logic here
299
+
300
+ // Same-tree moves are auto-handled - this callback is for notification/custom logic
301
+ function onDrop(dropNode, draggedNode, position, event, operation) {
302
+ console.log(`Dropped ${draggedNode.data.name} ${position} ${dropNode?.data.name}`);
303
+ // position is 'above', 'below', or 'child'
304
+ // operation is 'move' or 'copy' (Ctrl+drag)
258
305
  }
259
306
  </script>
260
307
 
261
- <div class="row">
262
- <div class="col-6">
263
- <Tree
264
- data={sourceData}
265
- idMember="path"
266
- pathMember="path"
267
- onNodeDragStart={onDragStart}
268
- />
269
- </div>
270
-
271
- <div class="col-6">
272
- <Tree
273
- data={targetData}
274
- idMember="path"
275
- pathMember="path"
276
- dragOverNodeClass="ltree-dragover-highlight"
277
- onNodeDrop={onDrop}
278
- />
279
- </div>
280
- </div>
308
+ <Tree
309
+ bind:this={treeRef}
310
+ {data}
311
+ idMember="path"
312
+ pathMember="path"
313
+ orderMember="sortOrder"
314
+ dragOverNodeClass="ltree-dragover-highlight"
315
+ onNodeDragStart={onDragStart}
316
+ onNodeDrop={onDrop}
317
+ />
318
+ ```
319
+
320
+ #### Drop Position Control
321
+
322
+ When using `dropZoneMode="floating"` (default), users can choose where to drop:
323
+ - **Above**: Insert as sibling before the target node
324
+ - **Below**: Insert as sibling after the target node
325
+ - **Child**: Insert as child of the target node
326
+
327
+ #### Per-Node Drop Position Restrictions
328
+
329
+ You can restrict which drop positions are allowed per node. This is useful for:
330
+ - **Trash/Recycle Bin**: Only allow dropping INTO (child), not above/below
331
+ - **Files**: Only allow above/below (can't drop INTO a file)
332
+ - **Folders**: Allow all positions (default)
333
+
334
+ ```svelte
335
+ <script lang="ts">
336
+ import { Tree, type DropPosition, type LTreeNode } from '@keenmate/svelte-treeview';
337
+
338
+ // Dynamic callback approach
339
+ function getAllowedDropPositions(node: LTreeNode<MyItem>): DropPosition[] | null {
340
+ if (node.data?.type === 'file') return ['above', 'below'];
341
+ if (node.data?.type === 'trash') return ['child'];
342
+ return undefined; // all positions allowed
343
+ }
344
+ </script>
345
+
346
+ <Tree
347
+ {data}
348
+ getAllowedDropPositionsCallback={getAllowedDropPositions}
349
+ />
281
350
  ```
282
351
 
352
+ Or use the member approach for server-side data:
353
+ ```svelte
354
+ <Tree
355
+ {data}
356
+ allowedDropPositionsMember="allowedDropPositions"
357
+ />
358
+
359
+ <!-- Where data items have: { allowedDropPositions: ['child'] } -->
360
+ ```
361
+
362
+ When restrictions are applied:
363
+ - **Glow mode**: Snaps to the nearest allowed position
364
+ - **Floating mode**: Only renders buttons for allowed positions
365
+
366
+ #### Async Drop Validation
367
+
368
+ Use `beforeDropCallback` to validate or modify drops, including async operations like confirmation dialogs:
369
+
370
+ ```svelte
371
+ <script lang="ts">
372
+ async function beforeDrop(dropNode, draggedNode, position, event, operation) {
373
+ // Cancel specific drops
374
+ if (draggedNode.data.locked) {
375
+ return false; // Cancel the drop
376
+ }
377
+
378
+ // Show confirmation dialog (async)
379
+ if (position === 'child' && !dropNode.data.isFolder) {
380
+ const confirmed = await showConfirmDialog('Drop as sibling instead?');
381
+ if (!confirmed) return false;
382
+ return { position: 'below' }; // Override position
383
+ }
384
+
385
+ // Proceed normally
386
+ return true;
387
+ }
388
+ </script>
389
+
390
+ <Tree
391
+ {data}
392
+ beforeDropCallback={beforeDrop}
393
+ onNodeDrop={onDrop}
394
+ />
395
+ ```
396
+
397
+ ### Tree Editing
398
+
399
+ The tree provides built-in methods for programmatic editing:
400
+
401
+ ```svelte
402
+ <script lang="ts">
403
+ import { Tree } from '@keenmate/svelte-treeview';
404
+
405
+ let treeRef: Tree<MyNode>;
406
+
407
+ // Add a new node
408
+ function addChild() {
409
+ const result = treeRef.addNode(
410
+ selectedNode?.path || '', // parent path (empty = root)
411
+ { id: Date.now(), path: '', name: 'New Item', sortOrder: 100 }
412
+ );
413
+ if (result.success) {
414
+ console.log('Added:', result.node);
415
+ }
416
+ }
417
+
418
+ // Move a node
419
+ function moveUp() {
420
+ const siblings = treeRef.getSiblings(selectedNode.path);
421
+ const index = siblings.findIndex(s => s.path === selectedNode.path);
422
+ if (index > 0) {
423
+ treeRef.moveNode(selectedNode.path, siblings[index - 1].path, 'above');
424
+ }
425
+ }
426
+
427
+ // Remove a node
428
+ function remove() {
429
+ treeRef.removeNode(selectedNode.path);
430
+ }
431
+ </script>
432
+
433
+ <Tree
434
+ bind:this={treeRef}
435
+ {data}
436
+ idMember="id"
437
+ pathMember="path"
438
+ orderMember="sortOrder"
439
+ />
440
+ ```
441
+
442
+ **Note**: When using `orderMember`, the tree automatically calculates sort order values when moving nodes with 'above' or 'below' positions.
443
+
283
444
  ### With Context Menus
284
445
 
285
446
  The tree supports context menus with two approaches: callback-based (recommended) and snippet-based.
@@ -492,6 +653,7 @@ The component includes several pre-built classes for styling selected nodes:
492
653
  | `isSelectedMember` | `string \| null` | `null` | Property name for selected state |
493
654
  | `isDraggableMember` | `string \| null` | `null` | Property name for draggable state |
494
655
  | `isDropAllowedMember` | `string \| null` | `null` | Property name for drop allowed state |
656
+ | `allowedDropPositionsMember` | `string \| null` | `null` | Property name for allowed drop positions array |
495
657
  | `hasChildrenMember` | `string \| null` | `null` | Property name for children existence |
496
658
  | `isSorted` | `boolean \| null` | `null` | Whether items should be sorted |
497
659
 
@@ -531,6 +693,10 @@ Without both requirements, no search indexing will occur.
531
693
  |------|------|---------|-------------|
532
694
  | `expandLevel` | `number \| null` | `2` | Automatically expand nodes up to this level |
533
695
  | `shouldToggleOnNodeClick` | `boolean` | `true` | Toggle expansion on node click |
696
+ | `useFlatRendering` | `boolean` | `true` | Use flat rendering mode (faster for large trees) |
697
+ | `progressiveRender` | `boolean` | `true` | Progressively render nodes in batches |
698
+ | `renderBatchSize` | `number` | `50` | Number of nodes to render per batch |
699
+ | `orderMember` | `string \| null` | `null` | Property name for sort order (enables above/below positioning in drag-drop) |
534
700
  | `indexerBatchSize` | `number \| null` | `25` | Number of nodes to process per batch during search indexing |
535
701
  | `indexerTimeout` | `number \| null` | `50` | Maximum time (ms) to wait for idle callback before forcing indexing |
536
702
  | `shouldDisplayDebugInformation` | `boolean` | `false` | Show debug information panel with tree statistics and enable console debug logging for tree operations and async search indexing |
@@ -542,7 +708,9 @@ Without both requirements, no search indexing will occur.
542
708
  | `onNodeClicked` | `(node) => void` | `undefined` | Node click event handler |
543
709
  | `onNodeDragStart` | `(node, event) => void` | `undefined` | Drag start event handler |
544
710
  | `onNodeDragOver` | `(node, event) => void` | `undefined` | Drag over event handler |
545
- | `onNodeDrop` | `(dropNode, draggedNode, event) => void` | `undefined` | Drop event handler |
711
+ | `getAllowedDropPositionsCallback` | `(node) => DropPosition[] \| null` | `undefined` | Callback returning allowed drop positions per node |
712
+ | `beforeDropCallback` | `(dropNode, draggedNode, position, event, operation) => boolean \| { position?, operation? } \| Promise<...>` | `undefined` | Async-capable callback to validate/modify drops before they happen |
713
+ | `onNodeDrop` | `(dropNode, draggedNode, position, event, operation) => void` | `undefined` | Drop event handler. Position is 'above', 'below', or 'child'. Operation is 'move' or 'copy' |
546
714
 
547
715
  #### Visual Styling Properties
548
716
  | Prop | Type | Default | Description |
@@ -577,6 +745,12 @@ Without both requirements, no search indexing will occur.
577
745
  | `searchNodes` | `searchText: string \| null \| undefined, searchOptions?: SearchOptions` | Search nodes using internal search index and return matching nodes with optional FlexSearch options |
578
746
  | `scrollToPath` | `path: string, options?: ScrollToPathOptions` | Scroll to and highlight a specific node |
579
747
  | `update` | `updates: Partial<Props>` | Programmatically update component props from external JavaScript |
748
+ | `addNode` | `parentPath: string, data: T, pathSegment?: string` | Add a new node under the specified parent |
749
+ | `moveNode` | `sourcePath: string, targetPath: string, position: 'above' \| 'below' \| 'child'` | Move a node to a new location |
750
+ | `removeNode` | `path: string, includeDescendants?: boolean` | Remove a node (and optionally its descendants) |
751
+ | `getNodeByPath` | `path: string` | Get a node by its path |
752
+ | `getChildren` | `parentPath: string` | Get direct children of a node |
753
+ | `getSiblings` | `path: string` | Get siblings of a node (including itself) |
580
754
 
581
755
  #### ScrollToPath Options
582
756
 
@@ -742,8 +916,17 @@ Triggered when drag operation starts.
742
916
  #### onNodeDragOver(node, event)
743
917
  Triggered when dragging over a potential drop target.
744
918
 
745
- #### onNodeDrop(dropNode, draggedNode, event)
746
- Triggered when a node is dropped onto another node.
919
+ #### beforeDropCallback(dropNode, draggedNode, position, event, operation)
920
+ Called before a drop is processed. Can be async for showing dialogs.
921
+ - Return `false` to cancel the drop
922
+ - Return `{ position: 'above'|'below'|'child' }` to override position
923
+ - Return `{ operation: 'move'|'copy' }` to override operation
924
+ - Return `true` or `undefined` to proceed normally
925
+
926
+ #### onNodeDrop(dropNode, draggedNode, position, event, operation)
927
+ Triggered when a node is dropped. For same-tree moves, the tree auto-handles the move and this callback is for notification.
928
+ - `position`: 'above', 'below', or 'child'
929
+ - `operation`: 'move' or 'copy' (Ctrl+drag)
747
930
 
748
931
  ### Slots
749
932
 
@@ -917,15 +1100,34 @@ interface InsertArrayResult<T> {
917
1100
 
918
1101
  The component is optimized for large datasets:
919
1102
 
1103
+ - **Flat Rendering Mode**: Single `{#each}` loop instead of recursive components (default, ~12x faster initial render)
1104
+ - **Progressive Rendering**: Batched rendering prevents UI freeze during initial load
1105
+ - **Context-Based Callbacks**: Stable function references eliminate unnecessary re-renders
920
1106
  - **LTree**: Efficient hierarchical data structure
921
1107
  - **Async Search Indexing**: Uses `requestIdleCallback` for non-blocking search index building
922
- - **Accurate Search Results**: Search index only includes successfully inserted nodes, ensuring results match visible tree structure
923
- - **Consistent Visual Hierarchy**: Optimized CSS-based indentation prevents exponential spacing growth
1108
+ - **Accurate Search Results**: Search index only includes successfully inserted nodes
924
1109
  - **Search Indexing**: Uses FlexSearch for fast search operations
925
1110
 
926
- ### v4.5 Performance Improvements
1111
+ ### Performance Benchmarks (5500 nodes)
1112
+
1113
+ | Operation | Time |
1114
+ |-----------|------|
1115
+ | Initial render | ~25ms |
1116
+ | Expand/collapse | ~100-150ms |
1117
+ | Search filtering | <50ms |
1118
+
1119
+ ### v4.5+ Performance Improvements
1120
+
1121
+ **Flat Rendering Mode** (default) - Renders all visible nodes in a single loop:
1122
+ ```svelte
1123
+ <Tree
1124
+ {data}
1125
+ useFlatRendering={true}
1126
+ progressiveRender={true}
1127
+ />
1128
+ ```
927
1129
 
928
- **Optimized `insertArray` algorithm** - Fixed O(n²) bottleneck that caused 85+ second load times with large datasets. Now loads 17,000+ nodes in under 100ms.
1130
+ **Optimized `insertArray` algorithm** - Fixed O(n²) bottleneck. Now loads 17,000+ nodes in under 100ms.
929
1131
 
930
1132
  **Performance Logging** - Built-in performance measurement for debugging:
931
1133
  ```typescript