@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.
- package/README.md +39 -4
- package/dist/components/Node.svelte +249 -12
- package/dist/components/Node.svelte.d.ts +17 -0
- package/dist/components/RenderCoordinator.svelte.d.ts +29 -0
- package/dist/components/RenderCoordinator.svelte.js +115 -0
- package/dist/components/Tree.svelte +855 -38
- package/dist/components/Tree.svelte.d.ts +160 -8
- package/dist/constants.generated.d.ts +6 -0
- package/dist/constants.generated.js +8 -0
- package/dist/global-api.d.ts +35 -0
- package/dist/global-api.js +36 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +56 -0
- package/dist/logger.js +159 -0
- package/dist/ltree/indexer.d.ts +0 -1
- package/dist/ltree/indexer.js +23 -19
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +593 -30
- package/dist/ltree/types.d.ts +62 -0
- package/dist/perf-logger.d.ts +70 -0
- package/dist/perf-logger.js +196 -0
- package/dist/styles/main.scss +437 -4
- package/dist/styles.css +329 -3
- package/dist/styles.css.map +1 -1
- package/dist/vendor/loglevel/index.d.ts +2 -0
- package/dist/vendor/loglevel/index.js +9 -0
- package/dist/vendor/loglevel/loglevel-esm.d.ts +2 -0
- package/dist/vendor/loglevel/loglevel-esm.js +349 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +7 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.js +132 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +2 -0
- package/dist/vendor/loglevel/loglevel-plugin-prefix.js +149 -0
- package/dist/vendor/loglevel/loglevel.js +357 -0
- package/dist/vendor/loglevel/prefix.d.ts +2 -0
- package/dist/vendor/loglevel/prefix.js +9 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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 &&
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
211
|
-
performance
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
});
|