@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.
@@ -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;
@@ -45,11 +47,22 @@ declare function $$render<T>(): {
45
47
  shouldDisplayContextMenuInDebugMode?: boolean;
46
48
  isLoading?: boolean;
47
49
  progressiveRender?: boolean;
48
- renderBatchSize?: number;
50
+ initialBatchSize?: number;
51
+ maxBatchSize?: number;
49
52
  isRendering?: boolean;
50
53
  onRenderStart?: () => void;
51
54
  onRenderProgress?: (stats: RenderStats) => void;
52
55
  onRenderComplete?: (stats: RenderStats) => void;
56
+ /**
57
+ * Use flat/centralized rendering instead of recursive node rendering.
58
+ * This significantly improves performance for large trees by:
59
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
60
+ * - Using a single flat loop instead of recursive component instantiation
61
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
62
+ */
63
+ useFlatRendering?: boolean;
64
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
65
+ flatIndentSize?: string;
53
66
  dragDropMode?: DragDropMode;
54
67
  dropZoneMode?: "floating" | "glow";
55
68
  dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
@@ -64,11 +77,15 @@ declare function $$render<T>(): {
64
77
  * Called before a drop is processed. Return false to cancel the drop.
65
78
  * Return { position, operation } to override the drop position or operation.
66
79
  * Return true or undefined to proceed normally.
80
+ * Can be async - return a Promise to show dialogs or perform async validation.
67
81
  */
68
82
  beforeDropCallback?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | {
69
83
  position?: DropPosition;
70
84
  operation?: DropOperation;
71
- } | void;
85
+ } | void | Promise<boolean | {
86
+ position?: DropPosition;
87
+ operation?: DropOperation;
88
+ } | void>;
72
89
  onNodeDrop?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void;
73
90
  contextMenuCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[];
74
91
  bodyClass?: string | null | undefined;
@@ -125,7 +142,10 @@ declare function $$render<T>(): {
125
142
  getAllData: () => T[];
126
143
  closeContextMenu: () => void;
127
144
  scrollToPath: (path: string, options?: {
145
+ /** Expand ancestors to make the node visible (default: true) */
128
146
  expand?: boolean;
147
+ /** Also expand the target node itself to show its children (default: false for performance) */
148
+ expandTarget?: boolean;
129
149
  highlight?: boolean;
130
150
  scrollOptions?: ScrollIntoViewOptions;
131
151
  /** Scroll only within the nearest scrollable container (prevents page scroll) */
@@ -140,6 +160,8 @@ declare function $$render<T>(): {
140
160
  isSelectedMember?: string | null | undefined;
141
161
  isDraggableMember?: string | null | undefined;
142
162
  isDropAllowedMember?: string | null | undefined;
163
+ allowedDropPositionsMember?: string | null | undefined;
164
+ getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
143
165
  hasChildrenMember?: string | null | undefined;
144
166
  isSorted?: boolean | null | undefined;
145
167
  displayValueMember?: string | null | undefined;
@@ -172,11 +194,22 @@ declare function $$render<T>(): {
172
194
  shouldDisplayContextMenuInDebugMode?: boolean;
173
195
  isLoading?: boolean;
174
196
  progressiveRender?: boolean;
175
- renderBatchSize?: number;
197
+ initialBatchSize?: number;
198
+ maxBatchSize?: number;
176
199
  isRendering?: boolean;
177
200
  onRenderStart?: () => void;
178
201
  onRenderProgress?: (stats: RenderStats) => void;
179
202
  onRenderComplete?: (stats: RenderStats) => void;
203
+ /**
204
+ * Use flat/centralized rendering instead of recursive node rendering.
205
+ * This significantly improves performance for large trees by:
206
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
207
+ * - Using a single flat loop instead of recursive component instantiation
208
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
209
+ */
210
+ useFlatRendering?: boolean;
211
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
212
+ flatIndentSize?: string;
180
213
  dragDropMode?: DragDropMode;
181
214
  dropZoneMode?: "floating" | "glow";
182
215
  dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
@@ -191,11 +224,15 @@ declare function $$render<T>(): {
191
224
  * Called before a drop is processed. Return false to cancel the drop.
192
225
  * Return { position, operation } to override the drop position or operation.
193
226
  * Return true or undefined to proceed normally.
227
+ * Can be async - return a Promise to show dialogs or perform async validation.
194
228
  */
195
229
  beforeDropCallback?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | {
196
230
  position?: DropPosition;
197
231
  operation?: DropOperation;
198
- } | void;
232
+ } | void | Promise<boolean | {
233
+ position?: DropPosition;
234
+ operation?: DropOperation;
235
+ } | void>;
199
236
  onNodeDrop?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void;
200
237
  contextMenuCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[];
201
238
  bodyClass?: string | null | undefined;
@@ -262,7 +299,10 @@ declare class __sveltets_Render<T> {
262
299
  getAllData: () => T[];
263
300
  closeContextMenu: () => void;
264
301
  scrollToPath: (path: string, options?: {
302
+ /** Expand ancestors to make the node visible (default: true) */
265
303
  expand?: boolean;
304
+ /** Also expand the target node itself to show its children (default: false for performance) */
305
+ expandTarget?: boolean;
266
306
  highlight?: boolean;
267
307
  scrollOptions?: ScrollIntoViewOptions;
268
308
  /** Scroll only within the nearest scrollable container (prevents page scroll) */
@@ -277,6 +317,8 @@ declare class __sveltets_Render<T> {
277
317
  isSelectedMember?: string | null | undefined;
278
318
  isDraggableMember?: string | null | undefined;
279
319
  isDropAllowedMember?: string | null | undefined;
320
+ allowedDropPositionsMember?: string | null | undefined;
321
+ getAllowedDropPositionsCallback?: ((node: LTreeNode<T>) => DropPosition[] | null | undefined) | undefined;
280
322
  hasChildrenMember?: string | null | undefined;
281
323
  isSorted?: boolean | null | undefined;
282
324
  displayValueMember?: string | null | undefined;
@@ -309,11 +351,22 @@ declare class __sveltets_Render<T> {
309
351
  shouldDisplayContextMenuInDebugMode?: boolean;
310
352
  isLoading?: boolean;
311
353
  progressiveRender?: boolean;
312
- renderBatchSize?: number;
354
+ initialBatchSize?: number;
355
+ maxBatchSize?: number;
313
356
  isRendering?: boolean;
314
357
  onRenderStart?: (() => void) | undefined;
315
358
  onRenderProgress?: ((stats: RenderStats) => void) | undefined;
316
359
  onRenderComplete?: ((stats: RenderStats) => void) | undefined;
360
+ /**
361
+ * Use flat/centralized rendering instead of recursive node rendering.
362
+ * This significantly improves performance for large trees by:
363
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
364
+ * - Using a single flat loop instead of recursive component instantiation
365
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
366
+ */
367
+ useFlatRendering?: boolean;
368
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
369
+ flatIndentSize?: string;
317
370
  dragDropMode?: DragDropMode;
318
371
  dropZoneMode?: "floating" | "glow";
319
372
  dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
@@ -328,11 +381,15 @@ declare class __sveltets_Render<T> {
328
381
  * Called before a drop is processed. Return false to cancel the drop.
329
382
  * Return { position, operation } to override the drop position or operation.
330
383
  * Return true or undefined to proceed normally.
384
+ * Can be async - return a Promise to show dialogs or perform async validation.
331
385
  */
332
386
  beforeDropCallback?: ((dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | void | {
333
387
  position?: DropPosition;
334
388
  operation?: DropOperation;
335
- }) | undefined;
389
+ } | Promise<boolean | void | {
390
+ position?: DropPosition;
391
+ operation?: DropOperation;
392
+ }>) | undefined;
336
393
  onNodeDrop?: ((dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void) | undefined;
337
394
  contextMenuCallback?: ((node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[]) | undefined;
338
395
  bodyClass?: string | null | undefined;
@@ -345,7 +402,7 @@ declare class __sveltets_Render<T> {
345
402
  scrollHighlightClass?: string | null | undefined;
346
403
  contextMenuXOffset?: number | null | undefined;
347
404
  contextMenuYOffset?: number | null | undefined;
348
- }, "treeId" | "data" | "onNodeClicked" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "shouldToggleOnNodeClick" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "selectedNodeClass" | "dragOverNodeClass" | "dropZoneMode" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isDraggableMember" | "isDropAllowedMember" | "shouldDisplayDebugInformation" | "isSelectedMember" | "selectedNode" | "expandLevel" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "dragDropMode" | "beforeDropCallback" | "contextMenuCallback" | "bodyClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
405
+ }, "treeId" | "data" | "shouldToggleOnNodeClick" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "selectedNodeClass" | "dragOverNodeClass" | "dropZoneMode" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isDraggableMember" | "isDropAllowedMember" | "shouldDisplayDebugInformation" | "isSelectedMember" | "selectedNode" | "expandLevel" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "dragDropMode" | "onNodeClicked" | "onNodeDragStart" | "onNodeDragOver" | "beforeDropCallback" | "onNodeDrop" | "contextMenuCallback" | "bodyClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
349
406
  };
350
407
  }
351
408
  interface $$IsomorphicComponent {
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "4.5.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.5.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";
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { default as Tree } from "./components/Tree.svelte";
2
2
  export type { LTreeNode, NodeId, VisualState } from "./ltree/ltree-node.svelte";
3
3
  export type { Ltree, DropPosition, DragDropMode, DropOperation, ContextMenuItem, InsertArrayResult, TreeChange, ApplyChangesResult } from "./ltree/types";
4
4
  export type { RenderStats } from "./components/RenderCoordinator.svelte";
5
+ export type { NodeCallbacks, NodeConfig } from "./components/Tree.svelte";
5
6
  export { enableLogging, disableLogging, setLogLevel, setCategoryLevel, LOGGING_CATEGORIES } from "./logger";
6
7
  export { enablePerfLogging, disablePerfLogging, setPerfThreshold, isPerfLoggingEnabled, perfStart, perfEnd, perfMeasure, perfSummary } from "./perf-logger";
7
8
  export type { GlobalTreeviewAPI } from "./global-api";
@@ -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
@@ -31,6 +32,9 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
31
32
  let flatTreeNodes = [];
32
33
  let filteredTree = null;
33
34
  let isFiltered = false;
35
+ // Cache for visibleFlatNodes - only recompute when tree changes
36
+ let cachedVisibleFlatNodes = [];
37
+ let cachedVisibleFlatNodesTracker = null;
34
38
  // Async search indexing infrastructure
35
39
  let indexer = null;
36
40
  // Initialize indexer when search index is available
@@ -54,11 +58,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
54
58
  isSelectableMember: _isSelectableMember,
55
59
  isDraggableMember: _isDraggableMember,
56
60
  isDropAllowedMember: _isDropAllowedMember,
61
+ allowedDropPositionsMember: _allowedDropPositionsMember,
57
62
  hasChildrenMember: _hasChildrenMember,
58
63
  displayValueMember: _displayValueMember,
59
64
  getDisplayValueCallback: _getDisplayValueCallback,
60
65
  searchValueMember: _searchValueMember,
61
66
  getSearchValueCallback: _getSearchValueCallback,
67
+ getAllowedDropPositionsCallback: _getAllowedDropPositionsCallback,
62
68
  orderMember: _orderMember,
63
69
  isSorted: false,
64
70
  // Properties for filtering
@@ -74,6 +80,54 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
74
80
  }
75
81
  return Object.values(root.children);
76
82
  },
83
+ /**
84
+ * Returns a flat array of all visible nodes in render order (depth-first).
85
+ * A node is visible if all its ancestors are expanded.
86
+ * This is optimized for flat/centralized rendering without recursion.
87
+ *
88
+ * Note: This getter depends on changeTracker to ensure reactivity when
89
+ * nodes are expanded/collapsed or the tree structure changes.
90
+ * Results are cached to avoid recomputation on repeated access.
91
+ */
92
+ get visibleFlatNodes() {
93
+ // Explicitly read changeTracker to create reactive dependency
94
+ const _tracker = changeTracker;
95
+ // Return cached result if changeTracker hasn't changed
96
+ if (_tracker === cachedVisibleFlatNodesTracker && cachedVisibleFlatNodes.length > 0) {
97
+ // console.log(`[visibleFlatNodes] Cache HIT - returning ${cachedVisibleFlatNodes.length} nodes`);
98
+ return cachedVisibleFlatNodes;
99
+ }
100
+ const computeStart = performance.now();
101
+ const startRoot = this.isFiltered ? filteredRoot : root;
102
+ if (!startRoot?.children || !_tracker) {
103
+ cachedVisibleFlatNodes = [];
104
+ cachedVisibleFlatNodesTracker = _tracker;
105
+ return cachedVisibleFlatNodes;
106
+ }
107
+ const result = [];
108
+ const self = this;
109
+ function traverse(node) {
110
+ // Get children and optionally sort them
111
+ let children = Object.values(node.children);
112
+ if (self.isSorted && children.length > 0) {
113
+ children = self.sortCallback(children);
114
+ }
115
+ for (const child of children) {
116
+ result.push(child);
117
+ // Only traverse into children if this node is expanded
118
+ if (child.isExpanded && child.hasChildren) {
119
+ traverse(child);
120
+ }
121
+ }
122
+ }
123
+ traverse(startRoot);
124
+ const computeTime = performance.now() - computeStart;
125
+ console.log(`[visibleFlatNodes] Computed ${result.length} nodes in ${computeTime.toFixed(2)}ms`);
126
+ // Cache the result
127
+ cachedVisibleFlatNodes = result;
128
+ cachedVisibleFlatNodesTracker = _tracker;
129
+ return result;
130
+ },
77
131
  get statistics() {
78
132
  const filteredNodeCount = isFiltered ? filteredTree?.length || 0 : 0;
79
133
  const indexerStatus = indexer?.getStatus() || { isProcessing: false, queueSize: 0 };
@@ -120,6 +174,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
120
174
  node.isDraggable = row[_isDraggableMember];
121
175
  if (!shouldCalculateIsDropAllowed)
122
176
  node.isDropAllowed = row[_isDropAllowedMember];
177
+ if (!shouldCalculateAllowedDropPositions)
178
+ node.allowedDropPositions = row[_allowedDropPositionsMember];
123
179
  if (!shouldCalculateHasChildren)
124
180
  node.hasChildren = row[_hasChildrenMember];
125
181
  node.data = row;
@@ -408,31 +464,46 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
408
464
  }
409
465
  },
410
466
  expandNodes: function (path, noEmitChanges = false) {
467
+ perfStart(`[${_treeId}] expandNodes`);
411
468
  let node = this.isFiltered ? filteredRoot : root;
469
+ let hasChanges = false;
412
470
  const segments = path.split(this.treePathSeparator);
413
471
  for (let i = 0; i < segments.length; i++) {
414
472
  const segment = segmentPrefix + segments[i];
415
473
  if (node.children.hasOwnProperty(segment)) {
416
474
  node = node.children[segment];
417
- node.isExpanded = true;
475
+ // Only mark as changed if actually changing from collapsed to expanded
476
+ if (!node.isExpanded) {
477
+ node.isExpanded = true;
478
+ hasChanges = true;
479
+ }
418
480
  }
419
481
  }
420
- if (!noEmitChanges) {
482
+ // Only emit changes if something actually changed
483
+ if (!noEmitChanges && hasChanges) {
484
+ console.log(`[Tree ${_treeId}] expandNodes triggering re-render for path: ${path}`);
421
485
  this._emitTreeChanged();
422
486
  }
487
+ perfEnd(`[${_treeId}] expandNodes`);
423
488
  return this; // Return the API object for chaining
424
489
  },
425
490
  collapseNodes: function (path, noEmitChanges = false) {
426
491
  let node = this.isFiltered ? filteredRoot : this.root;
492
+ let hasChanges = false;
427
493
  const segments = path.split(this.treePathSeparator);
428
494
  for (let i = 0; i < segments.length; i++) {
429
495
  const segment = segmentPrefix + segments[i];
430
496
  if (node.children.hasOwnProperty(segment)) {
431
497
  node = node.children[segment];
432
- node.isExpanded = false;
498
+ // Only mark as changed if actually changing from expanded to collapsed
499
+ if (node.isExpanded) {
500
+ node.isExpanded = false;
501
+ hasChanges = true;
502
+ }
433
503
  }
434
504
  }
435
- if (!noEmitChanges) {
505
+ // Only emit changes if something actually changed
506
+ if (!noEmitChanges && hasChanges) {
436
507
  this._emitTreeChanged();
437
508
  }
438
509
  return this; // Return the API object for chaining
@@ -466,6 +537,16 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
466
537
  return this.getSearchValueCallback(node);
467
538
  return '[N/A]';
468
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
+ },
469
550
  refresh() {
470
551
  this._emitTreeChanged();
471
552
  },
@@ -613,8 +694,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
613
694
  }
614
695
  }
615
696
  const newPath = newParentPath ? `${newParentPath}${this.treePathSeparator}${newSegment}` : newSegment;
616
- // Update source node's path and parentPath
617
697
  const oldPath = sourceNode.path;
698
+ // Update source node's path and parentPath
618
699
  sourceNode.path = newPath;
619
700
  sourceNode.pathSegment = newSegment;
620
701
  sourceNode.parentPath = newParentPath || null;
@@ -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,8 +68,13 @@ 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>[];
76
+ /** Flat array of all visible nodes in render order (depth-first, respects isExpanded) */
77
+ get visibleFlatNodes(): LTreeNode<T>[];
74
78
  get statistics(): {
75
79
  nodeCount: number;
76
80
  maxLevel: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/svelte-treeview",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --port 17777",
6
6
  "build": "vite build && npm run prepack",