@keenmate/svelte-treeview 4.5.0 → 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 CHANGED
@@ -2,14 +2,15 @@
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
+ > [!IMPORTANT]
6
+ > **Looking for a framework-agnostic solution?** There's also a web component version that can be used standalone or in other frameworks at https://github.com/KeenMate/web-treeview/
7
7
 
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
@@ -82,6 +83,40 @@ let treeData = $state.raw<TreeNode[]>([])
82
83
 
83
84
  The array itself remains reactive - only individual items lose deep reactivity (which Tree doesn't need).
84
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
+
85
120
  ## 🎯 Quick Start
86
121
 
87
122
  ```svelte
@@ -237,49 +272,124 @@ For complete FlexSearch documentation, visit: [FlexSearch Options](https://githu
237
272
  ```svelte
238
273
  <script lang="ts">
239
274
  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' }
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 }
249
282
  ];
250
-
283
+
251
284
  function onDragStart(node, event) {
252
285
  console.log('Dragging:', node.data.name);
253
286
  }
254
-
255
- function onDrop(dropNode, draggedNode, event) {
256
- console.log(`Dropped ${draggedNode.data.name} onto ${dropNode.data.name}`);
257
- // 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)
258
293
  }
259
294
  </script>
260
295
 
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>
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
+ />
281
306
  ```
282
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
+
283
393
  ### With Context Menus
284
394
 
285
395
  The tree supports context menus with two approaches: callback-based (recommended) and snippet-based.
@@ -531,6 +641,10 @@ Without both requirements, no search indexing will occur.
531
641
  |------|------|---------|-------------|
532
642
  | `expandLevel` | `number \| null` | `2` | Automatically expand nodes up to this level |
533
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) |
534
648
  | `indexerBatchSize` | `number \| null` | `25` | Number of nodes to process per batch during search indexing |
535
649
  | `indexerTimeout` | `number \| null` | `50` | Maximum time (ms) to wait for idle callback before forcing indexing |
536
650
  | `shouldDisplayDebugInformation` | `boolean` | `false` | Show debug information panel with tree statistics and enable console debug logging for tree operations and async search indexing |
@@ -542,7 +656,8 @@ Without both requirements, no search indexing will occur.
542
656
  | `onNodeClicked` | `(node) => void` | `undefined` | Node click event handler |
543
657
  | `onNodeDragStart` | `(node, event) => void` | `undefined` | Drag start event handler |
544
658
  | `onNodeDragOver` | `(node, event) => void` | `undefined` | Drag over event handler |
545
- | `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' |
546
661
 
547
662
  #### Visual Styling Properties
548
663
  | Prop | Type | Default | Description |
@@ -577,6 +692,12 @@ Without both requirements, no search indexing will occur.
577
692
  | `searchNodes` | `searchText: string \| null \| undefined, searchOptions?: SearchOptions` | Search nodes using internal search index and return matching nodes with optional FlexSearch options |
578
693
  | `scrollToPath` | `path: string, options?: ScrollToPathOptions` | Scroll to and highlight a specific node |
579
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) |
580
701
 
581
702
  #### ScrollToPath Options
582
703
 
@@ -742,8 +863,17 @@ Triggered when drag operation starts.
742
863
  #### onNodeDragOver(node, event)
743
864
  Triggered when dragging over a potential drop target.
744
865
 
745
- #### onNodeDrop(dropNode, draggedNode, event)
746
- 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)
747
877
 
748
878
  ### Slots
749
879
 
@@ -917,15 +1047,34 @@ interface InsertArrayResult<T> {
917
1047
 
918
1048
  The component is optimized for large datasets:
919
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
920
1053
  - **LTree**: Efficient hierarchical data structure
921
1054
  - **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
1055
+ - **Accurate Search Results**: Search index only includes successfully inserted nodes
924
1056
  - **Search Indexing**: Uses FlexSearch for fast search operations
925
1057
 
926
- ### v4.5 Performance Improvements
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
+ ```
927
1076
 
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.
1077
+ **Optimized `insertArray` algorithm** - Fixed O(n²) bottleneck. Now loads 17,000+ nodes in under 100ms.
929
1078
 
930
1079
  **Performance Logging** - Built-in performance measurement for debugging:
931
1080
  ```typescript
@@ -4,102 +4,73 @@
4
4
  import {getContext, onDestroy, type Snippet} from "svelte"
5
5
  import type {Ltree, DropPosition, DropOperation} from "../ltree/types.js"
6
6
  import type {RenderCoordinator} from "./RenderCoordinator.svelte.js"
7
+ import type {NodeCallbacks, NodeConfig} from "./Tree.svelte"
7
8
  import { uiLogger } from "../logger.js"
8
9
 
9
10
  // Define component props interface
11
+ // Callbacks and config come from context, drag state comes as props
10
12
  interface Props {
11
13
  node: LTreeNode<T>;
12
14
  children?: Snippet<[T]>; // Keep the general children slot for backward compatibility
13
- onNodeClicked?: (node: LTreeNode<T>) => void;
14
- onNodeRightClicked?: (node: LTreeNode<T>, event: MouseEvent) => void;
15
- onNodeDragStart?: (node: LTreeNode<T>, event: DragEvent) => void;
16
- onNodeDragOver?: (node: LTreeNode<T>, event: DragEvent) => void;
17
- onNodeDragLeave?: (node: LTreeNode<T>, event: DragEvent) => void;
18
- onNodeDrop?: (node: LTreeNode<T>, event: DragEvent) => void;
19
- onZoneDrop?: (node: LTreeNode<T>, position: DropPosition, event: DragEvent) => void;
20
-
21
- // Touch drag handlers for mobile support
22
- onTouchDragStart?: (node: LTreeNode<T>, event: TouchEvent) => void;
23
- onTouchDragMove?: (node: LTreeNode<T>, event: TouchEvent) => void;
24
- onTouchDragEnd?: (node: LTreeNode<T>, event: TouchEvent) => void;
25
-
26
- // BEHAVIOUR
27
- shouldToggleOnNodeClick?: boolean | null | undefined;
28
15
 
29
16
  // Progressive rendering
30
17
  progressiveRender?: boolean;
31
18
  renderBatchSize?: number;
32
19
 
33
- // VISUALS
34
- expandIconClass?: string | null | undefined;
35
- collapseIconClass?: string | null | undefined;
36
- leafIconClass?: string | null | undefined;
37
- selectedNodeClass?: string | null | undefined;
38
- dragOverNodeClass?: string | null | undefined;
39
- isDraggedNode?: boolean | null | undefined;
40
-
41
- // Drag position indicators
20
+ // Drag state (passed as props for efficient Svelte diffing)
21
+ isDraggedNode?: boolean;
42
22
  isDragInProgress?: boolean;
43
- hoveredNodeForDropPath?: string | null; // Path of node being hovered for drop
23
+ hoveredNodeForDropPath?: string | null;
44
24
  activeDropPosition?: DropPosition | null;
25
+ dropOperation?: DropOperation;
45
26
 
46
- // Drop zone configuration
47
- dropZoneMode?: 'floating' | 'glow'; // 'floating' = original floating zones, 'glow' = border glow indicators
48
- dropZoneLayout?: 'around' | 'above' | 'below' | 'wave' | 'wave2';
49
- dropZoneStart?: number | string; // number = percentage (0-100), string = any CSS value ("33%", "50px", "3rem")
50
- dropZoneMaxWidth?: number; // max width in pixels for wave layouts
51
- dropOperation?: DropOperation; // Current drag operation ('move' or 'copy')
52
- allowCopy?: boolean; // Whether copy operation is allowed (Ctrl+drag)
27
+ // Flat rendering mode
28
+ flatMode?: boolean; // When true, don't render children (Tree handles flat rendering)
29
+ flatIndentSize?: string; // CSS value for per-level indentation in flat mode
53
30
  }
54
31
 
55
32
  // Destructure props using Svelte 5 syntax
56
33
  let {
57
34
  node,
58
35
  children = undefined,
59
- onNodeClicked,
60
- onNodeRightClicked,
61
- onNodeDragStart,
62
- onNodeDragOver,
63
- onNodeDragLeave,
64
- onNodeDrop,
65
- onZoneDrop,
66
-
67
- // Touch drag handlers for mobile support
68
- onTouchDragStart,
69
- onTouchDragMove,
70
- onTouchDragEnd,
71
-
72
- // BEHAVIOUR
73
- shouldToggleOnNodeClick = true,
74
36
 
75
37
  // Progressive rendering
76
38
  progressiveRender = false,
77
39
  renderBatchSize = 50,
78
40
 
79
- // VISUALS
80
- expandIconClass = "ltree-icon-expand",
81
- collapseIconClass = "ltree-icon-collapse",
82
- leafIconClass = "ltree-icon-leaf",
83
- selectedNodeClass,
84
- dragOverNodeClass,
41
+ // Drag state
85
42
  isDraggedNode = false,
86
-
87
- // Drag position indicators
88
43
  isDragInProgress = false,
89
44
  hoveredNodeForDropPath = null,
90
45
  activeDropPosition = null,
91
-
92
- // Drop zone configuration
93
- dropZoneMode = 'glow',
94
- dropZoneLayout = 'around',
95
- dropZoneStart = 33,
96
- dropZoneMaxWidth = 120,
97
46
  dropOperation = 'move',
98
- allowCopy = false,
47
+
48
+ // Flat rendering mode
49
+ flatMode = false,
50
+ flatIndentSize = '1.5rem',
99
51
  }: Props = $props()
100
52
 
53
+ // Get stable references from context (avoids prop drilling and re-renders from inline functions)
54
+ const callbacks = getContext<NodeCallbacks<T>>('NodeCallbacks');
55
+ const config = getContext<NodeConfig>('NodeConfig');
56
+
57
+ // Destructure config for convenience (these are stable references)
58
+ const {
59
+ shouldToggleOnNodeClick,
60
+ expandIconClass,
61
+ collapseIconClass,
62
+ leafIconClass,
63
+ selectedNodeClass,
64
+ dragOverNodeClass,
65
+ dropZoneMode,
66
+ dropZoneLayout,
67
+ dropZoneStart,
68
+ dropZoneMaxWidth,
69
+ allowCopy,
70
+ } = config;
71
+
101
72
  // Compute if THIS node is the one being hovered for drop
102
- const isHoveredForDrop = $derived(hoveredNodeForDropPath === node.path)
73
+ const isHoveredForDrop = $derived(hoveredNodeForDropPath === node.path);
103
74
 
104
75
  // Format dropZoneStart - number = percentage, string = as-is
105
76
  const formattedDropZoneStart = $derived(
@@ -139,32 +110,36 @@
139
110
  }
140
111
 
141
112
  // Convert reactive statements to derived values
142
- const childrenArray = $derived(Object.values(node?.children || []))
143
- const childrenWithData = $derived(Object.values(node?.children || []))
113
+ // In flat mode, children rendering is handled by Tree.svelte, so we skip these computations
114
+ const childrenArray = $derived(!flatMode ? Object.values(node?.children || []) : [])
144
115
  const hasChildren = $derived(node?.hasChildren || false)
145
116
  const indentStyle = $derived(
146
- `margin-left: var(--tree-node-indent-per-level, 0.5rem)`,
117
+ flatMode
118
+ ? `margin-left: calc(${(node?.level || 1) - 1} * ${flatIndentSize})`
119
+ : `margin-left: var(--tree-node-indent-per-level, 0.5rem)`,
147
120
  )
148
121
 
149
- // Progressive rendering state
122
+ // Progressive rendering state - only used in recursive mode
150
123
  let renderedCount = $state(0);
151
124
  let unregisterFromCoordinator: (() => void) | null = null;
152
125
  let lastExpandedState = false;
153
126
  let lastChildrenLength = 0;
154
127
 
155
- // Get the children to render (all or progressive slice)
128
+ // Get the children to render (all or progressive slice) - only used in recursive mode
156
129
  const childrenToRender = $derived(
157
- progressiveRender && renderCoordinator
130
+ !flatMode && progressiveRender && renderCoordinator
158
131
  ? childrenArray.slice(0, renderedCount)
159
132
  : childrenArray
160
133
  );
161
134
  const hasMoreToRender = $derived(
162
- progressiveRender && renderCoordinator && renderedCount < childrenArray.length
135
+ !flatMode && progressiveRender && renderCoordinator && renderedCount < childrenArray.length
163
136
  );
164
137
 
165
138
  // Handle expansion state changes - use coordinator for progressive rendering
166
- // Only react to isExpanded changes, not renderedCount changes
139
+ // Skip entirely in flat mode since Tree.svelte handles children rendering
167
140
  $effect(() => {
141
+ if (flatMode) return; // Skip in flat mode - children handled by Tree
142
+
168
143
  const isExpanded = node?.isExpanded ?? false;
169
144
  const childCount = childrenArray.length;
170
145
  const shouldRenderProgressively = progressiveRender && renderCoordinator && childCount > 0;
@@ -232,7 +207,7 @@
232
207
 
233
208
  function _onNodeClicked() {
234
209
  uiLogger.debug(`Node clicked: ${node.path}`, { id: node.id, hasChildren: node.hasChildren })
235
- onNodeClicked?.(node)
210
+ callbacks.onNodeClicked(node)
236
211
  if (shouldToggleOnNodeClick) {
237
212
  toggleExpanded()
238
213
  }
@@ -280,7 +255,7 @@
280
255
  }}
281
256
  oncontextmenu={(e) => {
282
257
  e.stopPropagation();
283
- onNodeRightClicked?.(node, e);
258
+ callbacks.onNodeRightClicked(node, e);
284
259
  }}
285
260
  ondragstart={(e) => {
286
261
  if (node?.isDraggable && e.dataTransfer) {
@@ -289,7 +264,7 @@
289
264
  "application/svelte-treeview",
290
265
  JSON.stringify(node),
291
266
  );
292
- onNodeDragStart?.(node, e);
267
+ callbacks.onNodeDragStart(node, e);
293
268
  }
294
269
  }}
295
270
  ondragover={(e) => {
@@ -305,7 +280,7 @@
305
280
  glowPosition = calculateGlowPosition(e, e.currentTarget as HTMLElement);
306
281
  }
307
282
  }
308
- onNodeDragOver?.(node, e);
283
+ callbacks.onNodeDragOver(node, e);
309
284
  }}
310
285
  ondragleave={(e) => {
311
286
  const rect = e.currentTarget.getBoundingClientRect();
@@ -315,7 +290,7 @@
315
290
  if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
316
291
  isDraggedOver = false;
317
292
  glowPosition = null;
318
- onNodeDragLeave?.(node, e);
293
+ callbacks.onNodeDragLeave(node, e);
319
294
  }
320
295
  }}
321
296
  ondrop={(e) => {
@@ -327,15 +302,15 @@
327
302
  isDraggedOver = false;
328
303
  // In glow mode, use the calculated glowPosition for the drop
329
304
  if (dropZoneMode === 'glow' && glowPosition) {
330
- onZoneDrop?.(node, glowPosition, e);
305
+ callbacks.onZoneDrop(node, glowPosition, e);
331
306
  } else {
332
- onNodeDrop?.(node, e);
307
+ callbacks.onNodeDrop(node, e);
333
308
  }
334
309
  glowPosition = null;
335
310
  }}
336
- ontouchstart={(e) => onTouchDragStart?.(node, e)}
337
- ontouchmove={(e) => onTouchDragMove?.(node, e)}
338
- ontouchend={(e) => onTouchDragEnd?.(node, e)}
311
+ ontouchstart={(e) => callbacks.onTouchDragStart(node, e)}
312
+ ontouchmove={(e) => callbacks.onTouchDragMove(node, e)}
313
+ ontouchend={(e) => callbacks.onTouchDragEnd(node, e)}
339
314
  >
340
315
  {#if children}
341
316
  {@render children(node)}
@@ -354,62 +329,42 @@
354
329
  <div
355
330
  class="ltree-drop-zone ltree-drop-above"
356
331
  class:ltree-drop-zone-active={hoveredZone === 'above'}
357
- ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'above'; onNodeDragOver?.(node, e); }}
332
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'above'; callbacks.onNodeDragOver(node, e); }}
358
333
  ondragleave={() => { hoveredZone = null; }}
359
- ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; onZoneDrop?.(node, 'above', e); }}
334
+ ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'above', e); }}
360
335
  >↑ Above</div>
361
336
  <div
362
337
  class="ltree-drop-zone ltree-drop-below"
363
338
  class:ltree-drop-zone-active={hoveredZone === 'below'}
364
- ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'below'; onNodeDragOver?.(node, e); }}
339
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'below'; callbacks.onNodeDragOver(node, e); }}
365
340
  ondragleave={() => { hoveredZone = null; }}
366
- ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; onZoneDrop?.(node, 'below', e); }}
341
+ ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'below', e); }}
367
342
  >↓ Below</div>
368
343
  <div
369
344
  class="ltree-drop-zone ltree-drop-child"
370
345
  class:ltree-drop-zone-active={hoveredZone === 'child'}
371
- ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'child'; onNodeDragOver?.(node, e); }}
346
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'child'; callbacks.onNodeDragOver(node, e); }}
372
347
  ondragleave={() => { hoveredZone = null; }}
373
- ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; onZoneDrop?.(node, 'child', e); }}
348
+ ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'child', e); }}
374
349
  >→ Child</div>
375
350
  </div>
376
351
  {/if}
377
352
  </div>
378
353
 
379
- {#if node?.isExpanded && node?.hasChildren}
354
+ <!-- In flat mode, children are rendered by Tree.svelte, not recursively here -->
355
+ {#if !flatMode && node?.isExpanded && node?.hasChildren}
380
356
  <div class="ltree-children">
381
357
  {#each childrenToRender as item (item.id)}
382
358
  <Node
383
359
  node={item}
384
360
  {children}
385
- {shouldToggleOnNodeClick}
386
- {onNodeClicked}
387
- {onNodeRightClicked}
388
- {onNodeDragStart}
389
- {onNodeDragOver}
390
- {onNodeDragLeave}
391
- {onNodeDrop}
392
- {onZoneDrop}
393
- {onTouchDragStart}
394
- {onTouchDragMove}
395
- {onTouchDragEnd}
396
361
  {progressiveRender}
397
362
  {renderBatchSize}
398
- {expandIconClass}
399
- {collapseIconClass}
400
- {leafIconClass}
401
- {selectedNodeClass}
402
- {dragOverNodeClass}
403
363
  {isDraggedNode}
404
364
  {isDragInProgress}
405
365
  {hoveredNodeForDropPath}
406
366
  {activeDropPosition}
407
- {dropZoneMode}
408
- {dropZoneLayout}
409
- {dropZoneStart}
410
- {dropZoneMaxWidth}
411
367
  {dropOperation}
412
- {allowCopy}
413
368
  />
414
369
  {/each}
415
370
  {#if hasMoreToRender}
@@ -6,34 +6,15 @@ declare function $$render<T>(): {
6
6
  props: {
7
7
  node: LTreeNode<T>;
8
8
  children?: Snippet<[T]>;
9
- onNodeClicked?: (node: LTreeNode<T>) => void;
10
- onNodeRightClicked?: (node: LTreeNode<T>, event: MouseEvent) => void;
11
- onNodeDragStart?: (node: LTreeNode<T>, event: DragEvent) => void;
12
- onNodeDragOver?: (node: LTreeNode<T>, event: DragEvent) => void;
13
- onNodeDragLeave?: (node: LTreeNode<T>, event: DragEvent) => void;
14
- onNodeDrop?: (node: LTreeNode<T>, event: DragEvent) => void;
15
- onZoneDrop?: (node: LTreeNode<T>, position: DropPosition, event: DragEvent) => void;
16
- onTouchDragStart?: (node: LTreeNode<T>, event: TouchEvent) => void;
17
- onTouchDragMove?: (node: LTreeNode<T>, event: TouchEvent) => void;
18
- onTouchDragEnd?: (node: LTreeNode<T>, event: TouchEvent) => void;
19
- shouldToggleOnNodeClick?: boolean | null | undefined;
20
9
  progressiveRender?: boolean;
21
10
  renderBatchSize?: number;
22
- expandIconClass?: string | null | undefined;
23
- collapseIconClass?: string | null | undefined;
24
- leafIconClass?: string | null | undefined;
25
- selectedNodeClass?: string | null | undefined;
26
- dragOverNodeClass?: string | null | undefined;
27
- isDraggedNode?: boolean | null | undefined;
11
+ isDraggedNode?: boolean;
28
12
  isDragInProgress?: boolean;
29
13
  hoveredNodeForDropPath?: string | null;
30
14
  activeDropPosition?: DropPosition | null;
31
- dropZoneMode?: "floating" | "glow";
32
- dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
33
- dropZoneStart?: number | string;
34
- dropZoneMaxWidth?: number;
35
15
  dropOperation?: DropOperation;
36
- allowCopy?: boolean;
16
+ flatMode?: boolean;
17
+ flatIndentSize?: string;
37
18
  };
38
19
  exports: {};
39
20
  bindings: "";