@keenmate/svelte-treeview 4.8.0 → 5.0.0-rc02
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 +106 -117
- package/ai/INDEX.txt +310 -0
- package/ai/advanced-patterns.txt +506 -0
- package/ai/basic-setup.txt +336 -0
- package/ai/context-menu.txt +349 -0
- package/ai/data-handling.txt +390 -0
- package/ai/drag-drop.txt +397 -0
- package/ai/events-callbacks.txt +382 -0
- package/ai/import-patterns.txt +271 -0
- package/ai/performance.txt +349 -0
- package/ai/search-features.txt +359 -0
- package/ai/styling-theming.txt +354 -0
- package/ai/tree-editing.txt +423 -0
- package/ai/typescript-types.txt +357 -0
- package/dist/components/Node.svelte +47 -40
- package/dist/components/Node.svelte.d.ts +1 -1
- package/dist/components/Tree.svelte +384 -1479
- package/dist/components/Tree.svelte.d.ts +30 -28
- package/dist/components/TreeProvider.svelte +28 -0
- package/dist/components/TreeProvider.svelte.d.ts +28 -0
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/core/TreeController.svelte.d.ts +353 -0
- package/dist/core/TreeController.svelte.js +1503 -0
- package/dist/core/createTreeController.d.ts +9 -0
- package/dist/core/createTreeController.js +11 -0
- package/dist/global-api.d.ts +1 -1
- package/dist/global-api.js +5 -5
- package/dist/index.d.ts +10 -6
- package/dist/index.js +7 -3
- package/dist/logger.d.ts +7 -6
- package/dist/logger.js +0 -2
- package/dist/ltree/indexer.js +2 -4
- package/dist/ltree/ltree-node.svelte.d.ts +2 -1
- package/dist/ltree/ltree-node.svelte.js +1 -0
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +168 -175
- package/dist/ltree/types.d.ts +12 -8
- package/dist/perf-logger.d.ts +2 -1
- package/dist/perf-logger.js +0 -2
- package/dist/styles/main.scss +78 -78
- package/dist/styles.css +41 -41
- package/dist/styles.css.map +1 -1
- package/dist/vendor/loglevel/index.d.ts +55 -2
- package/dist/vendor/loglevel/prefix.d.ts +23 -2
- package/package.json +96 -95
- package/dist/ltree/ltree-demo.d.ts +0 -2
- package/dist/ltree/ltree-demo.js +0 -90
- package/dist/vendor/loglevel/loglevel-esm.d.ts +0 -2
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +0 -7
- package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +0 -2
|
@@ -5,7 +5,11 @@ 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
|
-
|
|
8
|
+
/** Helper to safely access a property on a generic data item using a string member name */
|
|
9
|
+
function getField(item, member) {
|
|
10
|
+
return item[member];
|
|
11
|
+
}
|
|
12
|
+
export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _getIsDraggableCallback, _isDropAllowedMember, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _isCollapsibleMember, _getIsCollapsibleCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
|
|
9
13
|
let shouldCalculateParentPath = isEmptyString(_parentPathMember);
|
|
10
14
|
let shouldCalculateLevel = isEmptyString(_levelMember);
|
|
11
15
|
let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
|
|
@@ -14,6 +18,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
14
18
|
let shouldCalculateIsDraggable = isEmptyString(_isDraggableMember);
|
|
15
19
|
let shouldCalculateIsDropAllowed = isEmptyString(_isDropAllowedMember);
|
|
16
20
|
let shouldCalculateAllowedDropPositions = isEmptyString(_allowedDropPositionsMember);
|
|
21
|
+
let shouldCalculateIsCollapsible = isEmptyString(_isCollapsibleMember);
|
|
17
22
|
let shouldCalculateDisplayValue = isEmptyString(_displayValueMember);
|
|
18
23
|
let shouldCalculateSearchValue = isEmptyString(_searchValueMember);
|
|
19
24
|
// this is absolutely crucial to keep order of sorted items. Segments are just numbers and numbers as properties are always sorted
|
|
@@ -41,33 +46,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
41
46
|
if (_shouldUseInternalSearchIndex && searchIndex) {
|
|
42
47
|
indexer = new Indexer(_treeId || 'unknown', searchIndex, shouldCalculateSearchValue, _searchValueMember, _getSearchValueCallback, _indexerBatchSize || 25, // batch size with fallback
|
|
43
48
|
_indexerTimeout || 50, // timeout with fallback
|
|
44
|
-
opts
|
|
45
|
-
}
|
|
46
|
-
// Per-node reactive signals for O(1) fine-grained updates.
|
|
47
|
-
// Each NodeSignal is an independent $state — bumping one only notifies
|
|
48
|
-
// the single Node component that reads it. Stored in a plain Map
|
|
49
|
-
// (not $state) to avoid proxy overhead.
|
|
50
|
-
class NodeSignal {
|
|
51
|
-
value = $state(0);
|
|
52
|
-
bump() { this.value++; }
|
|
53
|
-
}
|
|
54
|
-
let nodeSignals = new Map();
|
|
55
|
-
function _bumpRev(node) {
|
|
56
|
-
node._rev = (node._rev || 0) + 1;
|
|
57
|
-
}
|
|
58
|
-
function _bumpNodeRev(node) {
|
|
59
|
-
// Signal only — do NOT bump _rev. _rev is part of the {#each} key,
|
|
60
|
-
// so bumping it would cause {#each} to detect a key change and
|
|
61
|
-
// destroy/recreate the component (O(n) diffing). We only want the
|
|
62
|
-
// per-node signal to fire, which is O(1).
|
|
63
|
-
const signal = nodeSignals.get(String(node.id));
|
|
64
|
-
if (signal)
|
|
65
|
-
signal.bump();
|
|
49
|
+
opts?.shouldDisplayDebugInformation ?? false);
|
|
66
50
|
}
|
|
67
51
|
return {
|
|
68
52
|
// Properties
|
|
69
53
|
treePathSeparator: _treePathSeparator || '.',
|
|
70
54
|
root,
|
|
55
|
+
filteredRoot,
|
|
71
56
|
get changeTracker() {
|
|
72
57
|
return changeTracker;
|
|
73
58
|
},
|
|
@@ -78,6 +63,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
78
63
|
isExpandedMember: _isExpandedMember,
|
|
79
64
|
isSelectableMember: _isSelectableMember,
|
|
80
65
|
isDraggableMember: _isDraggableMember,
|
|
66
|
+
getIsDraggableCallback: _getIsDraggableCallback,
|
|
81
67
|
isDropAllowedMember: _isDropAllowedMember,
|
|
82
68
|
allowedDropPositionsMember: _allowedDropPositionsMember,
|
|
83
69
|
hasChildrenMember: _hasChildrenMember,
|
|
@@ -86,8 +72,11 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
86
72
|
searchValueMember: _searchValueMember,
|
|
87
73
|
getSearchValueCallback: _getSearchValueCallback,
|
|
88
74
|
getAllowedDropPositionsCallback: _getAllowedDropPositionsCallback,
|
|
75
|
+
isCollapsibleMember: _isCollapsibleMember,
|
|
76
|
+
getIsCollapsibleCallback: _getIsCollapsibleCallback,
|
|
89
77
|
orderMember: _orderMember,
|
|
90
|
-
isSorted:
|
|
78
|
+
isSorted: false,
|
|
79
|
+
shouldDisplayDebugInformation: opts?.shouldDisplayDebugInformation ?? false,
|
|
91
80
|
// Properties for filtering
|
|
92
81
|
filteredTree,
|
|
93
82
|
isFiltered,
|
|
@@ -128,7 +117,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
128
117
|
const result = [];
|
|
129
118
|
const self = this;
|
|
130
119
|
function traverse(node) {
|
|
131
|
-
// Get children in natural tree key order (same as recursive mode)
|
|
120
|
+
// Get children in natural tree key order (same as recursive mode).
|
|
121
|
+
// Sorting was already applied at insertion time in insertArray().
|
|
132
122
|
const children = Object.values(node.children);
|
|
133
123
|
for (const child of children) {
|
|
134
124
|
result.push(child);
|
|
@@ -157,12 +147,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
157
147
|
},
|
|
158
148
|
insertArray: function (data, noEmitChanges = false) {
|
|
159
149
|
data = data || [];
|
|
160
|
-
// Create per-node signals for O(1) reactive updates
|
|
161
|
-
nodeSignals = new Map();
|
|
162
|
-
for (let i = 0; i < data.length; i++) {
|
|
163
|
-
const id = _idMember ? data[i][_idMember] : i;
|
|
164
|
-
nodeSignals.set(String(id), new NodeSignal());
|
|
165
|
-
}
|
|
166
150
|
// Clear any pending indexing from previous calls
|
|
167
151
|
indexer?.clearQueue();
|
|
168
152
|
flatTreeNodes = [];
|
|
@@ -171,38 +155,56 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
171
155
|
nodeCount = 0;
|
|
172
156
|
maxLevel = 0;
|
|
173
157
|
perfStart(`[${_treeId}] insertArray:conversion`);
|
|
158
|
+
const conversionFailures = [];
|
|
174
159
|
let mappedData = data.map((row, index) => {
|
|
175
160
|
const node = createLTreeNode();
|
|
176
|
-
node.treeId = _treeId;
|
|
177
|
-
node.id = _idMember ? row
|
|
178
|
-
|
|
161
|
+
node.treeId = _treeId || '';
|
|
162
|
+
node.id = _idMember ? getField(row, _idMember) : undefined;
|
|
163
|
+
const rawPath = _pathMember ? getField(row, _pathMember) : undefined;
|
|
164
|
+
// Validate path - must be a non-empty string
|
|
165
|
+
if (rawPath == null || rawPath === '' || typeof rawPath !== 'string') {
|
|
166
|
+
node.path = '';
|
|
167
|
+
node.data = row;
|
|
168
|
+
const pathDesc = rawPath === '' ? 'empty string'
|
|
169
|
+
: rawPath == null ? 'undefined/null'
|
|
170
|
+
: `non-string (${typeof rawPath})`;
|
|
171
|
+
conversionFailures.push({
|
|
172
|
+
node,
|
|
173
|
+
originalData: row,
|
|
174
|
+
error: `Item at index ${index} has invalid path (${pathDesc}). Check that pathMember="${_pathMember}" matches your data. First item keys: ${index === 0 ? JSON.stringify(Object.keys(row)) : '(see index 0)'}`
|
|
175
|
+
});
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
node.path = rawPath;
|
|
179
179
|
if (shouldCalculateParentPath) {
|
|
180
180
|
node.parentPath = getParentPath(node.path, this.treePathSeparator);
|
|
181
181
|
}
|
|
182
182
|
else
|
|
183
|
-
node.parentPath = row
|
|
184
|
-
node.pathSegment = getPathSegments(getRelativePath(node.path, node.parentPath, this.treePathSeparator), 0, 1, this.treePathSeparator);
|
|
183
|
+
node.parentPath = getField(row, _parentPathMember);
|
|
184
|
+
node.pathSegment = getPathSegments(getRelativePath(node.path, node.parentPath ?? '', this.treePathSeparator), 0, 1, this.treePathSeparator);
|
|
185
185
|
if (!shouldCalculateLevel)
|
|
186
|
-
node.level = row
|
|
186
|
+
node.level = getField(row, _levelMember);
|
|
187
187
|
else
|
|
188
188
|
node.level = getLevel(node.path, this.treePathSeparator);
|
|
189
189
|
if (!shouldCalculateIsExpanded)
|
|
190
|
-
node.isExpanded = row
|
|
190
|
+
node.isExpanded = getField(row, _isExpandedMember);
|
|
191
191
|
else if (_expandLevel)
|
|
192
|
-
node.isExpanded = node.level <= _expandLevel;
|
|
192
|
+
node.isExpanded = (node.level ?? 0) <= _expandLevel;
|
|
193
193
|
if (!shouldCalculateIsSelectable)
|
|
194
|
-
node.isSelectable = row
|
|
194
|
+
node.isSelectable = getField(row, _isSelectableMember);
|
|
195
195
|
if (!shouldCalculateIsDraggable)
|
|
196
|
-
node.isDraggable = row
|
|
196
|
+
node.isDraggable = getField(row, _isDraggableMember);
|
|
197
|
+
if (!shouldCalculateIsCollapsible)
|
|
198
|
+
node.isCollapsible = getField(row, _isCollapsibleMember);
|
|
197
199
|
if (!shouldCalculateIsDropAllowed)
|
|
198
|
-
node.isDropAllowed = row
|
|
200
|
+
node.isDropAllowed = getField(row, _isDropAllowedMember);
|
|
199
201
|
if (!shouldCalculateAllowedDropPositions)
|
|
200
|
-
node.allowedDropPositions = row
|
|
202
|
+
node.allowedDropPositions = getField(row, _allowedDropPositionsMember);
|
|
201
203
|
if (!shouldCalculateHasChildren)
|
|
202
|
-
node.hasChildren = row
|
|
204
|
+
node.hasChildren = getField(row, _hasChildrenMember);
|
|
203
205
|
node.data = row;
|
|
204
206
|
return node;
|
|
205
|
-
});
|
|
207
|
+
}).filter((node) => node !== null);
|
|
206
208
|
const conversionTime = perfEnd(`[${_treeId}] insertArray:conversion`, data.length);
|
|
207
209
|
perfStart(`[${_treeId}] insertArray:sort`);
|
|
208
210
|
if (!this.isSorted) {
|
|
@@ -213,7 +215,12 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
213
215
|
}
|
|
214
216
|
const sortTime = perfEnd(`[${_treeId}] insertArray:sort`, data.length);
|
|
215
217
|
perfStart(`[${_treeId}] insertArray:insert`);
|
|
216
|
-
const failedNodes = [];
|
|
218
|
+
const failedNodes = [...conversionFailures];
|
|
219
|
+
// Warn early about data mapping issues (most common user error)
|
|
220
|
+
if (conversionFailures.length > 0) {
|
|
221
|
+
console.warn(`[Tree ${_treeId}] ${conversionFailures.length} of ${data.length} items have invalid paths (pathMember="${_pathMember}"). These items will be skipped.\n` +
|
|
222
|
+
`First failure: ${conversionFailures[0].error}`);
|
|
223
|
+
}
|
|
217
224
|
const itemsToIndex = [];
|
|
218
225
|
let realIndex = 0; // this is used to avoid scenario, when node cannot found a parent
|
|
219
226
|
let successfulCount = 0;
|
|
@@ -230,7 +237,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
mappedData.forEach((node, index) => {
|
|
233
|
-
const result = this.insertTreeNode(node.parentPath, node, true);
|
|
240
|
+
const result = this.insertTreeNode(node.parentPath ?? '', node, true);
|
|
234
241
|
if (result) {
|
|
235
242
|
failedNodes.push({
|
|
236
243
|
node: node,
|
|
@@ -276,7 +283,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
276
283
|
}
|
|
277
284
|
const insertTime = perfEnd(`[${_treeId}] insertArray:insert`, data.length);
|
|
278
285
|
// Log performance summary
|
|
279
|
-
perfSummary(_treeId, {
|
|
286
|
+
perfSummary(_treeId || 'unknown', {
|
|
280
287
|
'Conversion': conversionTime,
|
|
281
288
|
'Sort': sortTime,
|
|
282
289
|
'Insert': insertTime
|
|
@@ -411,27 +418,44 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
411
418
|
},
|
|
412
419
|
expandAll(nodePath) {
|
|
413
420
|
perfStart(`[${_treeId}] expandAll`);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
+
function setExpandedRecursive(node, value) {
|
|
422
|
+
node.isExpanded = value;
|
|
423
|
+
for (const key in node.children) {
|
|
424
|
+
setExpandedRecursive(node.children[key], value);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (isEmptyString(nodePath)) {
|
|
428
|
+
setExpandedRecursive(root, true);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
const target = this.getNodeByPath(nodePath);
|
|
432
|
+
if (target)
|
|
433
|
+
setExpandedRecursive(target, true);
|
|
434
|
+
}
|
|
421
435
|
this._emitTreeChanged();
|
|
422
|
-
perfEnd(`[${_treeId}] expandAll
|
|
436
|
+
perfEnd(`[${_treeId}] expandAll`);
|
|
423
437
|
},
|
|
424
438
|
collapseAll(nodePath) {
|
|
425
439
|
perfStart(`[${_treeId}] collapseAll`);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
440
|
+
const self = this;
|
|
441
|
+
function collapseRecursive(node) {
|
|
442
|
+
if (node.isExpanded && self.getNodeIsCollapsible(node)) {
|
|
443
|
+
node.isExpanded = false;
|
|
444
|
+
}
|
|
445
|
+
for (const key in node.children) {
|
|
446
|
+
collapseRecursive(node.children[key]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (isEmptyString(nodePath)) {
|
|
450
|
+
collapseRecursive(root);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const target = this.getNodeByPath(nodePath);
|
|
454
|
+
if (target)
|
|
455
|
+
collapseRecursive(target);
|
|
456
|
+
}
|
|
433
457
|
this._emitTreeChanged();
|
|
434
|
-
perfEnd(`[${_treeId}] collapseAll
|
|
458
|
+
perfEnd(`[${_treeId}] collapseAll`);
|
|
435
459
|
},
|
|
436
460
|
insert: function (path, data, noEmitChanges = false) {
|
|
437
461
|
let node = this.root;
|
|
@@ -465,7 +489,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
465
489
|
// Only mark as changed if actually changing from collapsed to expanded
|
|
466
490
|
if (!node.isExpanded) {
|
|
467
491
|
node.isExpanded = true;
|
|
468
|
-
_bumpRev(node);
|
|
469
492
|
hasChanges = true;
|
|
470
493
|
}
|
|
471
494
|
}
|
|
@@ -485,14 +508,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
485
508
|
const segment = segmentPrefix + segments[i];
|
|
486
509
|
if (node.children.hasOwnProperty(segment)) {
|
|
487
510
|
node = node.children[segment];
|
|
488
|
-
// Only mark as changed if actually changing from expanded to collapsed
|
|
489
|
-
if (node.isExpanded) {
|
|
490
|
-
node.isExpanded = false;
|
|
491
|
-
_bumpRev(node);
|
|
492
|
-
hasChanges = true;
|
|
493
|
-
}
|
|
494
511
|
}
|
|
495
512
|
}
|
|
513
|
+
// Only collapse the target node, not ancestors
|
|
514
|
+
if (node.isExpanded) {
|
|
515
|
+
node.isExpanded = false;
|
|
516
|
+
hasChanges = true;
|
|
517
|
+
}
|
|
496
518
|
// Only emit changes if something actually changed
|
|
497
519
|
if (!noEmitChanges && hasChanges) {
|
|
498
520
|
this._emitTreeChanged();
|
|
@@ -515,15 +537,15 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
515
537
|
return node;
|
|
516
538
|
},
|
|
517
539
|
getNodeDisplayValue(node) {
|
|
518
|
-
if (!shouldCalculateDisplayValue)
|
|
519
|
-
return node.data
|
|
540
|
+
if (!shouldCalculateDisplayValue && node.data)
|
|
541
|
+
return getField(node.data, _displayValueMember);
|
|
520
542
|
if (this.getDisplayValueCallback)
|
|
521
543
|
return this.getDisplayValueCallback(node);
|
|
522
544
|
return '[N/A]';
|
|
523
545
|
},
|
|
524
546
|
getNodeSearchValue(node) {
|
|
525
|
-
if (!shouldCalculateSearchValue)
|
|
526
|
-
return node.data
|
|
547
|
+
if (!shouldCalculateSearchValue && node.data)
|
|
548
|
+
return getField(node.data, _searchValueMember);
|
|
527
549
|
if (this.getSearchValueCallback)
|
|
528
550
|
return this.getSearchValueCallback(node);
|
|
529
551
|
return '[N/A]';
|
|
@@ -534,18 +556,26 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
534
556
|
return this.getAllowedDropPositionsCallback(node);
|
|
535
557
|
}
|
|
536
558
|
if (!shouldCalculateAllowedDropPositions && node.data) {
|
|
537
|
-
return node.data
|
|
559
|
+
return getField(node.data, _allowedDropPositionsMember);
|
|
538
560
|
}
|
|
539
561
|
return node.allowedDropPositions;
|
|
540
562
|
},
|
|
541
|
-
|
|
542
|
-
this.
|
|
563
|
+
getNodeIsDraggable(node) {
|
|
564
|
+
if (this.getIsDraggableCallback)
|
|
565
|
+
return this.getIsDraggableCallback(node);
|
|
566
|
+
if (!shouldCalculateIsDraggable && node.data)
|
|
567
|
+
return getField(node.data, _isDraggableMember);
|
|
568
|
+
return node.isDraggable;
|
|
543
569
|
},
|
|
544
|
-
|
|
545
|
-
|
|
570
|
+
getNodeIsCollapsible(node) {
|
|
571
|
+
if (this.getIsCollapsibleCallback)
|
|
572
|
+
return this.getIsCollapsibleCallback(node);
|
|
573
|
+
if (!shouldCalculateIsCollapsible && node.data)
|
|
574
|
+
return getField(node.data, _isCollapsibleMember);
|
|
575
|
+
return node.isCollapsible;
|
|
546
576
|
},
|
|
547
|
-
|
|
548
|
-
|
|
577
|
+
refresh() {
|
|
578
|
+
this._emitTreeChanged();
|
|
549
579
|
},
|
|
550
580
|
/**
|
|
551
581
|
* Get direct children of a node at the given path
|
|
@@ -553,8 +583,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
553
583
|
* @returns Array of child nodes
|
|
554
584
|
*/
|
|
555
585
|
getChildren(parentPath) {
|
|
586
|
+
// Read changeTracker to create reactive dependency for custom recursive renderers
|
|
587
|
+
const _tracker = changeTracker;
|
|
556
588
|
const parent = this.getNodeByPath(parentPath);
|
|
557
|
-
if (!parent)
|
|
589
|
+
if (!parent || !_tracker)
|
|
558
590
|
return [];
|
|
559
591
|
return Object.values(parent.children);
|
|
560
592
|
},
|
|
@@ -564,8 +596,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
564
596
|
* @returns Array of sibling nodes (nodes with same parent)
|
|
565
597
|
*/
|
|
566
598
|
getSiblings(path) {
|
|
599
|
+
// Read changeTracker to create reactive dependency for custom recursive renderers
|
|
600
|
+
const _tracker = changeTracker;
|
|
567
601
|
const node = this.getNodeByPath(path);
|
|
568
|
-
if (!node)
|
|
602
|
+
if (!node || !_tracker)
|
|
569
603
|
return [];
|
|
570
604
|
// Get parent and return all its children
|
|
571
605
|
const parentPath = node.parentPath || '';
|
|
@@ -594,8 +628,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
594
628
|
sorted = [...children].sort((a, b) => {
|
|
595
629
|
// If orderMember is provided, use it
|
|
596
630
|
if (this.orderMember && a.data && b.data) {
|
|
597
|
-
const aOrder = a.data
|
|
598
|
-
const bOrder = b.data
|
|
631
|
+
const aOrder = getField(a.data, this.orderMember) ?? 0;
|
|
632
|
+
const bOrder = getField(b.data, this.orderMember) ?? 0;
|
|
599
633
|
if (aOrder !== bOrder) {
|
|
600
634
|
return aOrder - bOrder;
|
|
601
635
|
}
|
|
@@ -611,7 +645,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
611
645
|
newChildren[segment] = child;
|
|
612
646
|
});
|
|
613
647
|
parent.children = newChildren;
|
|
614
|
-
_bumpRev(parent);
|
|
615
648
|
this._emitTreeChanged();
|
|
616
649
|
},
|
|
617
650
|
/**
|
|
@@ -620,16 +653,15 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
620
653
|
* @param path - Path to the node to refresh
|
|
621
654
|
*/
|
|
622
655
|
refreshNode(path) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
656
|
+
// For now, just trigger a tree change
|
|
657
|
+
// Future optimization: only re-render the specific subtree
|
|
658
|
+
this._emitTreeChanged();
|
|
627
659
|
},
|
|
628
660
|
/**
|
|
629
661
|
* Move a node to a new location in the tree
|
|
630
662
|
* @param sourcePath - Path of the node to move
|
|
631
663
|
* @param targetPath - Path of the target node
|
|
632
|
-
* @param position - Where to place relative to target: '
|
|
664
|
+
* @param position - Where to place relative to target: 'before', 'after', or 'child'
|
|
633
665
|
* @returns Object with success status and optional error message
|
|
634
666
|
*/
|
|
635
667
|
moveNode(sourcePath, targetPath, position) {
|
|
@@ -656,7 +688,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
656
688
|
// Remove source from current parent
|
|
657
689
|
const sourceSegment = segmentPrefix + sourceNode.pathSegment;
|
|
658
690
|
delete sourceParent.children[sourceSegment];
|
|
659
|
-
_bumpRev(sourceParent);
|
|
660
691
|
// Update source parent's hasChildren
|
|
661
692
|
if (Object.keys(sourceParent.children).length === 0) {
|
|
662
693
|
sourceParent.hasChildren = false;
|
|
@@ -670,7 +701,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
670
701
|
newParent = targetNode;
|
|
671
702
|
}
|
|
672
703
|
else {
|
|
673
|
-
// Insert as sibling (
|
|
704
|
+
// Insert as sibling (before or after)
|
|
674
705
|
newParentPath = targetNode.parentPath || '';
|
|
675
706
|
newParent = newParentPath ? this.getNodeByPath(newParentPath) : root;
|
|
676
707
|
}
|
|
@@ -695,7 +726,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
695
726
|
// Update source node's path and parentPath
|
|
696
727
|
sourceNode.path = newPath;
|
|
697
728
|
sourceNode.pathSegment = newSegment;
|
|
698
|
-
|
|
729
|
+
// Keep '' for root nodes (matching insertArray's getParentPath convention)
|
|
730
|
+
sourceNode.parentPath = newParentPath;
|
|
699
731
|
sourceNode.level = getLevel(newPath, this.treePathSeparator);
|
|
700
732
|
// Update the data object's path if pathMember is defined
|
|
701
733
|
if (this.pathMember && sourceNode.data) {
|
|
@@ -703,52 +735,37 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
703
735
|
}
|
|
704
736
|
// Update all descendants' paths recursively
|
|
705
737
|
this._updateDescendantPaths(sourceNode, oldPath, newPath);
|
|
706
|
-
_bumpRev(sourceNode);
|
|
707
738
|
// Insert into new parent
|
|
708
739
|
newParent.children[segmentPrefix + newSegment] = sourceNode;
|
|
709
740
|
newParent.hasChildren = true;
|
|
710
|
-
// If orderMember is defined and position is
|
|
741
|
+
// If orderMember is defined and position is before/after, calculate order
|
|
711
742
|
if (this.orderMember && position !== 'child' && sourceNode.data) {
|
|
743
|
+
const om = this.orderMember;
|
|
712
744
|
const siblings = Object.values(newParent.children);
|
|
713
|
-
const targetOrder = targetNode.data
|
|
714
|
-
if (position === '
|
|
715
|
-
// Find order value just
|
|
745
|
+
const targetOrder = (targetNode.data ? getField(targetNode.data, om) : 0) ?? 0;
|
|
746
|
+
if (position === 'before') {
|
|
747
|
+
// Find order value just before target
|
|
716
748
|
const siblingOrders = siblings
|
|
717
|
-
.filter(s => s !== sourceNode && s.data
|
|
718
|
-
.map(s => s.data
|
|
749
|
+
.filter(s => s !== sourceNode && s.data && getField(s.data, om) !== undefined)
|
|
750
|
+
.map(s => getField(s.data, om))
|
|
719
751
|
.filter(o => o < targetOrder)
|
|
720
752
|
.sort((a, b) => b - a);
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
console.log(`[moveNode] position=above, targetOrder=${targetOrder}, belowOrder=${belowOrder}, newOrder=${newOrder}, siblingOrders=`, siblings.map(s => ({ path: s.path, order: s.data?.[this.orderMember] })));
|
|
724
|
-
sourceNode.data[this.orderMember] = newOrder;
|
|
753
|
+
const prevOrder = siblingOrders[0] ?? targetOrder - 20;
|
|
754
|
+
sourceNode.data[om] = Math.floor((prevOrder + targetOrder) / 2);
|
|
725
755
|
}
|
|
726
756
|
else {
|
|
727
|
-
// Find order value just
|
|
757
|
+
// Find order value just after target
|
|
728
758
|
const siblingOrders = siblings
|
|
729
|
-
.filter(s => s !== sourceNode && s.data
|
|
730
|
-
.map(s => s.data
|
|
759
|
+
.filter(s => s !== sourceNode && s.data && getField(s.data, om) !== undefined)
|
|
760
|
+
.map(s => getField(s.data, om))
|
|
731
761
|
.filter(o => o > targetOrder)
|
|
732
762
|
.sort((a, b) => a - b);
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
console.log(`[moveNode] position=below, targetOrder=${targetOrder}, aboveOrder=${aboveOrder}, newOrder=${newOrder}, siblingOrders=`, siblings.map(s => ({ path: s.path, order: s.data?.[this.orderMember] })));
|
|
736
|
-
sourceNode.data[this.orderMember] = newOrder;
|
|
763
|
+
const nextOrder = siblingOrders[0] ?? targetOrder + 20;
|
|
764
|
+
sourceNode.data[om] = Math.floor((targetOrder + nextOrder) / 2);
|
|
737
765
|
}
|
|
738
766
|
}
|
|
739
|
-
else if (position !== 'child') {
|
|
740
|
-
console.log(`[moveNode] position=${position}, but orderMember=${this.orderMember}, sourceNode.data=${!!sourceNode.data} — order NOT calculated`);
|
|
741
|
-
}
|
|
742
767
|
// Re-sort siblings if needed
|
|
743
768
|
this.refreshSiblings(newParentPath);
|
|
744
|
-
// Log final sibling order after refresh
|
|
745
|
-
{
|
|
746
|
-
const parent = newParentPath ? this.getNodeByPath(newParentPath) : root;
|
|
747
|
-
if (parent) {
|
|
748
|
-
const finalOrder = Object.values(parent.children).map((s) => ({ path: s.path, order: s.data?.[this.orderMember] }));
|
|
749
|
-
console.log(`[moveNode] Final sibling order after refreshSiblings:`, finalOrder);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
769
|
return { success: true };
|
|
753
770
|
},
|
|
754
771
|
/**
|
|
@@ -792,7 +809,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
792
809
|
// Remove from parent
|
|
793
810
|
const segment = segmentPrefix + node.pathSegment;
|
|
794
811
|
delete parent.children[segment];
|
|
795
|
-
_bumpRev(parent);
|
|
796
812
|
// Update parent's hasChildren
|
|
797
813
|
if (Object.keys(parent.children).length === 0) {
|
|
798
814
|
parent.hasChildren = false;
|
|
@@ -832,19 +848,22 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
832
848
|
const id = _idMember && data ? data[_idMember] : undefined;
|
|
833
849
|
pathSegment = id?.toString() || `new_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
834
850
|
}
|
|
851
|
+
// At this point pathSegment is guaranteed to be a string
|
|
852
|
+
const segment = pathSegment;
|
|
835
853
|
// Calculate full path
|
|
836
|
-
const newPath = parentPath ? `${parentPath}${this.treePathSeparator}${
|
|
854
|
+
const newPath = parentPath ? `${parentPath}${this.treePathSeparator}${segment}` : segment;
|
|
837
855
|
// Check if path already exists
|
|
838
856
|
if (this.getNodeByPath(newPath)) {
|
|
839
857
|
return { success: false, error: `Node already exists at path: ${newPath}` };
|
|
840
858
|
}
|
|
841
859
|
// Create the node
|
|
842
860
|
const newNode = createLTreeNode();
|
|
843
|
-
newNode.treeId = _treeId;
|
|
861
|
+
newNode.treeId = _treeId || '';
|
|
844
862
|
newNode.id = _idMember && data ? data[_idMember] : undefined;
|
|
845
863
|
newNode.path = newPath;
|
|
846
|
-
newNode.pathSegment =
|
|
847
|
-
|
|
864
|
+
newNode.pathSegment = segment;
|
|
865
|
+
// Keep '' for root nodes (matching insertArray's getParentPath convention)
|
|
866
|
+
newNode.parentPath = parentPath;
|
|
848
867
|
newNode.level = getLevel(newPath, this.treePathSeparator);
|
|
849
868
|
newNode.data = data;
|
|
850
869
|
newNode.isExpanded = _expandLevel ? newNode.level <= _expandLevel : false;
|
|
@@ -853,10 +872,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
853
872
|
if (_pathMember && data) {
|
|
854
873
|
data[_pathMember] = newPath;
|
|
855
874
|
}
|
|
856
|
-
// Create per-node signal for O(1) reactive updates
|
|
857
|
-
if (newNode.id !== undefined) {
|
|
858
|
-
nodeSignals.set(String(newNode.id), new NodeSignal());
|
|
859
|
-
}
|
|
860
875
|
// Add to parent
|
|
861
876
|
const targetParent = parent || root;
|
|
862
877
|
targetParent.children[segmentPrefix + pathSegment] = newNode;
|
|
@@ -889,6 +904,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
889
904
|
const orderMemberUpdated = this.orderMember && this.orderMember in dataUpdates;
|
|
890
905
|
// Merge updates into existing data
|
|
891
906
|
node.data = { ...node.data, ...dataUpdates };
|
|
907
|
+
node._rev = (node._rev || 0) + 1;
|
|
892
908
|
// Re-index for search if needed
|
|
893
909
|
if (indexer && _shouldUseInternalSearchIndex) {
|
|
894
910
|
const flatIndex = flatTreeNodes.indexOf(node);
|
|
@@ -896,14 +912,11 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
896
912
|
indexer.addItem({ node, index: flatIndex });
|
|
897
913
|
}
|
|
898
914
|
}
|
|
899
|
-
// Re-sort siblings if order was updated
|
|
915
|
+
// Re-sort siblings if order was updated
|
|
900
916
|
if (orderMemberUpdated) {
|
|
901
|
-
_bumpRev(node);
|
|
902
917
|
this.refreshSiblings(node.parentPath || '');
|
|
903
|
-
return { success: true, node };
|
|
904
918
|
}
|
|
905
|
-
|
|
906
|
-
_bumpNodeRev(node);
|
|
919
|
+
this._emitTreeChanged();
|
|
907
920
|
return { success: true, node };
|
|
908
921
|
},
|
|
909
922
|
/**
|
|
@@ -975,7 +988,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
975
988
|
* @param targetParentPath - Path where to insert the copy (empty string for root)
|
|
976
989
|
* @param transformData - Function to transform each node's data (e.g., assign new IDs)
|
|
977
990
|
* @param siblingPath - Optional path of sibling to position relative to
|
|
978
|
-
* @param position - Optional position relative to sibling ('
|
|
991
|
+
* @param position - Optional position relative to sibling ('before' or 'after')
|
|
979
992
|
* @returns Object with success status, the created root node, and count of nodes created
|
|
980
993
|
*/
|
|
981
994
|
copyNodeWithDescendants(sourceNode, targetParentPath, transformData, siblingPath, position) {
|
|
@@ -1015,53 +1028,37 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1015
1028
|
// Handle positioning relative to sibling if specified
|
|
1016
1029
|
if (siblingPath && position && rootNode.data) {
|
|
1017
1030
|
const siblingNode = this.getNodeByPath(siblingPath);
|
|
1018
|
-
console.log(`[copyNodeWithDescendants] Positioning: siblingPath=${siblingPath}, position=${position}, siblingFound=${!!siblingNode}, orderMember=${this.orderMember}`);
|
|
1019
1031
|
if (siblingNode && this.orderMember) {
|
|
1020
1032
|
// Get the parent to access siblings
|
|
1021
1033
|
const parent = targetParentPath ? this.getNodeByPath(targetParentPath) : root;
|
|
1022
1034
|
if (parent) {
|
|
1023
1035
|
const siblings = Object.values(parent.children);
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1036
|
+
const oKey = this.orderMember;
|
|
1037
|
+
const siblingOrder = siblingNode.data?.[oKey] ?? 0;
|
|
1038
|
+
if (position === 'before') {
|
|
1039
|
+
// Find order value just before sibling
|
|
1027
1040
|
const siblingOrders = siblings
|
|
1028
|
-
.filter(s => s !== rootNode && s.data?.[
|
|
1029
|
-
.map(s => s.data[
|
|
1041
|
+
.filter(s => s !== rootNode && s.data?.[oKey] !== undefined)
|
|
1042
|
+
.map(s => s.data[oKey])
|
|
1030
1043
|
.filter(o => o < siblingOrder)
|
|
1031
1044
|
.sort((a, b) => b - a);
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
console.log(`[copyNodeWithDescendants] position=above, siblingOrder=${siblingOrder}, belowOrder=${belowOrder}, newOrder=${newOrder}`);
|
|
1035
|
-
rootNode.data[this.orderMember] = newOrder;
|
|
1045
|
+
const prevOrder = siblingOrders[0] ?? siblingOrder - 20;
|
|
1046
|
+
rootNode.data[oKey] = Math.floor((prevOrder + siblingOrder) / 2);
|
|
1036
1047
|
}
|
|
1037
1048
|
else {
|
|
1038
|
-
// Find order value just
|
|
1049
|
+
// Find order value just after sibling
|
|
1039
1050
|
const siblingOrders = siblings
|
|
1040
|
-
.filter(s => s !== rootNode && s.data?.[
|
|
1041
|
-
.map(s => s.data[
|
|
1051
|
+
.filter(s => s !== rootNode && s.data?.[oKey] !== undefined)
|
|
1052
|
+
.map(s => s.data[oKey])
|
|
1042
1053
|
.filter(o => o > siblingOrder)
|
|
1043
1054
|
.sort((a, b) => a - b);
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
console.log(`[copyNodeWithDescendants] position=below, siblingOrder=${siblingOrder}, aboveOrder=${aboveOrder}, newOrder=${newOrder}`);
|
|
1047
|
-
rootNode.data[this.orderMember] = newOrder;
|
|
1055
|
+
const nextOrder = siblingOrders[0] ?? siblingOrder + 20;
|
|
1056
|
+
rootNode.data[oKey] = Math.floor((siblingOrder + nextOrder) / 2);
|
|
1048
1057
|
}
|
|
1049
1058
|
// Re-sort siblings
|
|
1050
1059
|
this.refreshSiblings(targetParentPath);
|
|
1051
|
-
// Log final order
|
|
1052
|
-
const finalOrder = Object.values(parent.children).map((s) => ({ path: s.path, order: s.data?.[this.orderMember] }));
|
|
1053
|
-
console.log(`[copyNodeWithDescendants] Final sibling order:`, finalOrder);
|
|
1054
1060
|
}
|
|
1055
1061
|
}
|
|
1056
|
-
else if (!siblingNode) {
|
|
1057
|
-
console.warn(`[copyNodeWithDescendants] Sibling not found at path: ${siblingPath}`);
|
|
1058
|
-
}
|
|
1059
|
-
else if (!this.orderMember) {
|
|
1060
|
-
console.warn(`[copyNodeWithDescendants] No orderMember set — cannot position`);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
else {
|
|
1064
|
-
console.log(`[copyNodeWithDescendants] No positioning: siblingPath=${siblingPath}, position=${position}, hasData=${!!rootNode.data}`);
|
|
1065
1062
|
}
|
|
1066
1063
|
return { success: true, rootNode, count: totalCount };
|
|
1067
1064
|
},
|
|
@@ -1092,11 +1089,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1092
1089
|
const pathSet = new Set(paths);
|
|
1093
1090
|
const traverse = (node) => {
|
|
1094
1091
|
if (node.path) {
|
|
1095
|
-
|
|
1096
|
-
if (node.isExpanded !== shouldExpand) {
|
|
1097
|
-
node.isExpanded = shouldExpand;
|
|
1098
|
-
_bumpRev(node);
|
|
1099
|
-
}
|
|
1092
|
+
node.isExpanded = pathSet.has(node.path);
|
|
1100
1093
|
}
|
|
1101
1094
|
for (const child of Object.values(node.children)) {
|
|
1102
1095
|
traverse(child);
|
|
@@ -1133,9 +1126,9 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1133
1126
|
}
|
|
1134
1127
|
// Then sort by parent path
|
|
1135
1128
|
if (a.parentPath !== b.parentPath) {
|
|
1136
|
-
if (a.parentPath
|
|
1129
|
+
if (!a.parentPath)
|
|
1137
1130
|
return -1;
|
|
1138
|
-
if (b.parentPath
|
|
1131
|
+
if (!b.parentPath)
|
|
1139
1132
|
return 1;
|
|
1140
1133
|
return a.parentPath.localeCompare(b.parentPath);
|
|
1141
1134
|
}
|