@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.
@@ -7,6 +7,36 @@
7
7
  import { setContext, tick } from 'svelte';
8
8
  import { createRenderCoordinator, type RenderCoordinator, type RenderStats } from './RenderCoordinator.svelte.js';
9
9
  import { uiLogger, dragLogger } from '../logger.js';
10
+ import { perfStart, perfEnd } from '../perf-logger.js';
11
+
12
+ // Context types for stable function references (prevents re-renders from inline arrow functions)
13
+ export interface NodeCallbacks<T> {
14
+ onNodeClicked: (node: LTreeNode<T>) => void;
15
+ onNodeRightClicked: (node: LTreeNode<T>, event: MouseEvent) => void;
16
+ onNodeDragStart: (node: LTreeNode<T>, event: DragEvent) => void;
17
+ onNodeDragOver: (node: LTreeNode<T>, event: DragEvent) => void;
18
+ onNodeDragLeave: (node: LTreeNode<T>, event: DragEvent) => void;
19
+ onNodeDrop: (node: LTreeNode<T>, event: DragEvent) => void;
20
+ onZoneDrop: (node: LTreeNode<T>, position: DropPosition, event: DragEvent) => void;
21
+ onTouchDragStart: (node: LTreeNode<T>, event: TouchEvent) => void;
22
+ onTouchDragMove: (node: LTreeNode<T>, event: TouchEvent) => void;
23
+ onTouchDragEnd: (node: LTreeNode<T>, event: TouchEvent) => void;
24
+ }
25
+
26
+ export interface NodeConfig {
27
+ shouldToggleOnNodeClick: boolean;
28
+ expandIconClass: string;
29
+ collapseIconClass: string;
30
+ leafIconClass: string;
31
+ selectedNodeClass: string | null | undefined;
32
+ dragOverNodeClass: string | null | undefined;
33
+ dropZoneMode: 'floating' | 'glow';
34
+ dropZoneLayout: 'around' | 'above' | 'below' | 'wave' | 'wave2';
35
+ dropZoneStart: number | string;
36
+ dropZoneMaxWidth: number;
37
+ allowCopy: boolean;
38
+ }
39
+
10
40
 
11
41
  // Register global API for runtime logging control
12
42
  import '../global-api.js';
@@ -17,6 +47,9 @@
17
47
  let contextMenuY = $state(0);
18
48
  let contextMenuNode: LTreeNode<T> | null = $state(null);
19
49
 
50
+ // Scroll highlight state - track current highlight to clear on next scroll
51
+ let currentHighlight: { element: HTMLElement; timeoutId: ReturnType<typeof setTimeout> } | null = null;
52
+
20
53
  // Drag and drop state
21
54
  let draggedNode: LTreeNode<any> | null = $state.raw(null);
22
55
 
@@ -32,6 +65,13 @@
32
65
 
33
66
  let touchTimer: ReturnType<typeof setTimeout> | null = null;
34
67
 
68
+ // Progressive rendering for flat mode
69
+ // Track which node IDs we've rendered and progressively add new ones
70
+ let flatRenderedIds = $state<Set<string>>(new Set());
71
+ let flatRenderQueue = $state<string[]>([]);
72
+ let flatRenderAnimationFrame: number | null = null;
73
+ let currentBatchSize: number = 0; // Exponential: doubles each batch up to maxBatchSize
74
+
35
75
  // Drop placeholder state for empty trees
36
76
  let isDropPlaceholderActive = $state(false);
37
77
 
@@ -99,12 +139,24 @@
99
139
 
100
140
  // Progressive rendering - render children in batches to avoid UI freeze
101
141
  progressiveRender?: boolean;
102
- renderBatchSize?: number;
142
+ initialBatchSize?: number;
143
+ maxBatchSize?: number;
103
144
  isRendering?: boolean; // Bindable: true while progressive rendering is active
104
145
  onRenderStart?: () => void;
105
146
  onRenderProgress?: (stats: RenderStats) => void;
106
147
  onRenderComplete?: (stats: RenderStats) => void;
107
148
 
149
+ /**
150
+ * Use flat/centralized rendering instead of recursive node rendering.
151
+ * This significantly improves performance for large trees by:
152
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
153
+ * - Using a single flat loop instead of recursive component instantiation
154
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
155
+ */
156
+ useFlatRendering?: boolean;
157
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
158
+ flatIndentSize?: string;
159
+
108
160
  // DRAG AND DROP
109
161
  dragDropMode?: DragDropMode;
110
162
  dropZoneMode?: 'floating' | 'glow'; // 'floating' = original floating zones, 'glow' = border glow indicators
@@ -122,8 +174,9 @@
122
174
  * Called before a drop is processed. Return false to cancel the drop.
123
175
  * Return { position, operation } to override the drop position or operation.
124
176
  * Return true or undefined to proceed normally.
177
+ * Can be async - return a Promise to show dialogs or perform async validation.
125
178
  */
126
- beforeDropCallback?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | { position?: DropPosition; operation?: DropOperation } | void;
179
+ beforeDropCallback?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | { position?: DropPosition; operation?: DropOperation } | void | Promise<boolean | { position?: DropPosition; operation?: DropOperation } | void>;
127
180
  onNodeDrop?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void;
128
181
  contextMenuCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[];
129
182
 
@@ -191,14 +244,19 @@
191
244
  shouldDisplayContextMenuInDebugMode = false,
192
245
  isLoading = false,
193
246
 
194
- // Progressive rendering
195
- progressiveRender = false,
196
- renderBatchSize = 50,
247
+ // Progressive rendering (exponential batching: 20 → 40 → 80 → 160...)
248
+ progressiveRender = true,
249
+ initialBatchSize = 20,
250
+ maxBatchSize = 500,
197
251
  isRendering = $bindable(false),
198
252
  onRenderStart,
199
253
  onRenderProgress,
200
254
  onRenderComplete,
201
255
 
256
+ // Flat rendering mode
257
+ useFlatRendering = true,
258
+ flatIndentSize = '1.5rem',
259
+
202
260
  // DRAG AND DROP
203
261
  dragDropMode = 'both',
204
262
  dropZoneMode = 'glow',
@@ -351,15 +409,20 @@
351
409
  export async function scrollToPath(
352
410
  path: string,
353
411
  options?: {
412
+ /** Expand ancestors to make the node visible (default: true) */
354
413
  expand?: boolean;
414
+ /** Also expand the target node itself to show its children (default: false for performance) */
415
+ expandTarget?: boolean;
355
416
  highlight?: boolean;
356
417
  scrollOptions?: ScrollIntoViewOptions;
357
418
  /** Scroll only within the nearest scrollable container (prevents page scroll) */
358
419
  containerScroll?: boolean;
359
420
  }
360
421
  ): Promise<boolean> {
422
+ perfStart(`[${treeId}] scrollToPath`);
361
423
  const {
362
424
  expand = true,
425
+ expandTarget = false,
363
426
  highlight = true,
364
427
  scrollOptions = { behavior: 'smooth', block: 'center' },
365
428
  containerScroll = false
@@ -369,13 +432,22 @@
369
432
  const node = tree.getNodeByPath(path);
370
433
  if (!node || !node.id) {
371
434
  console.warn(`[Tree ${treeId}] Node not found for path: ${path}`);
435
+ perfEnd(`[${treeId}] scrollToPath`);
372
436
  return false;
373
437
  }
374
438
 
375
- // Expand the path if requested
376
- if (expand) {
439
+ // Expand ancestors to make the node visible
440
+ // The target node's visibility depends on its parent being expanded, not on its own isExpanded state
441
+ if (expand && node.parentPath) {
442
+ tree.expandNodes(node.parentPath);
443
+ }
444
+
445
+ // Optionally expand the target node itself (shows its children, but triggers re-render if not already expanded)
446
+ if (expandTarget) {
377
447
  tree.expandNodes(path);
378
- tree.refresh();
448
+ }
449
+
450
+ if (expand || expandTarget) {
379
451
  await tick();
380
452
  }
381
453
 
@@ -386,6 +458,7 @@
386
458
 
387
459
  if (!contentDiv) {
388
460
  console.warn(`[Tree ${treeId}] DOM element not found for node ID: ${elementId}`);
461
+ perfEnd(`[${treeId}] scrollToPath`);
389
462
  return false;
390
463
  }
391
464
 
@@ -408,12 +481,23 @@
408
481
 
409
482
  // Highlight the node temporarily if requested
410
483
  if (highlight && scrollHighlightClass) {
484
+ // Clear previous highlight immediately (for rapid next/prev navigation)
485
+ if (currentHighlight) {
486
+ currentHighlight.element.classList.remove(scrollHighlightClass);
487
+ clearTimeout(currentHighlight.timeoutId);
488
+ currentHighlight = null;
489
+ }
490
+
411
491
  contentDiv.classList.add(scrollHighlightClass);
412
- setTimeout(() => {
492
+ const timeoutId = setTimeout(() => {
413
493
  contentDiv.classList.remove(scrollHighlightClass);
494
+ currentHighlight = null;
414
495
  }, scrollHighlightTimeout);
496
+
497
+ currentHighlight = { element: contentDiv, timeoutId };
415
498
  }
416
499
 
500
+ perfEnd(`[${treeId}] scrollToPath`);
417
501
  return true;
418
502
  }
419
503
 
@@ -581,8 +665,26 @@
581
665
 
582
666
  setContext('Ltree', tree);
583
667
 
584
- // Create and provide render coordinator for progressive rendering
585
- // Process only 2 nodes per frame - each node renders renderBatchSize children
668
+ // Create stable callback references to avoid inline arrow functions causing re-renders
669
+ // These are defined as a plain object with function references, not new functions each render
670
+ const nodeCallbacks: NodeCallbacks<T> = {
671
+ onNodeClicked: _onNodeClicked,
672
+ onNodeRightClicked: _onNodeRightClicked,
673
+ onNodeDragStart: _onNodeDragStart,
674
+ onNodeDragOver: _onNodeDragOver,
675
+ onNodeDragLeave: _onNodeDragLeave,
676
+ onNodeDrop: _onNodeDrop,
677
+ onZoneDrop: _onZoneDrop,
678
+ onTouchDragStart: _onTouchStart,
679
+ onTouchDragMove: _onTouchMove,
680
+ onTouchDragEnd: _onTouchEnd,
681
+ };
682
+ setContext('NodeCallbacks', nodeCallbacks);
683
+
684
+ // Note: NodeConfig is set via $effect below since it depends on reactive props
685
+
686
+ // Create and provide render coordinator for progressive rendering (recursive mode)
687
+ // Process only 2 nodes per frame - each node renders initialBatchSize children
586
688
  // This prevents too many reactive updates per frame
587
689
  const renderCoordinator = progressiveRender ? createRenderCoordinator(2, {
588
690
  onStart: () => {
@@ -601,10 +703,63 @@
601
703
  setContext('RenderCoordinator', renderCoordinator);
602
704
  }
603
705
 
706
+ // Create stable config object - updated when props change
707
+ // Using $state.raw to avoid deep reactivity on the config object itself
708
+ let nodeConfig = $state.raw<NodeConfig>({
709
+ shouldToggleOnNodeClick: shouldToggleOnNodeClick ?? true,
710
+ expandIconClass: expandIconClass ?? 'ltree-icon-expand',
711
+ collapseIconClass: collapseIconClass ?? 'ltree-icon-collapse',
712
+ leafIconClass: leafIconClass ?? 'ltree-icon-leaf',
713
+ selectedNodeClass,
714
+ dragOverNodeClass,
715
+ dropZoneMode: dropZoneMode ?? 'glow',
716
+ dropZoneLayout: dropZoneLayout ?? 'around',
717
+ dropZoneStart: dropZoneStart ?? 33,
718
+ dropZoneMaxWidth: dropZoneMaxWidth ?? 120,
719
+ allowCopy: allowCopy ?? false,
720
+ });
721
+ setContext('NodeConfig', nodeConfig);
722
+
723
+ // Update config when props change (rarely happens, but supports dynamic updates)
724
+ $effect(() => {
725
+ nodeConfig = {
726
+ shouldToggleOnNodeClick: shouldToggleOnNodeClick ?? true,
727
+ expandIconClass: expandIconClass ?? 'ltree-icon-expand',
728
+ collapseIconClass: collapseIconClass ?? 'ltree-icon-collapse',
729
+ leafIconClass: leafIconClass ?? 'ltree-icon-leaf',
730
+ selectedNodeClass,
731
+ dragOverNodeClass,
732
+ dropZoneMode: dropZoneMode ?? 'glow',
733
+ dropZoneLayout: dropZoneLayout ?? 'around',
734
+ dropZoneStart: dropZoneStart ?? 33,
735
+ dropZoneMaxWidth: dropZoneMaxWidth ?? 120,
736
+ allowCopy: allowCopy ?? false,
737
+ };
738
+ });
739
+
604
740
  $effect(() => {
605
741
  tree.filterNodes(searchText);
606
742
  });
607
743
 
744
+ // Performance instrumentation for flat mode
745
+ let flatRenderStart: number | null = null;
746
+ $effect(() => {
747
+ if (useFlatRendering && tree?.changeTracker) {
748
+ // changeTracker changed - flat render is about to happen
749
+ flatRenderStart = performance.now();
750
+ console.log('[Flat Mode] changeTracker changed, render starting...');
751
+
752
+ // Measure after Svelte processes the DOM update (single rAF = next paint)
753
+ requestAnimationFrame(() => {
754
+ if (flatRenderStart) {
755
+ const elapsed = performance.now() - flatRenderStart;
756
+ console.log(`[Flat Mode] DOM update complete: ${elapsed.toFixed(2)}ms`);
757
+ flatRenderStart = null;
758
+ }
759
+ });
760
+ }
761
+ });
762
+
608
763
  $effect(() => {
609
764
  if (tree && data) {
610
765
  if (_skipInsertArray) {
@@ -614,11 +769,126 @@
614
769
  }
615
770
  // Reset progressive render coordinator when data changes
616
771
  renderCoordinator?.reset();
772
+ // Reset flat progressive render state when data changes
773
+ flatRenderedIds = new Set();
774
+ flatRenderQueue = [];
775
+ currentBatchSize = 0; // Reset exponential batch size
617
776
  console.log('[Tree] Running insertArray with', data.length, 'items');
618
777
  insertResult = tree.insertArray(data);
619
778
  }
620
779
  });
621
780
 
781
+ // Progressive rendering for flat mode - detect new nodes and queue them
782
+ // Use a separate tracker to avoid reactive loops (effect reads AND writes flatRenderedIds)
783
+ let lastFlatNodesTracker: Symbol | null = null;
784
+
785
+ $effect(() => {
786
+ if (!useFlatRendering || !progressiveRender || !tree?.visibleFlatNodes) return;
787
+
788
+ // Only react to changeTracker changes, not to our own state updates
789
+ const tracker = tree.changeTracker;
790
+ if (tracker === lastFlatNodesTracker) return;
791
+ lastFlatNodesTracker = tracker;
792
+
793
+ const allNodes = tree.visibleFlatNodes;
794
+ const currentIds = new Set(allNodes.map(n => n.id));
795
+
796
+ // Snapshot current state to avoid reactive reads during computation
797
+ const renderedSnapshot = new Set(flatRenderedIds);
798
+ const queueSnapshot = new Set(flatRenderQueue);
799
+
800
+ // Find new nodes (in current but not yet rendered AND not already queued)
801
+ const newIds: string[] = [];
802
+ for (const node of allNodes) {
803
+ if (!renderedSnapshot.has(node.id) && !queueSnapshot.has(node.id)) {
804
+ newIds.push(node.id);
805
+ }
806
+ }
807
+
808
+ // Find removed nodes (rendered but no longer in current)
809
+ const removedIds: string[] = [];
810
+ for (const id of renderedSnapshot) {
811
+ if (!currentIds.has(id)) {
812
+ removedIds.push(id);
813
+ }
814
+ }
815
+
816
+ // Remove nodes that are no longer visible
817
+ if (removedIds.length > 0) {
818
+ const newRendered = new Set(renderedSnapshot);
819
+ for (const id of removedIds) {
820
+ newRendered.delete(id);
821
+ }
822
+ flatRenderedIds = newRendered;
823
+ }
824
+
825
+ // Queue new nodes for progressive rendering
826
+ if (newIds.length > 0) {
827
+ // If we already have many rendered nodes and adding few new ones,
828
+ // skip progressive batching to minimize Svelte diffs (expand/collapse case)
829
+ const alreadyHasManyNodes = renderedSnapshot.size > 1000;
830
+ const addingFewNodes = newIds.length < 200;
831
+
832
+ if (alreadyHasManyNodes && addingFewNodes) {
833
+ // Add all at once - one diff is faster than multiple diffs on large arrays
834
+ console.log(`[Flat Progressive] Adding ${newIds.length} nodes immediately (large tree optimization)`);
835
+ flatRenderedIds = new Set([...flatRenderedIds, ...newIds]);
836
+ } else {
837
+ // Progressive batching for initial load (exponential: 20 → 40 → 80 → 160...)
838
+ currentBatchSize = initialBatchSize; // Start with initial batch size
839
+ console.log(`[Flat Progressive] Queueing ${newIds.length} new nodes for progressive render (exponential batching)`);
840
+ const immediateBatch = newIds.slice(0, currentBatchSize);
841
+ const remaining = newIds.slice(currentBatchSize);
842
+
843
+ if (immediateBatch.length > 0) {
844
+ flatRenderedIds = new Set([...flatRenderedIds, ...immediateBatch]);
845
+ }
846
+
847
+ // Double batch size for next iteration (capped at maxBatchSize)
848
+ currentBatchSize = Math.min(currentBatchSize * 2, maxBatchSize);
849
+
850
+ if (remaining.length > 0) {
851
+ flatRenderQueue = [...remaining]; // Replace queue, don't append
852
+ scheduleFlatRenderBatch();
853
+ }
854
+ }
855
+ }
856
+ });
857
+
858
+ // Process flat render queue in batches (exponential sizing)
859
+ function scheduleFlatRenderBatch() {
860
+ if (flatRenderAnimationFrame) return; // Already scheduled
861
+
862
+ flatRenderAnimationFrame = requestAnimationFrame(() => {
863
+ flatRenderAnimationFrame = null;
864
+
865
+ if (flatRenderQueue.length === 0) return;
866
+
867
+ const batchSize = currentBatchSize || initialBatchSize;
868
+ const batch = flatRenderQueue.slice(0, batchSize);
869
+ const remaining = flatRenderQueue.slice(batchSize);
870
+
871
+ flatRenderedIds = new Set([...flatRenderedIds, ...batch]);
872
+ flatRenderQueue = remaining;
873
+
874
+ // Double batch size for next iteration (capped at maxBatchSize)
875
+ currentBatchSize = Math.min(batchSize * 2, maxBatchSize);
876
+
877
+ // console.log(`[Flat Progressive] Rendered batch of ${batch.length}, next batch: ${currentBatchSize}, ${remaining.length} remaining`);
878
+
879
+ if (remaining.length > 0) {
880
+ scheduleFlatRenderBatch();
881
+ }
882
+ });
883
+ }
884
+
885
+ // Derived: nodes to render in flat mode (filtered by progressive state)
886
+ const flatNodesToRender = $derived(
887
+ useFlatRendering && progressiveRender
888
+ ? tree?.visibleFlatNodes?.filter(n => flatRenderedIds.has(n.id)) ?? []
889
+ : tree?.visibleFlatNodes ?? []
890
+ );
891
+
622
892
  // $inspect("tree change tracker", tree?.changeTracker?.toString());
623
893
 
624
894
  function generateTreeId(): string {
@@ -724,7 +994,7 @@
724
994
  * Same-tree moves are auto-handled by default - the library calls moveNode() internally.
725
995
  * onNodeDrop is still called for notification/logging purposes.
726
996
  */
727
- function _handleDrop(dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent): boolean {
997
+ async function _handleDrop(dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent): Promise<boolean> {
728
998
  // Determine operation based on Ctrl key and allowCopy setting
729
999
  // Touch events always use 'move' (no Ctrl key on mobile)
730
1000
  let operation: DropOperation = 'move';
@@ -741,9 +1011,9 @@
741
1011
  isCrossTree: draggedNode.treeId !== treeId
742
1012
  });
743
1013
 
744
- // Call beforeDropCallback if provided
1014
+ // Call beforeDropCallback if provided (supports async for dialogs)
745
1015
  if (beforeDropCallback) {
746
- const result = beforeDropCallback(dropNode, draggedNode, position, event, operation);
1016
+ const result = await beforeDropCallback(dropNode, draggedNode, position, event, operation);
747
1017
  if (result === false) {
748
1018
  // Drop cancelled
749
1019
  return false;
@@ -1335,40 +1605,21 @@
1335
1605
 
1336
1606
  <div class:bodyClass>
1337
1607
  {#if tree?.root}
1338
- {#key tree.changeTracker}
1339
- <div class="ltree-tree">
1340
- {#each tree.tree as node (node.id)}
1608
+ <!-- Flat rendering mode: no {#key} block, uses visibleFlatNodes for efficient updates -->
1609
+ {#if useFlatRendering}
1610
+ <div class="ltree-tree ltree-flat-mode">
1611
+ {#each flatNodesToRender as node (node.id + '|' + node.path + '|' + node.hasChildren)}
1341
1612
  <Node
1342
1613
  {node}
1343
1614
  children={nodeTemplate}
1344
- {shouldToggleOnNodeClick}
1345
- {progressiveRender}
1346
- {renderBatchSize}
1347
- onNodeClicked={(node) => _onNodeClicked(node)}
1348
- onNodeRightClicked={(node, event) => _onNodeRightClicked(node, event)}
1349
- onNodeDragStart={(node, event) => _onNodeDragStart(node, event)}
1350
- onNodeDragOver={(node, event) => _onNodeDragOver(node, event)}
1351
- onNodeDragLeave={(node, event) => _onNodeDragLeave(node, event)}
1352
- onNodeDrop={(node, event) => _onNodeDrop(node, event)}
1353
- onZoneDrop={(node, position, event) => _onZoneDrop(node, position, event)}
1354
- onTouchDragStart={(node, event) => _onTouchStart(node, event)}
1355
- onTouchDragMove={(node, event) => _onTouchMove(node, event)}
1356
- onTouchDragEnd={(node, event) => _onTouchEnd(node, event)}
1357
- {expandIconClass}
1358
- {collapseIconClass}
1359
- {leafIconClass}
1360
- {selectedNodeClass}
1361
- {dragOverNodeClass}
1615
+ progressiveRender={false}
1362
1616
  isDraggedNode={draggedNode?.path === node.path}
1363
1617
  {isDragInProgress}
1364
1618
  hoveredNodeForDropPath={hoveredNodeForDrop?.path}
1365
1619
  {activeDropPosition}
1366
- {dropZoneMode}
1367
- {dropZoneLayout}
1368
- {dropZoneStart}
1369
- {dropZoneMaxWidth}
1370
1620
  dropOperation={currentDropOperation}
1371
- {allowCopy}
1621
+ flatMode={true}
1622
+ {flatIndentSize}
1372
1623
  />
1373
1624
  {:else}
1374
1625
  <!-- Empty state when tree has no items -->
@@ -1396,7 +1647,50 @@
1396
1647
  </div>
1397
1648
  {/each}
1398
1649
  </div>
1399
- {/key}
1650
+ {:else}
1651
+ <!-- Recursive rendering mode: uses {#key} block for forced re-renders -->
1652
+ {#key tree.changeTracker}
1653
+ <div class="ltree-tree">
1654
+ {#each tree.tree as node (node.id)}
1655
+ <Node
1656
+ {node}
1657
+ children={nodeTemplate}
1658
+ {progressiveRender}
1659
+ renderBatchSize={initialBatchSize}
1660
+ isDraggedNode={draggedNode?.path === node.path}
1661
+ {isDragInProgress}
1662
+ hoveredNodeForDropPath={hoveredNodeForDrop?.path}
1663
+ {activeDropPosition}
1664
+ dropOperation={currentDropOperation}
1665
+ />
1666
+ {:else}
1667
+ <!-- Empty state when tree has no items -->
1668
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
1669
+ <div
1670
+ class="ltree-empty-state"
1671
+ class:ltree-drop-placeholder={isDropPlaceholderActive}
1672
+ ondragenter={handleEmptyTreeDragOver}
1673
+ ondragover={handleEmptyTreeDragOver}
1674
+ ondragleave={handleEmptyTreeDragLeave}
1675
+ ondrop={handleEmptyTreeDrop}
1676
+ ontouchend={handleEmptyTreeTouchEnd}
1677
+ >
1678
+ {#if isDropPlaceholderActive}
1679
+ {#if dropPlaceholder}
1680
+ {@render dropPlaceholder()}
1681
+ {:else}
1682
+ <div class="ltree-drop-placeholder-content">
1683
+ Drop here to add
1684
+ </div>
1685
+ {/if}
1686
+ {:else}
1687
+ {@render noDataFound?.()}
1688
+ {/if}
1689
+ </div>
1690
+ {/each}
1691
+ </div>
1692
+ {/key}
1693
+ {/if}
1400
1694
  {:else}
1401
1695
  <!-- Empty tree drop zone -->
1402
1696
  <!-- svelte-ignore a11y_no_static_element_interactions -->