@keenmate/svelte-treeview 4.4.0 → 4.5.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.
Files changed (37) hide show
  1. package/README.md +39 -4
  2. package/dist/components/Node.svelte +249 -12
  3. package/dist/components/Node.svelte.d.ts +17 -0
  4. package/dist/components/RenderCoordinator.svelte.d.ts +29 -0
  5. package/dist/components/RenderCoordinator.svelte.js +115 -0
  6. package/dist/components/Tree.svelte +855 -38
  7. package/dist/components/Tree.svelte.d.ts +160 -8
  8. package/dist/constants.generated.d.ts +6 -0
  9. package/dist/constants.generated.js +8 -0
  10. package/dist/global-api.d.ts +35 -0
  11. package/dist/global-api.js +36 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +5 -0
  14. package/dist/logger.d.ts +56 -0
  15. package/dist/logger.js +159 -0
  16. package/dist/ltree/indexer.d.ts +0 -1
  17. package/dist/ltree/indexer.js +23 -19
  18. package/dist/ltree/ltree.svelte.d.ts +1 -1
  19. package/dist/ltree/ltree.svelte.js +593 -30
  20. package/dist/ltree/types.d.ts +62 -0
  21. package/dist/perf-logger.d.ts +70 -0
  22. package/dist/perf-logger.js +196 -0
  23. package/dist/styles/main.scss +437 -4
  24. package/dist/styles.css +329 -3
  25. package/dist/styles.css.map +1 -1
  26. package/dist/vendor/loglevel/index.d.ts +2 -0
  27. package/dist/vendor/loglevel/index.js +9 -0
  28. package/dist/vendor/loglevel/loglevel-esm.d.ts +2 -0
  29. package/dist/vendor/loglevel/loglevel-esm.js +349 -0
  30. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +7 -0
  31. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.js +132 -0
  32. package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +2 -0
  33. package/dist/vendor/loglevel/loglevel-plugin-prefix.js +149 -0
  34. package/dist/vendor/loglevel/loglevel.js +357 -0
  35. package/dist/vendor/loglevel/prefix.d.ts +2 -0
  36. package/dist/vendor/loglevel/prefix.js +9 -0
  37. package/package.json +3 -2
@@ -4,7 +4,8 @@ import { isEmptyString } from '../helpers/string-helpers.js';
4
4
  import { getLevel, getParentPath, getPathSegments, getRelativePath } from '../helpers/ltree-helpers.js';
5
5
  import { createSearchIndex } from './flex.js';
6
6
  import { Indexer } from './indexer.js';
7
- export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
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
9
  let shouldCalculateParentPath = isEmptyString(_parentPathMember);
9
10
  let shouldCalculateLevel = isEmptyString(_levelMember);
10
11
  let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
@@ -58,6 +59,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
58
59
  getDisplayValueCallback: _getDisplayValueCallback,
59
60
  searchValueMember: _searchValueMember,
60
61
  getSearchValueCallback: _getSearchValueCallback,
62
+ orderMember: _orderMember,
61
63
  isSorted: false,
62
64
  // Properties for filtering
63
65
  filteredTree,
@@ -88,7 +90,11 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
88
90
  // Clear any pending indexing from previous calls
89
91
  indexer?.clearQueue();
90
92
  flatTreeNodes = [];
91
- performance.mark('conversion-start');
93
+ // Clear existing tree data - reset root children
94
+ root.children = {};
95
+ nodeCount = 0;
96
+ maxLevel = 0;
97
+ perfStart(`[${_treeId}] insertArray:conversion`);
92
98
  let mappedData = data.map((row, index) => {
93
99
  const node = createLTreeNode();
94
100
  node.treeId = _treeId;
@@ -119,10 +125,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
119
125
  node.data = row;
120
126
  return node;
121
127
  });
122
- performance.mark('conversion-end');
128
+ const conversionTime = perfEnd(`[${_treeId}] insertArray:conversion`, data.length);
123
129
  if (this.shouldDisplayDebugInformation)
124
130
  console.log(`[Tree ${_treeId}] Mapped data before sort`, mappedData);
125
- performance.mark('sort-start');
131
+ perfStart(`[${_treeId}] insertArray:sort`);
126
132
  if (!this.isSorted) {
127
133
  if (this.sortCallback)
128
134
  mappedData = this.sortCallback(mappedData);
@@ -131,13 +137,24 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
131
137
  }
132
138
  if (this.shouldDisplayDebugInformation)
133
139
  console.log(`[Tree ${_treeId}] Mapped data after sort`, mappedData);
134
- performance.mark('sort-end');
135
- performance.mark('insert-start');
140
+ const sortTime = perfEnd(`[${_treeId}] insertArray:sort`, data.length);
141
+ perfStart(`[${_treeId}] insertArray:insert`);
136
142
  const failedNodes = [];
137
143
  const itemsToIndex = [];
138
144
  let realIndex = 0; // this is used to avoid scenario, when node cannot found a parent
139
145
  let successfulCount = 0;
140
146
  let hasRenderedExpandLevel = false;
147
+ // Pre-compute the last index at expandLevel to avoid O(n²) lookup
148
+ let lastExpandLevelIndex = -1;
149
+ if (_expandLevel && !noEmitChanges) {
150
+ for (let i = mappedData.length - 1; i >= 0; i--) {
151
+ const nodeLevel = mappedData[i].level || getLevel(mappedData[i].path, this.treePathSeparator);
152
+ if (nodeLevel <= _expandLevel) {
153
+ lastExpandLevelIndex = i;
154
+ break;
155
+ }
156
+ }
157
+ }
141
158
  mappedData.forEach((node, index) => {
142
159
  const result = this.insertTreeNode(node.parentPath, node, true);
143
160
  if (result) {
@@ -156,20 +173,12 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
156
173
  realIndex++;
157
174
  }
158
175
  // Progressive rendering: emit changes when we complete expandLevel
159
- if (!noEmitChanges && !hasRenderedExpandLevel && _expandLevel && node.level && node.level <= _expandLevel) {
160
- // Check if this might be the last node at expandLevel by looking ahead
161
- const remainingNodes = mappedData.slice(index + 1);
162
- const hasMoreAtExpandLevel = remainingNodes.some(futureNode => {
163
- const futureLevel = futureNode.level || getLevel(futureNode.path, this.treePathSeparator);
164
- return futureLevel <= _expandLevel;
165
- });
166
- if (!hasMoreAtExpandLevel) {
167
- // We've processed all nodes up to expandLevel - render now!
168
- hasRenderedExpandLevel = true;
169
- this._emitTreeChanged();
170
- if (this.shouldDisplayDebugInformation) {
171
- console.log(`[Tree ${_treeId}] Progressive render: Displayed levels 1-${_expandLevel} (${successfulCount} nodes processed so far)`);
172
- }
176
+ if (!noEmitChanges && !hasRenderedExpandLevel && _expandLevel && index === lastExpandLevelIndex) {
177
+ // We've processed all nodes up to expandLevel - render now!
178
+ hasRenderedExpandLevel = true;
179
+ this._emitTreeChanged();
180
+ if (this.shouldDisplayDebugInformation) {
181
+ console.log(`[Tree ${_treeId}] Progressive render: Displayed levels 1-${_expandLevel} (${successfulCount} nodes processed so far)`);
173
182
  }
174
183
  }
175
184
  }
@@ -207,16 +216,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
207
216
  }
208
217
  }
209
218
  }
210
- performance.mark('insert-end');
211
- performance.measure('sort-duration', 'sort-start', 'sort-end');
212
- performance.measure('conversion-duration', 'conversion-start', 'conversion-end');
213
- performance.measure('insert-duration', 'insert-start', 'insert-end');
214
- let measure = performance.getEntriesByName('sort-duration')[0];
215
- console.log(`[Tree ${_treeId}] Sort took: ${measure.duration}ms`);
216
- measure = performance.getEntriesByName('conversion-duration')[0];
217
- console.log(`[Tree ${_treeId}] Conversion took: ${measure.duration}ms`);
218
- measure = performance.getEntriesByName('insert-duration')[0];
219
- console.log(`[Tree ${_treeId}] Insert took: ${measure.duration}ms`);
219
+ const insertTime = perfEnd(`[${_treeId}] insertArray:insert`, data.length);
220
+ // Log performance summary
221
+ perfSummary(_treeId, {
222
+ 'Conversion': conversionTime,
223
+ 'Sort': sortTime,
224
+ 'Insert': insertTime
225
+ }, data.length);
220
226
  return {
221
227
  successful: successfulCount,
222
228
  failed: failedNodes,
@@ -263,8 +269,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
263
269
  console.warn(`[Tree ${_treeId}] Internal search index is disabled`);
264
270
  return;
265
271
  }
272
+ perfStart(`[${_treeId}] filterNodes:search`);
266
273
  const resultIndices = searchIndex.search(_searchText, _searchOptions);
267
274
  const foundPaths = resultIndices.map((row) => flatTreeNodes[row].path);
275
+ perfEnd(`[${_treeId}] filterNodes:search`, resultIndices.length);
268
276
  if (this.shouldDisplayDebugInformation)
269
277
  console.warn(`[Tree ${_treeId}] Found indices:`, resultIndices, foundPaths);
270
278
  this.createFilteredTree(foundPaths);
@@ -287,6 +295,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
287
295
  return foundNodes;
288
296
  },
289
297
  createFilteredTree(targetPaths) {
298
+ perfStart(`[${_treeId}] createFilteredTree`);
290
299
  filteredRoot.children = {};
291
300
  filteredTree = null;
292
301
  // isFiltered = false;
@@ -350,6 +359,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
350
359
  filteredTree = rootNodes;
351
360
  this.isFiltered = true;
352
361
  this._emitTreeChanged();
362
+ perfEnd(`[${_treeId}] createFilteredTree`, rootNodes.length);
353
363
  if (this.shouldDisplayDebugInformation)
354
364
  console.log(`[Tree ${_treeId}] Created filtered tree with`, rootNodes.length, 'root nodes');
355
365
  },
@@ -360,18 +370,22 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
360
370
  this._emitTreeChanged();
361
371
  },
362
372
  expandAll(nodePath) {
373
+ perfStart(`[${_treeId}] expandAll`);
363
374
  if (isEmptyString(nodePath))
364
375
  flatTreeNodes.forEach((row) => {
365
376
  row.isExpanded = true;
366
377
  });
367
378
  this._emitTreeChanged();
379
+ perfEnd(`[${_treeId}] expandAll`, flatTreeNodes.length);
368
380
  },
369
381
  collapseAll(nodePath) {
382
+ perfStart(`[${_treeId}] collapseAll`);
370
383
  if (isEmptyString(nodePath))
371
384
  flatTreeNodes.forEach((row) => {
372
385
  row.isExpanded = false;
373
386
  });
374
387
  this._emitTreeChanged();
388
+ perfEnd(`[${_treeId}] collapseAll`, flatTreeNodes.length);
375
389
  },
376
390
  insert: function (path, data, noEmitChanges = false) {
377
391
  let node = this.root;
@@ -455,6 +469,547 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
455
469
  refresh() {
456
470
  this._emitTreeChanged();
457
471
  },
472
+ /**
473
+ * Get direct children of a node at the given path
474
+ * @param parentPath - Path to parent node (empty string for root)
475
+ * @returns Array of child nodes
476
+ */
477
+ getChildren(parentPath) {
478
+ const parent = this.getNodeByPath(parentPath);
479
+ if (!parent)
480
+ return [];
481
+ return Object.values(parent.children);
482
+ },
483
+ /**
484
+ * Get siblings of a node (including the node itself)
485
+ * @param path - Path to the node
486
+ * @returns Array of sibling nodes (nodes with same parent)
487
+ */
488
+ getSiblings(path) {
489
+ const node = this.getNodeByPath(path);
490
+ if (!node)
491
+ return [];
492
+ // Get parent and return all its children
493
+ const parentPath = node.parentPath || '';
494
+ return this.getChildren(parentPath);
495
+ },
496
+ /**
497
+ * Re-sort siblings under a parent path using sortCallback or default sort
498
+ * This reorders the children object to reflect updated order values
499
+ * @param parentPath - Path to parent node (empty string for root)
500
+ */
501
+ refreshSiblings(parentPath) {
502
+ const parent = parentPath ? this.getNodeByPath(parentPath) : root;
503
+ if (!parent)
504
+ return;
505
+ // Get current children as array
506
+ const children = Object.values(parent.children);
507
+ if (children.length === 0)
508
+ return;
509
+ // Sort using sortCallback or default sort
510
+ let sorted;
511
+ if (this.sortCallback) {
512
+ sorted = this.sortCallback(children);
513
+ }
514
+ else {
515
+ // Use a simplified sort for siblings only (all same level/parent)
516
+ sorted = [...children].sort((a, b) => {
517
+ // If orderMember is provided, use it
518
+ if (this.orderMember && a.data && b.data) {
519
+ const aOrder = a.data[this.orderMember] ?? 0;
520
+ const bOrder = b.data[this.orderMember] ?? 0;
521
+ if (aOrder !== bOrder) {
522
+ return aOrder - bOrder;
523
+ }
524
+ }
525
+ // Fall back to display value
526
+ return this.getNodeDisplayValue(a).localeCompare(this.getNodeDisplayValue(b));
527
+ });
528
+ }
529
+ // Rebuild children object in sorted order
530
+ const newChildren = {};
531
+ sorted.forEach(child => {
532
+ const segment = segmentPrefix + child.pathSegment;
533
+ newChildren[segment] = child;
534
+ });
535
+ parent.children = newChildren;
536
+ if (this.shouldDisplayDebugInformation) {
537
+ console.log(`[Tree ${_treeId}] refreshSiblings: Re-sorted ${sorted.length} children under "${parentPath || 'root'}":`, sorted.map(s => ({ path: s.path, sortOrder: s.data?.[this.orderMember] })));
538
+ }
539
+ this._emitTreeChanged();
540
+ },
541
+ /**
542
+ * Refresh a single node and optionally its descendants
543
+ * Useful after modifying node data externally
544
+ * @param path - Path to the node to refresh
545
+ */
546
+ refreshNode(path) {
547
+ // For now, just trigger a tree change
548
+ // Future optimization: only re-render the specific subtree
549
+ this._emitTreeChanged();
550
+ },
551
+ /**
552
+ * Move a node to a new location in the tree
553
+ * @param sourcePath - Path of the node to move
554
+ * @param targetPath - Path of the target node
555
+ * @param position - Where to place relative to target: 'above', 'below', or 'child'
556
+ * @returns Object with success status and optional error message
557
+ */
558
+ moveNode(sourcePath, targetPath, position) {
559
+ // Find source node
560
+ const sourceNode = this.getNodeByPath(sourcePath);
561
+ if (!sourceNode) {
562
+ return { success: false, error: `Source node not found: ${sourcePath}` };
563
+ }
564
+ // Find target node
565
+ const targetNode = this.getNodeByPath(targetPath);
566
+ if (!targetNode) {
567
+ return { success: false, error: `Target node not found: ${targetPath}` };
568
+ }
569
+ // Prevent moving a node into itself or its descendants
570
+ if (targetPath.startsWith(sourcePath + this.treePathSeparator) || targetPath === sourcePath) {
571
+ return { success: false, error: 'Cannot move a node into itself or its descendants' };
572
+ }
573
+ // Get source's current parent
574
+ const sourceParentPath = sourceNode.parentPath || '';
575
+ const sourceParent = sourceParentPath ? this.getNodeByPath(sourceParentPath) : root;
576
+ if (!sourceParent) {
577
+ return { success: false, error: `Source parent not found: ${sourceParentPath}` };
578
+ }
579
+ // Remove source from current parent
580
+ const sourceSegment = segmentPrefix + sourceNode.pathSegment;
581
+ delete sourceParent.children[sourceSegment];
582
+ // Update source parent's hasChildren
583
+ if (Object.keys(sourceParent.children).length === 0) {
584
+ sourceParent.hasChildren = false;
585
+ }
586
+ // Calculate new parent and path
587
+ let newParentPath;
588
+ let newParent;
589
+ if (position === 'child') {
590
+ // Insert as child of target
591
+ newParentPath = targetPath;
592
+ newParent = targetNode;
593
+ }
594
+ else {
595
+ // Insert as sibling (above or below)
596
+ newParentPath = targetNode.parentPath || '';
597
+ newParent = newParentPath ? this.getNodeByPath(newParentPath) : root;
598
+ }
599
+ // Generate new path segment (use source's original segment if unique)
600
+ let newSegment = sourceNode.pathSegment;
601
+ // Check if a node with this segment already exists in the new parent (excluding source node itself)
602
+ const existingChild = newParent.children[segmentPrefix + newSegment];
603
+ if (existingChild && existingChild !== sourceNode) {
604
+ // Segment collision - generate a unique segment
605
+ // Try using the source's ID first, then fall back to timestamp
606
+ const sourceId = sourceNode.id?.toString();
607
+ if (sourceId && !newParent.children[segmentPrefix + sourceId]) {
608
+ newSegment = sourceId;
609
+ }
610
+ else {
611
+ // Generate unique segment with timestamp
612
+ newSegment = `${newSegment}_${Date.now()}`;
613
+ }
614
+ }
615
+ const newPath = newParentPath ? `${newParentPath}${this.treePathSeparator}${newSegment}` : newSegment;
616
+ // Update source node's path and parentPath
617
+ const oldPath = sourceNode.path;
618
+ sourceNode.path = newPath;
619
+ sourceNode.pathSegment = newSegment;
620
+ sourceNode.parentPath = newParentPath || null;
621
+ sourceNode.level = getLevel(newPath, this.treePathSeparator);
622
+ // Update the data object's path if pathMember is defined
623
+ if (this.pathMember && sourceNode.data) {
624
+ sourceNode.data[this.pathMember] = newPath;
625
+ }
626
+ // Update all descendants' paths recursively
627
+ this._updateDescendantPaths(sourceNode, oldPath, newPath);
628
+ // Insert into new parent
629
+ newParent.children[segmentPrefix + newSegment] = sourceNode;
630
+ newParent.hasChildren = true;
631
+ // If orderMember is defined and position is above/below, calculate order
632
+ if (this.orderMember && position !== 'child' && sourceNode.data) {
633
+ const siblings = Object.values(newParent.children);
634
+ const targetOrder = targetNode.data?.[this.orderMember] ?? 0;
635
+ if (position === 'above') {
636
+ // Find order value just below target
637
+ const siblingOrders = siblings
638
+ .filter(s => s !== sourceNode && s.data?.[this.orderMember] !== undefined)
639
+ .map(s => s.data[this.orderMember])
640
+ .filter(o => o < targetOrder)
641
+ .sort((a, b) => b - a);
642
+ const belowOrder = siblingOrders[0] ?? targetOrder - 20;
643
+ sourceNode.data[this.orderMember] = Math.floor((belowOrder + targetOrder) / 2);
644
+ }
645
+ else {
646
+ // Find order value just above target
647
+ const siblingOrders = siblings
648
+ .filter(s => s !== sourceNode && s.data?.[this.orderMember] !== undefined)
649
+ .map(s => s.data[this.orderMember])
650
+ .filter(o => o > targetOrder)
651
+ .sort((a, b) => a - b);
652
+ const aboveOrder = siblingOrders[0] ?? targetOrder + 20;
653
+ sourceNode.data[this.orderMember] = Math.floor((targetOrder + aboveOrder) / 2);
654
+ }
655
+ }
656
+ // Re-sort siblings if needed
657
+ this.refreshSiblings(newParentPath);
658
+ return { success: true };
659
+ },
660
+ /**
661
+ * Helper to recursively update descendant paths after a move
662
+ */
663
+ _updateDescendantPaths(node, oldBasePath, newBasePath) {
664
+ for (const child of Object.values(node.children)) {
665
+ // Save the old path BEFORE updating, for correct recursive calculation
666
+ const oldChildPath = child.path;
667
+ // Calculate new path by replacing the old base with new base
668
+ const relativePath = oldChildPath.substring(oldBasePath.length);
669
+ const newChildPath = newBasePath + relativePath;
670
+ child.path = newChildPath;
671
+ child.parentPath = node.path;
672
+ child.level = getLevel(newChildPath, this.treePathSeparator);
673
+ // Update the data object's path if pathMember is defined
674
+ if (this.pathMember && child.data) {
675
+ child.data[this.pathMember] = newChildPath;
676
+ }
677
+ // Recurse into children using the OLD child path as base
678
+ this._updateDescendantPaths(child, oldChildPath, newChildPath);
679
+ }
680
+ },
681
+ /**
682
+ * Remove a node from the tree
683
+ * @param path - Path of the node to remove
684
+ * @param includeDescendants - If true, removes all descendants (default: true)
685
+ * @returns Object with success status and the removed node
686
+ */
687
+ removeNode(path, includeDescendants = true) {
688
+ const node = this.getNodeByPath(path);
689
+ if (!node) {
690
+ return { success: false, error: `Node not found: ${path}` };
691
+ }
692
+ // Get parent
693
+ const parentPath = node.parentPath || '';
694
+ const parent = parentPath ? this.getNodeByPath(parentPath) : root;
695
+ if (!parent) {
696
+ return { success: false, error: `Parent not found: ${parentPath}` };
697
+ }
698
+ // Remove from parent
699
+ const segment = segmentPrefix + node.pathSegment;
700
+ delete parent.children[segment];
701
+ // Update parent's hasChildren
702
+ if (Object.keys(parent.children).length === 0) {
703
+ parent.hasChildren = false;
704
+ }
705
+ // Update node count
706
+ if (includeDescendants) {
707
+ const countDescendants = (n) => {
708
+ let count = 1;
709
+ for (const child of Object.values(n.children)) {
710
+ count += countDescendants(child);
711
+ }
712
+ return count;
713
+ };
714
+ nodeCount -= countDescendants(node);
715
+ }
716
+ else {
717
+ nodeCount--;
718
+ }
719
+ this._emitTreeChanged();
720
+ return { success: true, node };
721
+ },
722
+ /**
723
+ * Add a new node to the tree
724
+ * @param parentPath - Path of the parent (empty string for root)
725
+ * @param data - The data object for the new node
726
+ * @param pathSegment - Optional path segment (auto-generated if not provided)
727
+ * @returns Object with success status and the created node
728
+ */
729
+ addNode(parentPath, data, pathSegment) {
730
+ const parent = parentPath ? this.getNodeByPath(parentPath) : root;
731
+ if (!parent && parentPath) {
732
+ return { success: false, error: `Parent not found: ${parentPath}` };
733
+ }
734
+ // Generate path segment if not provided
735
+ if (!pathSegment) {
736
+ // Use ID from data if available, otherwise generate a unique one
737
+ const id = _idMember && data ? data[_idMember] : undefined;
738
+ pathSegment = id?.toString() || `new_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
739
+ }
740
+ // Calculate full path
741
+ const newPath = parentPath ? `${parentPath}${this.treePathSeparator}${pathSegment}` : pathSegment;
742
+ // Check if path already exists
743
+ if (this.getNodeByPath(newPath)) {
744
+ return { success: false, error: `Node already exists at path: ${newPath}` };
745
+ }
746
+ // Create the node
747
+ const newNode = createLTreeNode();
748
+ newNode.treeId = _treeId;
749
+ newNode.id = _idMember && data ? data[_idMember] : undefined;
750
+ newNode.path = newPath;
751
+ newNode.pathSegment = pathSegment;
752
+ newNode.parentPath = parentPath || null;
753
+ newNode.level = getLevel(newPath, this.treePathSeparator);
754
+ newNode.data = data;
755
+ newNode.isExpanded = _expandLevel ? newNode.level <= _expandLevel : false;
756
+ newNode.hasChildren = false;
757
+ // Update path in data if pathMember is defined
758
+ if (_pathMember && data) {
759
+ data[_pathMember] = newPath;
760
+ }
761
+ // Add to parent
762
+ const targetParent = parent || root;
763
+ targetParent.children[segmentPrefix + pathSegment] = newNode;
764
+ targetParent.hasChildren = true;
765
+ // Update statistics
766
+ nodeCount++;
767
+ maxLevel = Math.max(maxLevel, newNode.level || 0);
768
+ // Add to flat tree for search indexing
769
+ flatTreeNodes.push(newNode);
770
+ // Re-sort siblings to place new node in correct position
771
+ this.refreshSiblings(parentPath);
772
+ if (this.shouldDisplayDebugInformation) {
773
+ const siblings = Object.values(targetParent.children);
774
+ console.log(`[Tree ${_treeId}] addNode: Added "${newPath}" to "${parentPath || 'root'}". Siblings after sort:`, siblings.map(s => ({ path: s.path, sortOrder: s.data?.[this.orderMember] })));
775
+ }
776
+ this._emitTreeChanged();
777
+ return { success: true, node: newNode };
778
+ },
779
+ /**
780
+ * Update an existing node's data
781
+ * @param path - Path of the node to update
782
+ * @param dataUpdates - Partial data to merge into existing node data
783
+ * @returns Object with success status and the updated node
784
+ */
785
+ updateNode(path, dataUpdates) {
786
+ const node = this.getNodeByPath(path);
787
+ if (!node) {
788
+ return { success: false, error: `Node not found: ${path}` };
789
+ }
790
+ if (!node.data) {
791
+ return { success: false, error: `Node has no data: ${path}` };
792
+ }
793
+ // Check if orderMember is being updated (will need re-sort)
794
+ const orderMemberUpdated = this.orderMember && this.orderMember in dataUpdates;
795
+ if (this.shouldDisplayDebugInformation) {
796
+ console.log(`[Tree ${_treeId}] updateNode: "${path}" updating:`, dataUpdates);
797
+ }
798
+ // Merge updates into existing data
799
+ node.data = { ...node.data, ...dataUpdates };
800
+ // Re-index for search if needed
801
+ if (indexer && _shouldUseInternalSearchIndex) {
802
+ const flatIndex = flatTreeNodes.indexOf(node);
803
+ if (flatIndex !== -1) {
804
+ indexer.addItem({ node, index: flatIndex });
805
+ }
806
+ }
807
+ // Re-sort siblings if order was updated
808
+ if (orderMemberUpdated) {
809
+ this.refreshSiblings(node.parentPath || '');
810
+ }
811
+ this._emitTreeChanged();
812
+ return { success: true, node };
813
+ },
814
+ /**
815
+ * Apply multiple changes to the tree in a single batch
816
+ * @param changes - Array of create/update/delete operations
817
+ * @returns Object with count of successful operations and array of failures
818
+ */
819
+ applyChanges(changes) {
820
+ const failures = [];
821
+ let successCount = 0;
822
+ for (let i = 0; i < changes.length; i++) {
823
+ const change = changes[i];
824
+ let result;
825
+ switch (change.operation) {
826
+ case 'create':
827
+ result = this.addNode(change.parentPath, change.data, change.pathSegment);
828
+ if (result.success) {
829
+ successCount++;
830
+ }
831
+ else {
832
+ failures.push({
833
+ index: i,
834
+ operation: change.operation,
835
+ path: change.parentPath,
836
+ error: result.error || 'Unknown error'
837
+ });
838
+ }
839
+ break;
840
+ case 'update':
841
+ result = this.updateNode(change.path, change.data);
842
+ if (result.success) {
843
+ successCount++;
844
+ }
845
+ else {
846
+ failures.push({
847
+ index: i,
848
+ operation: change.operation,
849
+ path: change.path,
850
+ error: result.error || 'Unknown error'
851
+ });
852
+ }
853
+ break;
854
+ case 'delete':
855
+ result = this.removeNode(change.path);
856
+ if (result.success) {
857
+ successCount++;
858
+ }
859
+ else {
860
+ failures.push({
861
+ index: i,
862
+ operation: change.operation,
863
+ path: change.path,
864
+ error: result.error || 'Unknown error'
865
+ });
866
+ }
867
+ break;
868
+ }
869
+ }
870
+ // Single emission after all changes
871
+ if (successCount > 0) {
872
+ this._emitTreeChanged();
873
+ }
874
+ return { successful: successCount, failed: failures };
875
+ },
876
+ /**
877
+ * Copy a node and all its descendants to a new location
878
+ * Useful for cross-tree drag-drop operations
879
+ * @param sourceNode - The node to copy (including its children)
880
+ * @param targetParentPath - Path where to insert the copy (empty string for root)
881
+ * @param transformData - Function to transform each node's data (e.g., assign new IDs)
882
+ * @param siblingPath - Optional path of sibling to position relative to
883
+ * @param position - Optional position relative to sibling ('above' or 'below')
884
+ * @returns Object with success status, the created root node, and count of nodes created
885
+ */
886
+ copyNodeWithDescendants(sourceNode, targetParentPath, transformData, siblingPath, position) {
887
+ if (!sourceNode.data) {
888
+ return { success: false, count: 0, error: 'Source node has no data' };
889
+ }
890
+ let totalCount = 0;
891
+ // Recursive helper function
892
+ const copyRecursive = (node, parentPath) => {
893
+ if (!node.data)
894
+ return null;
895
+ // Transform the data (user assigns new IDs, etc.)
896
+ const transformedData = transformData(node.data);
897
+ // Add the node
898
+ const result = this.addNode(parentPath, transformedData);
899
+ if (!result.success || !result.node) {
900
+ if (this.shouldDisplayDebugInformation) {
901
+ console.warn(`[Tree ${_treeId}] copyNodeWithDescendants: Failed to add node`, result.error);
902
+ }
903
+ return null;
904
+ }
905
+ totalCount++;
906
+ const newNode = result.node;
907
+ // Recursively copy children
908
+ if (node.children && Object.keys(node.children).length > 0) {
909
+ for (const child of Object.values(node.children)) {
910
+ copyRecursive(child, newNode.path);
911
+ }
912
+ }
913
+ return newNode;
914
+ };
915
+ // Start the recursive copy
916
+ const rootNode = copyRecursive(sourceNode, targetParentPath);
917
+ if (!rootNode) {
918
+ return { success: false, count: 0, error: 'Failed to copy root node' };
919
+ }
920
+ // Handle positioning relative to sibling if specified
921
+ if (siblingPath && position && rootNode.data) {
922
+ const siblingNode = this.getNodeByPath(siblingPath);
923
+ if (siblingNode && this.orderMember) {
924
+ // Get the parent to access siblings
925
+ const parent = targetParentPath ? this.getNodeByPath(targetParentPath) : root;
926
+ if (parent) {
927
+ const siblings = Object.values(parent.children);
928
+ const siblingOrder = siblingNode.data?.[this.orderMember] ?? 0;
929
+ if (position === 'above') {
930
+ // Find order value just below sibling
931
+ const siblingOrders = siblings
932
+ .filter(s => s !== rootNode && s.data?.[this.orderMember] !== undefined)
933
+ .map(s => s.data[this.orderMember])
934
+ .filter(o => o < siblingOrder)
935
+ .sort((a, b) => b - a);
936
+ const belowOrder = siblingOrders[0] ?? siblingOrder - 20;
937
+ rootNode.data[this.orderMember] = Math.floor((belowOrder + siblingOrder) / 2);
938
+ }
939
+ else {
940
+ // Find order value just above sibling
941
+ const siblingOrders = siblings
942
+ .filter(s => s !== rootNode && s.data?.[this.orderMember] !== undefined)
943
+ .map(s => s.data[this.orderMember])
944
+ .filter(o => o > siblingOrder)
945
+ .sort((a, b) => a - b);
946
+ const aboveOrder = siblingOrders[0] ?? siblingOrder + 20;
947
+ rootNode.data[this.orderMember] = Math.floor((siblingOrder + aboveOrder) / 2);
948
+ }
949
+ // Re-sort siblings
950
+ this.refreshSiblings(targetParentPath);
951
+ }
952
+ }
953
+ }
954
+ if (this.shouldDisplayDebugInformation) {
955
+ console.log(`[Tree ${_treeId}] copyNodeWithDescendants: Copied ${totalCount} nodes to "${targetParentPath}"${siblingPath ? ` ${position} "${siblingPath}"` : ''}`);
956
+ }
957
+ return { success: true, rootNode, count: totalCount };
958
+ },
959
+ /**
960
+ * Get paths of all expanded nodes
961
+ * Useful for saving expanded state before a full redraw
962
+ * @returns Array of paths that are currently expanded
963
+ */
964
+ getExpandedPaths() {
965
+ const paths = [];
966
+ const traverse = (node) => {
967
+ if (node.isExpanded && node.path) {
968
+ paths.push(node.path);
969
+ }
970
+ for (const child of Object.values(node.children)) {
971
+ traverse(child);
972
+ }
973
+ };
974
+ traverse(root);
975
+ return paths;
976
+ },
977
+ /**
978
+ * Set expanded state for given paths
979
+ * Useful for restoring expanded state after a full redraw
980
+ * @param paths - Array of paths to expand (all others will be collapsed)
981
+ */
982
+ setExpandedPaths(paths) {
983
+ const pathSet = new Set(paths);
984
+ const traverse = (node) => {
985
+ if (node.path) {
986
+ node.isExpanded = pathSet.has(node.path);
987
+ }
988
+ for (const child of Object.values(node.children)) {
989
+ traverse(child);
990
+ }
991
+ };
992
+ traverse(root);
993
+ this._emitTreeChanged();
994
+ },
995
+ /**
996
+ * Extract all node data as a flat array
997
+ * Useful for saving the entire tree state to a database
998
+ * @returns Array of all node data objects
999
+ */
1000
+ getAllData() {
1001
+ const result = [];
1002
+ const traverse = (node) => {
1003
+ if (node.data) {
1004
+ result.push(node.data);
1005
+ }
1006
+ for (const child of Object.values(node.children)) {
1007
+ traverse(child);
1008
+ }
1009
+ };
1010
+ traverse(root);
1011
+ return result;
1012
+ },
458
1013
  _defaultSort: function (self, items) {
459
1014
  return items.sort((a, b) => {
460
1015
  // First, sort by level (shallower levels first)
@@ -471,6 +1026,14 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
471
1026
  return 1;
472
1027
  return a.parentPath.localeCompare(b.parentPath);
473
1028
  }
1029
+ // If orderMember is provided, use it for sibling ordering
1030
+ if (self.orderMember && a.data && b.data) {
1031
+ const aOrder = a.data[self.orderMember] ?? 0;
1032
+ const bOrder = b.data[self.orderMember] ?? 0;
1033
+ if (aOrder !== bOrder) {
1034
+ return aOrder - bOrder;
1035
+ }
1036
+ }
474
1037
  // Finally sort by display value
475
1038
  return self.getNodeDisplayValue(a).localeCompare(self.getNodeDisplayValue(b));
476
1039
  });