@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.
@@ -45,11 +45,22 @@ declare function $$render<T>(): {
45
45
  shouldDisplayContextMenuInDebugMode?: boolean;
46
46
  isLoading?: boolean;
47
47
  progressiveRender?: boolean;
48
- renderBatchSize?: number;
48
+ initialBatchSize?: number;
49
+ maxBatchSize?: number;
49
50
  isRendering?: boolean;
50
51
  onRenderStart?: () => void;
51
52
  onRenderProgress?: (stats: RenderStats) => void;
52
53
  onRenderComplete?: (stats: RenderStats) => void;
54
+ /**
55
+ * Use flat/centralized rendering instead of recursive node rendering.
56
+ * This significantly improves performance for large trees by:
57
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
58
+ * - Using a single flat loop instead of recursive component instantiation
59
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
60
+ */
61
+ useFlatRendering?: boolean;
62
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
63
+ flatIndentSize?: string;
53
64
  dragDropMode?: DragDropMode;
54
65
  dropZoneMode?: "floating" | "glow";
55
66
  dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
@@ -64,11 +75,15 @@ declare function $$render<T>(): {
64
75
  * Called before a drop is processed. Return false to cancel the drop.
65
76
  * Return { position, operation } to override the drop position or operation.
66
77
  * Return true or undefined to proceed normally.
78
+ * Can be async - return a Promise to show dialogs or perform async validation.
67
79
  */
68
80
  beforeDropCallback?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | {
69
81
  position?: DropPosition;
70
82
  operation?: DropOperation;
71
- } | void;
83
+ } | void | Promise<boolean | {
84
+ position?: DropPosition;
85
+ operation?: DropOperation;
86
+ } | void>;
72
87
  onNodeDrop?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void;
73
88
  contextMenuCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[];
74
89
  bodyClass?: string | null | undefined;
@@ -125,7 +140,10 @@ declare function $$render<T>(): {
125
140
  getAllData: () => T[];
126
141
  closeContextMenu: () => void;
127
142
  scrollToPath: (path: string, options?: {
143
+ /** Expand ancestors to make the node visible (default: true) */
128
144
  expand?: boolean;
145
+ /** Also expand the target node itself to show its children (default: false for performance) */
146
+ expandTarget?: boolean;
129
147
  highlight?: boolean;
130
148
  scrollOptions?: ScrollIntoViewOptions;
131
149
  /** Scroll only within the nearest scrollable container (prevents page scroll) */
@@ -172,11 +190,22 @@ declare function $$render<T>(): {
172
190
  shouldDisplayContextMenuInDebugMode?: boolean;
173
191
  isLoading?: boolean;
174
192
  progressiveRender?: boolean;
175
- renderBatchSize?: number;
193
+ initialBatchSize?: number;
194
+ maxBatchSize?: number;
176
195
  isRendering?: boolean;
177
196
  onRenderStart?: () => void;
178
197
  onRenderProgress?: (stats: RenderStats) => void;
179
198
  onRenderComplete?: (stats: RenderStats) => void;
199
+ /**
200
+ * Use flat/centralized rendering instead of recursive node rendering.
201
+ * This significantly improves performance for large trees by:
202
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
203
+ * - Using a single flat loop instead of recursive component instantiation
204
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
205
+ */
206
+ useFlatRendering?: boolean;
207
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
208
+ flatIndentSize?: string;
180
209
  dragDropMode?: DragDropMode;
181
210
  dropZoneMode?: "floating" | "glow";
182
211
  dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
@@ -191,11 +220,15 @@ declare function $$render<T>(): {
191
220
  * Called before a drop is processed. Return false to cancel the drop.
192
221
  * Return { position, operation } to override the drop position or operation.
193
222
  * Return true or undefined to proceed normally.
223
+ * Can be async - return a Promise to show dialogs or perform async validation.
194
224
  */
195
225
  beforeDropCallback?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | {
196
226
  position?: DropPosition;
197
227
  operation?: DropOperation;
198
- } | void;
228
+ } | void | Promise<boolean | {
229
+ position?: DropPosition;
230
+ operation?: DropOperation;
231
+ } | void>;
199
232
  onNodeDrop?: (dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void;
200
233
  contextMenuCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[];
201
234
  bodyClass?: string | null | undefined;
@@ -262,7 +295,10 @@ declare class __sveltets_Render<T> {
262
295
  getAllData: () => T[];
263
296
  closeContextMenu: () => void;
264
297
  scrollToPath: (path: string, options?: {
298
+ /** Expand ancestors to make the node visible (default: true) */
265
299
  expand?: boolean;
300
+ /** Also expand the target node itself to show its children (default: false for performance) */
301
+ expandTarget?: boolean;
266
302
  highlight?: boolean;
267
303
  scrollOptions?: ScrollIntoViewOptions;
268
304
  /** Scroll only within the nearest scrollable container (prevents page scroll) */
@@ -309,11 +345,22 @@ declare class __sveltets_Render<T> {
309
345
  shouldDisplayContextMenuInDebugMode?: boolean;
310
346
  isLoading?: boolean;
311
347
  progressiveRender?: boolean;
312
- renderBatchSize?: number;
348
+ initialBatchSize?: number;
349
+ maxBatchSize?: number;
313
350
  isRendering?: boolean;
314
351
  onRenderStart?: (() => void) | undefined;
315
352
  onRenderProgress?: ((stats: RenderStats) => void) | undefined;
316
353
  onRenderComplete?: ((stats: RenderStats) => void) | undefined;
354
+ /**
355
+ * Use flat/centralized rendering instead of recursive node rendering.
356
+ * This significantly improves performance for large trees by:
357
+ * - Removing the {#key changeTracker} block that destroys all nodes on any change
358
+ * - Using a single flat loop instead of recursive component instantiation
359
+ * - Allowing Svelte's keyed {#each} to efficiently diff only changed nodes
360
+ */
361
+ useFlatRendering?: boolean;
362
+ /** Indentation per level in flat rendering mode (CSS value, default: '1.5rem') */
363
+ flatIndentSize?: string;
317
364
  dragDropMode?: DragDropMode;
318
365
  dropZoneMode?: "floating" | "glow";
319
366
  dropZoneLayout?: "around" | "above" | "below" | "wave" | "wave2";
@@ -328,11 +375,15 @@ declare class __sveltets_Render<T> {
328
375
  * Called before a drop is processed. Return false to cancel the drop.
329
376
  * Return { position, operation } to override the drop position or operation.
330
377
  * Return true or undefined to proceed normally.
378
+ * Can be async - return a Promise to show dialogs or perform async validation.
331
379
  */
332
380
  beforeDropCallback?: ((dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => boolean | void | {
333
381
  position?: DropPosition;
334
382
  operation?: DropOperation;
335
- }) | undefined;
383
+ } | Promise<boolean | void | {
384
+ position?: DropPosition;
385
+ operation?: DropOperation;
386
+ }>) | undefined;
336
387
  onNodeDrop?: ((dropNode: LTreeNode<T> | null, draggedNode: LTreeNode<T>, position: DropPosition, event: DragEvent | TouchEvent, operation: DropOperation) => void) | undefined;
337
388
  contextMenuCallback?: ((node: LTreeNode<T>, closeMenuCallback: () => void) => ContextMenuItem[]) | undefined;
338
389
  bodyClass?: string | null | undefined;
@@ -345,7 +396,7 @@ declare class __sveltets_Render<T> {
345
396
  scrollHighlightClass?: string | null | undefined;
346
397
  contextMenuXOffset?: number | null | undefined;
347
398
  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;
399
+ }, "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
400
  };
350
401
  }
351
402
  interface $$IsomorphicComponent {
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "4.5.0";
1
+ export declare const VERSION = "4.6.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.6.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";
@@ -31,6 +31,9 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
31
31
  let flatTreeNodes = [];
32
32
  let filteredTree = null;
33
33
  let isFiltered = false;
34
+ // Cache for visibleFlatNodes - only recompute when tree changes
35
+ let cachedVisibleFlatNodes = [];
36
+ let cachedVisibleFlatNodesTracker = null;
34
37
  // Async search indexing infrastructure
35
38
  let indexer = null;
36
39
  // Initialize indexer when search index is available
@@ -74,6 +77,54 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
74
77
  }
75
78
  return Object.values(root.children);
76
79
  },
80
+ /**
81
+ * Returns a flat array of all visible nodes in render order (depth-first).
82
+ * A node is visible if all its ancestors are expanded.
83
+ * This is optimized for flat/centralized rendering without recursion.
84
+ *
85
+ * Note: This getter depends on changeTracker to ensure reactivity when
86
+ * nodes are expanded/collapsed or the tree structure changes.
87
+ * Results are cached to avoid recomputation on repeated access.
88
+ */
89
+ get visibleFlatNodes() {
90
+ // Explicitly read changeTracker to create reactive dependency
91
+ const _tracker = changeTracker;
92
+ // Return cached result if changeTracker hasn't changed
93
+ if (_tracker === cachedVisibleFlatNodesTracker && cachedVisibleFlatNodes.length > 0) {
94
+ // console.log(`[visibleFlatNodes] Cache HIT - returning ${cachedVisibleFlatNodes.length} nodes`);
95
+ return cachedVisibleFlatNodes;
96
+ }
97
+ const computeStart = performance.now();
98
+ const startRoot = this.isFiltered ? filteredRoot : root;
99
+ if (!startRoot?.children || !_tracker) {
100
+ cachedVisibleFlatNodes = [];
101
+ cachedVisibleFlatNodesTracker = _tracker;
102
+ return cachedVisibleFlatNodes;
103
+ }
104
+ const result = [];
105
+ const self = this;
106
+ function traverse(node) {
107
+ // Get children and optionally sort them
108
+ let children = Object.values(node.children);
109
+ if (self.isSorted && children.length > 0) {
110
+ children = self.sortCallback(children);
111
+ }
112
+ for (const child of children) {
113
+ result.push(child);
114
+ // Only traverse into children if this node is expanded
115
+ if (child.isExpanded && child.hasChildren) {
116
+ traverse(child);
117
+ }
118
+ }
119
+ }
120
+ traverse(startRoot);
121
+ const computeTime = performance.now() - computeStart;
122
+ console.log(`[visibleFlatNodes] Computed ${result.length} nodes in ${computeTime.toFixed(2)}ms`);
123
+ // Cache the result
124
+ cachedVisibleFlatNodes = result;
125
+ cachedVisibleFlatNodesTracker = _tracker;
126
+ return result;
127
+ },
77
128
  get statistics() {
78
129
  const filteredNodeCount = isFiltered ? filteredTree?.length || 0 : 0;
79
130
  const indexerStatus = indexer?.getStatus() || { isProcessing: false, queueSize: 0 };
@@ -408,31 +459,46 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
408
459
  }
409
460
  },
410
461
  expandNodes: function (path, noEmitChanges = false) {
462
+ perfStart(`[${_treeId}] expandNodes`);
411
463
  let node = this.isFiltered ? filteredRoot : root;
464
+ let hasChanges = false;
412
465
  const segments = path.split(this.treePathSeparator);
413
466
  for (let i = 0; i < segments.length; i++) {
414
467
  const segment = segmentPrefix + segments[i];
415
468
  if (node.children.hasOwnProperty(segment)) {
416
469
  node = node.children[segment];
417
- node.isExpanded = true;
470
+ // Only mark as changed if actually changing from collapsed to expanded
471
+ if (!node.isExpanded) {
472
+ node.isExpanded = true;
473
+ hasChanges = true;
474
+ }
418
475
  }
419
476
  }
420
- if (!noEmitChanges) {
477
+ // Only emit changes if something actually changed
478
+ if (!noEmitChanges && hasChanges) {
479
+ console.log(`[Tree ${_treeId}] expandNodes triggering re-render for path: ${path}`);
421
480
  this._emitTreeChanged();
422
481
  }
482
+ perfEnd(`[${_treeId}] expandNodes`);
423
483
  return this; // Return the API object for chaining
424
484
  },
425
485
  collapseNodes: function (path, noEmitChanges = false) {
426
486
  let node = this.isFiltered ? filteredRoot : this.root;
487
+ let hasChanges = false;
427
488
  const segments = path.split(this.treePathSeparator);
428
489
  for (let i = 0; i < segments.length; i++) {
429
490
  const segment = segmentPrefix + segments[i];
430
491
  if (node.children.hasOwnProperty(segment)) {
431
492
  node = node.children[segment];
432
- node.isExpanded = false;
493
+ // Only mark as changed if actually changing from expanded to collapsed
494
+ if (node.isExpanded) {
495
+ node.isExpanded = false;
496
+ hasChanges = true;
497
+ }
433
498
  }
434
499
  }
435
- if (!noEmitChanges) {
500
+ // Only emit changes if something actually changed
501
+ if (!noEmitChanges && hasChanges) {
436
502
  this._emitTreeChanged();
437
503
  }
438
504
  return this; // Return the API object for chaining
@@ -613,8 +679,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
613
679
  }
614
680
  }
615
681
  const newPath = newParentPath ? `${newParentPath}${this.treePathSeparator}${newSegment}` : newSegment;
616
- // Update source node's path and parentPath
617
682
  const oldPath = sourceNode.path;
683
+ // Update source node's path and parentPath
618
684
  sourceNode.path = newPath;
619
685
  sourceNode.pathSegment = newSegment;
620
686
  sourceNode.parentPath = newParentPath || null;
@@ -71,6 +71,8 @@ export interface Ltree<T> {
71
71
  isDropAllowedMember: string | null | undefined;
72
72
  shouldDisplayDebugInformation: boolean | null | undefined;
73
73
  get tree(): LTreeNode<T>[];
74
+ /** Flat array of all visible nodes in render order (depth-first, respects isExpanded) */
75
+ get visibleFlatNodes(): LTreeNode<T>[];
74
76
  get statistics(): {
75
77
  nodeCount: number;
76
78
  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.6.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --port 17777",
6
6
  "build": "vite build && npm run prepack",