@keenmate/svelte-treeview 4.6.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,8 +2,54 @@
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
- > [!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/
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
 
@@ -83,40 +129,6 @@ let treeData = $state.raw<TreeNode[]>([])
83
129
 
84
130
  The array itself remains reactive - only individual items lose deep reactivity (which Tree doesn't need).
85
131
 
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
-
120
132
  ## 🎯 Quick Start
121
133
 
122
134
  ```svelte
@@ -312,6 +324,45 @@ When using `dropZoneMode="floating"` (default), users can choose where to drop:
312
324
  - **Below**: Insert as sibling after the target node
313
325
  - **Child**: Insert as child of the target node
314
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
+ />
350
+ ```
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
+
315
366
  #### Async Drop Validation
316
367
 
317
368
  Use `beforeDropCallback` to validate or modify drops, including async operations like confirmation dialogs:
@@ -602,6 +653,7 @@ The component includes several pre-built classes for styling selected nodes:
602
653
  | `isSelectedMember` | `string \| null` | `null` | Property name for selected state |
603
654
  | `isDraggableMember` | `string \| null` | `null` | Property name for draggable state |
604
655
  | `isDropAllowedMember` | `string \| null` | `null` | Property name for drop allowed state |
656
+ | `allowedDropPositionsMember` | `string \| null` | `null` | Property name for allowed drop positions array |
605
657
  | `hasChildrenMember` | `string \| null` | `null` | Property name for children existence |
606
658
  | `isSorted` | `boolean \| null` | `null` | Whether items should be sorted |
607
659
 
@@ -656,6 +708,7 @@ Without both requirements, no search indexing will occur.
656
708
  | `onNodeClicked` | `(node) => void` | `undefined` | Node click event handler |
657
709
  | `onNodeDragStart` | `(node, event) => void` | `undefined` | Drag start event handler |
658
710
  | `onNodeDragOver` | `(node, event) => void` | `undefined` | Drag over event handler |
711
+ | `getAllowedDropPositionsCallback` | `(node) => DropPosition[] \| null` | `undefined` | Callback returning allowed drop positions per node |
659
712
  | `beforeDropCallback` | `(dropNode, draggedNode, position, event, operation) => boolean \| { position?, operation? } \| Promise<...>` | `undefined` | Async-capable callback to validate/modify drops before they happen |
660
713
  | `onNodeDrop` | `(dropNode, draggedNode, position, event, operation) => void` | `undefined` | Drop event handler. Position is 'above', 'below', or 'child'. Operation is 'move' or 'copy' |
661
714
 
@@ -89,24 +89,63 @@
89
89
  // Track glow position for glow mode
90
90
  let glowPosition = $state<'above' | 'below' | 'child' | null>(null);
91
91
 
92
+ // Get allowed drop positions for this node (empty/undefined = all allowed)
93
+ // Uses tree.getNodeAllowedDropPositions which checks callback > member > node property
94
+ const allowedPositions = $derived(tree.getNodeAllowedDropPositions(node));
95
+
96
+ // Check if a position is allowed for this node
97
+ function isPositionAllowed(position: DropPosition): boolean {
98
+ if (!allowedPositions || allowedPositions.length === 0) {
99
+ return true; // All positions allowed by default
100
+ }
101
+ return allowedPositions.includes(position);
102
+ }
103
+
92
104
  // Calculate glow position based on mouse position in the node row
93
- function calculateGlowPosition(event: DragEvent, element: HTMLElement): 'above' | 'below' | 'child' {
105
+ // Respects allowedDropPositions - snaps to nearest allowed position
106
+ function calculateGlowPosition(event: DragEvent, element: HTMLElement): 'above' | 'below' | 'child' | null {
94
107
  const rect = element.getBoundingClientRect();
95
108
  const x = event.clientX - rect.left;
96
109
  const y = event.clientY - rect.top;
97
110
  const width = rect.width;
98
111
  const height = rect.height;
99
112
 
100
- // Right half = child
113
+ // Calculate the ideal position based on mouse position
114
+ let idealPosition: DropPosition;
101
115
  if (x > width / 2) {
102
- return 'child';
116
+ idealPosition = 'child';
117
+ } else if (y < height / 2) {
118
+ idealPosition = 'above';
119
+ } else {
120
+ idealPosition = 'below';
103
121
  }
104
- // Left half, top 50% = above
105
- if (y < height / 2) {
106
- return 'above';
122
+
123
+ // If no restrictions, return the ideal position
124
+ if (!allowedPositions || allowedPositions.length === 0) {
125
+ return idealPosition;
126
+ }
127
+
128
+ // If the ideal position is allowed, use it
129
+ if (allowedPositions.includes(idealPosition)) {
130
+ return idealPosition;
107
131
  }
108
- // Left half, bottom 50% = below
109
- return 'below';
132
+
133
+ // Otherwise, snap to the nearest allowed position
134
+ // Priority: if only one position allowed, use that
135
+ if (allowedPositions.length === 1) {
136
+ return allowedPositions[0];
137
+ }
138
+
139
+ // Multiple positions allowed but not the ideal one
140
+ // For above/below: pick based on Y position
141
+ // For child: pick based on what's available
142
+ if (allowedPositions.includes('above') && allowedPositions.includes('below')) {
143
+ // Both above and below allowed, pick based on Y
144
+ return y < height / 2 ? 'above' : 'below';
145
+ }
146
+
147
+ // Return the first allowed position
148
+ return allowedPositions[0];
110
149
  }
111
150
 
112
151
  // Convert reactive statements to derived values
@@ -244,9 +283,9 @@
244
283
  class:ltree-clickable={node.isSelectable}
245
284
  class:ltree-dragged={isDraggedNode}
246
285
  class:ltree-draggable={node?.isDraggable}
247
- class:ltree-glow-above={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'above'}
248
- class:ltree-glow-below={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'below'}
249
- class:ltree-glow-child={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'child'}
286
+ class:ltree-glow-above={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'above' && isPositionAllowed('above')}
287
+ class:ltree-glow-below={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'below' && isPositionAllowed('below')}
288
+ class:ltree-glow-child={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'child' && isPositionAllowed('child')}
250
289
  class:ltree-drop-copy={isDragInProgress && isHoveredForDrop && dropOperation === 'copy'}
251
290
  draggable={node?.isDraggable}
252
291
  onclick={(e) => {
@@ -320,33 +359,39 @@
320
359
  </div>
321
360
 
322
361
  <!-- Drop zones: positioned relative to .ltree-node-row (outside content to avoid padding issues) -->
323
- <!-- Only render floating drop zones when in 'floating' mode -->
362
+ <!-- Only render floating drop zones when in 'floating' mode, filtered by allowedDropPositions -->
324
363
  {#if dropZoneMode === 'floating' && isDragInProgress && isHoveredForDrop}
325
364
  <div
326
365
  class="ltree-drop-zones ltree-drop-zones-{dropZoneLayout}"
327
366
  style="--drop-zone-start: {formattedDropZoneStart}; --drop-zone-max-width: {dropZoneMaxWidth}px;"
328
367
  >
329
- <div
330
- class="ltree-drop-zone ltree-drop-above"
331
- class:ltree-drop-zone-active={hoveredZone === 'above'}
332
- ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'above'; callbacks.onNodeDragOver(node, e); }}
333
- ondragleave={() => { hoveredZone = null; }}
334
- ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'above', e); }}
335
- >↑ Above</div>
336
- <div
337
- class="ltree-drop-zone ltree-drop-below"
338
- class:ltree-drop-zone-active={hoveredZone === 'below'}
339
- ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'below'; callbacks.onNodeDragOver(node, e); }}
340
- ondragleave={() => { hoveredZone = null; }}
341
- ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'below', e); }}
342
- >↓ Below</div>
343
- <div
344
- class="ltree-drop-zone ltree-drop-child"
345
- class:ltree-drop-zone-active={hoveredZone === 'child'}
346
- ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'child'; callbacks.onNodeDragOver(node, e); }}
347
- ondragleave={() => { hoveredZone = null; }}
348
- ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'child', e); }}
349
- >→ Child</div>
368
+ {#if isPositionAllowed('above')}
369
+ <div
370
+ class="ltree-drop-zone ltree-drop-above"
371
+ class:ltree-drop-zone-active={hoveredZone === 'above'}
372
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'above'; callbacks.onNodeDragOver(node, e); }}
373
+ ondragleave={() => { hoveredZone = null; }}
374
+ ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'above', e); }}
375
+ >↑ Above</div>
376
+ {/if}
377
+ {#if isPositionAllowed('below')}
378
+ <div
379
+ class="ltree-drop-zone ltree-drop-below"
380
+ class:ltree-drop-zone-active={hoveredZone === 'below'}
381
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'below'; callbacks.onNodeDragOver(node, e); }}
382
+ ondragleave={() => { hoveredZone = null; }}
383
+ ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'below', e); }}
384
+ >↓ Below</div>
385
+ {/if}
386
+ {#if isPositionAllowed('child')}
387
+ <div
388
+ class="ltree-drop-zone ltree-drop-child"
389
+ class:ltree-drop-zone-active={hoveredZone === 'child'}
390
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'child'; callbacks.onNodeDragOver(node, e); }}
391
+ ondragleave={() => { hoveredZone = null; }}
392
+ ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'child', e); }}
393
+ >→ Child</div>
394
+ {/if}
350
395
  </div>
351
396
  {/if}
352
397
  </div>
@@ -94,6 +94,8 @@
94
94
  isSelectedMember?: string | null | undefined;
95
95
  isDraggableMember?: string | null | undefined;
96
96
  isDropAllowedMember?: string | null | undefined;
97
+ allowedDropPositionsMember?: string | null | undefined;
98
+ getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
97
99
  hasChildrenMember?: string | null | undefined;
98
100
  isSorted?: boolean | null | undefined;
99
101
 
@@ -208,6 +210,8 @@
208
210
  isSelectedMember,
209
211
  isDraggableMember,
210
212
  isDropAllowedMember,
213
+ allowedDropPositionsMember,
214
+ getAllowedDropPositionsCallback,
211
215
 
212
216
  displayValueMember,
213
217
  getDisplayValueCallback,
@@ -636,11 +640,13 @@
636
640
  isSelectedMember,
637
641
  isDraggableMember,
638
642
  isDropAllowedMember,
643
+ allowedDropPositionsMember,
639
644
 
640
645
  displayValueMember,
641
646
  getDisplayValueCallback,
642
647
  searchValueMember,
643
648
  getSearchValueCallback,
649
+ getAllowedDropPositionsCallback,
644
650
  orderMember,
645
651
  treeId,
646
652
  treePathSeparator,
@@ -13,6 +13,8 @@ declare function $$render<T>(): {
13
13
  isSelectedMember?: string | null | undefined;
14
14
  isDraggableMember?: string | null | undefined;
15
15
  isDropAllowedMember?: string | null | undefined;
16
+ allowedDropPositionsMember?: string | null | undefined;
17
+ getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
16
18
  hasChildrenMember?: string | null | undefined;
17
19
  isSorted?: boolean | null | undefined;
18
20
  displayValueMember?: string | null | undefined;
@@ -158,6 +160,8 @@ declare function $$render<T>(): {
158
160
  isSelectedMember?: string | null | undefined;
159
161
  isDraggableMember?: string | null | undefined;
160
162
  isDropAllowedMember?: string | null | undefined;
163
+ allowedDropPositionsMember?: string | null | undefined;
164
+ getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
161
165
  hasChildrenMember?: string | null | undefined;
162
166
  isSorted?: boolean | null | undefined;
163
167
  displayValueMember?: string | null | undefined;
@@ -313,6 +317,8 @@ declare class __sveltets_Render<T> {
313
317
  isSelectedMember?: string | null | undefined;
314
318
  isDraggableMember?: string | null | undefined;
315
319
  isDropAllowedMember?: string | null | undefined;
320
+ allowedDropPositionsMember?: string | null | undefined;
321
+ getAllowedDropPositionsCallback?: ((node: LTreeNode<T>) => DropPosition[] | null | undefined) | undefined;
316
322
  hasChildrenMember?: string | null | undefined;
317
323
  isSorted?: boolean | null | undefined;
318
324
  displayValueMember?: string | null | undefined;
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "4.6.0";
1
+ export declare const VERSION = "4.7.0";
2
2
  export declare const PACKAGE_NAME = "@keenmate/svelte-treeview";
3
3
  export declare const AUTHOR = "KeenMate";
4
4
  export declare const LICENSE = "MIT";
@@ -1,6 +1,6 @@
1
1
  // Auto-generated file - do not edit manually
2
2
  // Generated by scripts/generate-constants.js
3
- export const VERSION = "4.6.0";
3
+ export const VERSION = "4.7.0";
4
4
  export const PACKAGE_NAME = "@keenmate/svelte-treeview";
5
5
  export const AUTHOR = "KeenMate";
6
6
  export const LICENSE = "MIT";
@@ -26,7 +26,8 @@ export class Indexer {
26
26
  }
27
27
  // Add items to the processing queue
28
28
  addToQueue(items) {
29
- this.processingQueue.push(...items);
29
+ // Use concat instead of push(...items) to avoid stack overflow with large arrays
30
+ this.processingQueue = this.processingQueue.concat(items);
30
31
  this.totalItemsAdded += items.length;
31
32
  indexLogger.debug(`[${this.treeId}] Added ${items.length} items to queue. Queue size: ${this.processingQueue.length}`);
32
33
  // Start processing if not already running
@@ -1,4 +1,5 @@
1
1
  export type NodeId = string | number;
2
+ export type DropPosition = 'above' | 'below' | 'child';
2
3
  export declare enum VisualState {
3
4
  indeterminate = "indeterminate",
4
5
  selected = "true",
@@ -18,6 +19,7 @@ export interface LTreeNode<T> {
18
19
  priority: number | null | undefined;
19
20
  isDraggable: boolean;
20
21
  isDropAllowed: boolean;
22
+ allowedDropPositions: DropPosition[] | null | undefined;
21
23
  isInsertAllowed: boolean;
22
24
  isNestAllowed: boolean;
23
25
  isCheckboxVisible: boolean | null | undefined;
@@ -19,6 +19,7 @@ export function createLTreeNode(data) {
19
19
  priority: undefined,
20
20
  isDraggable: true,
21
21
  isDropAllowed: true,
22
+ allowedDropPositions: undefined,
22
23
  isInsertAllowed: true,
23
24
  isNestAllowed: true,
24
25
  isCheckboxVisible: false,
@@ -1,4 +1,4 @@
1
1
  import { Index } from 'flexsearch';
2
2
  import { type LTreeNode } from './ltree-node.svelte';
3
3
  import type { Ltree } from './types.js';
4
- export declare function createLTree<T>(_idMember: string, _pathMember: string, _parentPathMember?: string | null | undefined, _levelMember?: string | null | undefined, _hasChildrenMember?: string | null | undefined, _isExpandedMember?: string | null | undefined, _isSelectableMember?: string | null | undefined, _isDraggableMember?: string | null | undefined, _isDropAllowedMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _orderMember?: string | null | undefined, _treeId?: string, _treePathSeparator?: string | null | undefined, _expandLevel?: number | null | undefined, _shouldUseInternalSearchIndex?: boolean | null | undefined, _initializeIndexCallback?: () => Index, _indexerBatchSize?: number | null | undefined, _indexerTimeout?: number | null | undefined, opts?: Partial<Ltree<T>>): Ltree<T>;
4
+ export declare function createLTree<T>(_idMember: string, _pathMember: string, _parentPathMember?: string | null | undefined, _levelMember?: string | null | undefined, _hasChildrenMember?: string | null | undefined, _isExpandedMember?: string | null | undefined, _isSelectableMember?: string | null | undefined, _isDraggableMember?: string | null | undefined, _isDropAllowedMember?: string | null | undefined, _allowedDropPositionsMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => import('./types').DropPosition[] | null | undefined, _orderMember?: string | null | undefined, _treeId?: string, _treePathSeparator?: string | null | undefined, _expandLevel?: number | null | undefined, _shouldUseInternalSearchIndex?: boolean | null | undefined, _initializeIndexCallback?: () => Index, _indexerBatchSize?: number | null | undefined, _indexerTimeout?: number | null | undefined, opts?: Partial<Ltree<T>>): Ltree<T>;
@@ -5,7 +5,7 @@ import { getLevel, getParentPath, getPathSegments, getRelativePath } from '../he
5
5
  import { createSearchIndex } from './flex.js';
6
6
  import { Indexer } from './indexer.js';
7
7
  import { perfStart, perfEnd, perfSummary } from '../perf-logger.js';
8
- export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
8
+ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
9
9
  let shouldCalculateParentPath = isEmptyString(_parentPathMember);
10
10
  let shouldCalculateLevel = isEmptyString(_levelMember);
11
11
  let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
@@ -13,6 +13,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
13
13
  let shouldCalculateIsSelectable = isEmptyString(_isSelectableMember);
14
14
  let shouldCalculateIsDraggable = isEmptyString(_isDraggableMember);
15
15
  let shouldCalculateIsDropAllowed = isEmptyString(_isDropAllowedMember);
16
+ let shouldCalculateAllowedDropPositions = isEmptyString(_allowedDropPositionsMember);
16
17
  let shouldCalculateDisplayValue = isEmptyString(_displayValueMember);
17
18
  let shouldCalculateSearchValue = isEmptyString(_searchValueMember);
18
19
  // this is absolutely crucial to keep order of sorted items. Segments are just numbers and numbers as properties are always sorted
@@ -57,11 +58,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
57
58
  isSelectableMember: _isSelectableMember,
58
59
  isDraggableMember: _isDraggableMember,
59
60
  isDropAllowedMember: _isDropAllowedMember,
61
+ allowedDropPositionsMember: _allowedDropPositionsMember,
60
62
  hasChildrenMember: _hasChildrenMember,
61
63
  displayValueMember: _displayValueMember,
62
64
  getDisplayValueCallback: _getDisplayValueCallback,
63
65
  searchValueMember: _searchValueMember,
64
66
  getSearchValueCallback: _getSearchValueCallback,
67
+ getAllowedDropPositionsCallback: _getAllowedDropPositionsCallback,
65
68
  orderMember: _orderMember,
66
69
  isSorted: false,
67
70
  // Properties for filtering
@@ -171,6 +174,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
171
174
  node.isDraggable = row[_isDraggableMember];
172
175
  if (!shouldCalculateIsDropAllowed)
173
176
  node.isDropAllowed = row[_isDropAllowedMember];
177
+ if (!shouldCalculateAllowedDropPositions)
178
+ node.allowedDropPositions = row[_allowedDropPositionsMember];
174
179
  if (!shouldCalculateHasChildren)
175
180
  node.hasChildren = row[_hasChildrenMember];
176
181
  node.data = row;
@@ -532,6 +537,16 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
532
537
  return this.getSearchValueCallback(node);
533
538
  return '[N/A]';
534
539
  },
540
+ getNodeAllowedDropPositions(node) {
541
+ // Priority: callback > member > node property
542
+ if (this.getAllowedDropPositionsCallback) {
543
+ return this.getAllowedDropPositionsCallback(node);
544
+ }
545
+ if (!shouldCalculateAllowedDropPositions && node.data) {
546
+ return node.data[_allowedDropPositionsMember];
547
+ }
548
+ return node.allowedDropPositions;
549
+ },
535
550
  refresh() {
536
551
  this._emitTreeChanged();
537
552
  },
@@ -1,8 +1,7 @@
1
1
  import type { SearchOptions } from 'flexsearch';
2
- import type { LTreeNode } from './ltree-node.svelte';
3
- export type { LTreeNode } from './ltree-node.svelte';
2
+ import type { LTreeNode, DropPosition } from './ltree-node.svelte';
3
+ export type { LTreeNode, DropPosition } from './ltree-node.svelte';
4
4
  export type Tuple<T, U> = [T, U];
5
- export type DropPosition = 'above' | 'below' | 'child';
6
5
  export type DragDropMode = 'none' | 'self' | 'cross' | 'both';
7
6
  export type DropZoneLayout = 'around' | 'above' | 'below' | 'wave' | 'wave2';
8
7
  export type DropOperation = 'move' | 'copy';
@@ -69,7 +68,10 @@ export interface Ltree<T> {
69
68
  isSelectableMember: string | null | undefined;
70
69
  isDraggableMember: string | null | undefined;
71
70
  isDropAllowedMember: string | null | undefined;
71
+ allowedDropPositionsMember: string | null | undefined;
72
+ getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
72
73
  shouldDisplayDebugInformation: boolean | null | undefined;
74
+ getNodeAllowedDropPositions(node: LTreeNode<T>): DropPosition[] | null | undefined;
73
75
  get tree(): LTreeNode<T>[];
74
76
  /** Flat array of all visible nodes in render order (depth-first, respects isExpanded) */
75
77
  get visibleFlatNodes(): LTreeNode<T>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/svelte-treeview",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --port 17777",
6
6
  "build": "vite build && npm run prepack",