@keenmate/svelte-treeview 4.6.0 → 4.8.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 +218 -222
- package/dist/components/Node.svelte +89 -72
- package/dist/components/Node.svelte.d.ts +1 -1
- package/dist/components/Tree.svelte +422 -171
- package/dist/components/Tree.svelte.d.ts +37 -13
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/ltree/indexer.js +5 -2
- package/dist/ltree/ltree-node.svelte.d.ts +3 -0
- package/dist/ltree/ltree-node.svelte.js +2 -0
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +120 -73
- package/dist/ltree/types.d.ts +9 -3
- package/dist/styles/main.scss +53 -6
- package/dist/styles.css +43 -6
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { getLevel, getParentPath, getPathSegments, getRelativePath } from '../he
|
|
|
5
5
|
import { createSearchIndex } from './flex.js';
|
|
6
6
|
import { Indexer } from './indexer.js';
|
|
7
7
|
import { perfStart, perfEnd, perfSummary } from '../perf-logger.js';
|
|
8
|
-
export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
|
|
8
|
+
export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
|
|
9
9
|
let shouldCalculateParentPath = isEmptyString(_parentPathMember);
|
|
10
10
|
let shouldCalculateLevel = isEmptyString(_levelMember);
|
|
11
11
|
let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
|
|
@@ -13,6 +13,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
13
13
|
let shouldCalculateIsSelectable = isEmptyString(_isSelectableMember);
|
|
14
14
|
let shouldCalculateIsDraggable = isEmptyString(_isDraggableMember);
|
|
15
15
|
let shouldCalculateIsDropAllowed = isEmptyString(_isDropAllowedMember);
|
|
16
|
+
let shouldCalculateAllowedDropPositions = isEmptyString(_allowedDropPositionsMember);
|
|
16
17
|
let shouldCalculateDisplayValue = isEmptyString(_displayValueMember);
|
|
17
18
|
let shouldCalculateSearchValue = isEmptyString(_searchValueMember);
|
|
18
19
|
// this is absolutely crucial to keep order of sorted items. Segments are just numbers and numbers as properties are always sorted
|
|
@@ -42,6 +43,27 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
42
43
|
_indexerTimeout || 50, // timeout with fallback
|
|
43
44
|
opts.shouldDisplayDebugInformation);
|
|
44
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();
|
|
66
|
+
}
|
|
45
67
|
return {
|
|
46
68
|
// Properties
|
|
47
69
|
treePathSeparator: _treePathSeparator || '.',
|
|
@@ -57,13 +79,15 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
57
79
|
isSelectableMember: _isSelectableMember,
|
|
58
80
|
isDraggableMember: _isDraggableMember,
|
|
59
81
|
isDropAllowedMember: _isDropAllowedMember,
|
|
82
|
+
allowedDropPositionsMember: _allowedDropPositionsMember,
|
|
60
83
|
hasChildrenMember: _hasChildrenMember,
|
|
61
84
|
displayValueMember: _displayValueMember,
|
|
62
85
|
getDisplayValueCallback: _getDisplayValueCallback,
|
|
63
86
|
searchValueMember: _searchValueMember,
|
|
64
87
|
getSearchValueCallback: _getSearchValueCallback,
|
|
88
|
+
getAllowedDropPositionsCallback: _getAllowedDropPositionsCallback,
|
|
65
89
|
orderMember: _orderMember,
|
|
66
|
-
isSorted: false,
|
|
90
|
+
isSorted: opts?.isSorted ?? false,
|
|
67
91
|
// Properties for filtering
|
|
68
92
|
filteredTree,
|
|
69
93
|
isFiltered,
|
|
@@ -104,11 +128,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
104
128
|
const result = [];
|
|
105
129
|
const self = this;
|
|
106
130
|
function traverse(node) {
|
|
107
|
-
// Get children
|
|
108
|
-
|
|
109
|
-
if (self.isSorted && children.length > 0) {
|
|
110
|
-
children = self.sortCallback(children);
|
|
111
|
-
}
|
|
131
|
+
// Get children in natural tree key order (same as recursive mode)
|
|
132
|
+
const children = Object.values(node.children);
|
|
112
133
|
for (const child of children) {
|
|
113
134
|
result.push(child);
|
|
114
135
|
// Only traverse into children if this node is expanded
|
|
@@ -118,8 +139,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
118
139
|
}
|
|
119
140
|
}
|
|
120
141
|
traverse(startRoot);
|
|
121
|
-
const computeTime = performance.now() - computeStart;
|
|
122
|
-
console.log(`[visibleFlatNodes] Computed ${result.length} nodes in ${computeTime.toFixed(2)}ms`);
|
|
123
142
|
// Cache the result
|
|
124
143
|
cachedVisibleFlatNodes = result;
|
|
125
144
|
cachedVisibleFlatNodesTracker = _tracker;
|
|
@@ -138,6 +157,12 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
138
157
|
},
|
|
139
158
|
insertArray: function (data, noEmitChanges = false) {
|
|
140
159
|
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
|
+
}
|
|
141
166
|
// Clear any pending indexing from previous calls
|
|
142
167
|
indexer?.clearQueue();
|
|
143
168
|
flatTreeNodes = [];
|
|
@@ -171,14 +196,14 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
171
196
|
node.isDraggable = row[_isDraggableMember];
|
|
172
197
|
if (!shouldCalculateIsDropAllowed)
|
|
173
198
|
node.isDropAllowed = row[_isDropAllowedMember];
|
|
199
|
+
if (!shouldCalculateAllowedDropPositions)
|
|
200
|
+
node.allowedDropPositions = row[_allowedDropPositionsMember];
|
|
174
201
|
if (!shouldCalculateHasChildren)
|
|
175
202
|
node.hasChildren = row[_hasChildrenMember];
|
|
176
203
|
node.data = row;
|
|
177
204
|
return node;
|
|
178
205
|
});
|
|
179
206
|
const conversionTime = perfEnd(`[${_treeId}] insertArray:conversion`, data.length);
|
|
180
|
-
if (this.shouldDisplayDebugInformation)
|
|
181
|
-
console.log(`[Tree ${_treeId}] Mapped data before sort`, mappedData);
|
|
182
207
|
perfStart(`[${_treeId}] insertArray:sort`);
|
|
183
208
|
if (!this.isSorted) {
|
|
184
209
|
if (this.sortCallback)
|
|
@@ -186,8 +211,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
186
211
|
else
|
|
187
212
|
mappedData = this._defaultSort(this, mappedData);
|
|
188
213
|
}
|
|
189
|
-
if (this.shouldDisplayDebugInformation)
|
|
190
|
-
console.log(`[Tree ${_treeId}] Mapped data after sort`, mappedData);
|
|
191
214
|
const sortTime = perfEnd(`[${_treeId}] insertArray:sort`, data.length);
|
|
192
215
|
perfStart(`[${_treeId}] insertArray:insert`);
|
|
193
216
|
const failedNodes = [];
|
|
@@ -228,9 +251,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
228
251
|
// We've processed all nodes up to expandLevel - render now!
|
|
229
252
|
hasRenderedExpandLevel = true;
|
|
230
253
|
this._emitTreeChanged();
|
|
231
|
-
if (this.shouldDisplayDebugInformation) {
|
|
232
|
-
console.log(`[Tree ${_treeId}] Progressive render: Displayed levels 1-${_expandLevel} (${successfulCount} nodes processed so far)`);
|
|
233
|
-
}
|
|
234
254
|
}
|
|
235
255
|
}
|
|
236
256
|
});
|
|
@@ -252,20 +272,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
252
272
|
}
|
|
253
273
|
// Final render (only if we haven't already rendered progressively)
|
|
254
274
|
if (!noEmitChanges) {
|
|
255
|
-
|
|
256
|
-
// We already rendered expandLevel, now render the complete tree
|
|
257
|
-
this._emitTreeChanged();
|
|
258
|
-
if (this.shouldDisplayDebugInformation) {
|
|
259
|
-
console.log(`[Tree ${_treeId}] Final render: Complete tree with all ${successfulCount} nodes`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
// No progressive rendering occurred, render everything at once
|
|
264
|
-
this._emitTreeChanged();
|
|
265
|
-
if (this.shouldDisplayDebugInformation) {
|
|
266
|
-
console.log(`[Tree ${_treeId}] Single render: All ${successfulCount} nodes (no expandLevel or progressive render conditions met)`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
275
|
+
this._emitTreeChanged();
|
|
269
276
|
}
|
|
270
277
|
const insertTime = perfEnd(`[${_treeId}] insertArray:insert`, data.length);
|
|
271
278
|
// Log performance summary
|
|
@@ -304,11 +311,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
304
311
|
return null;
|
|
305
312
|
},
|
|
306
313
|
filterNodes(_searchText, _searchOptions) {
|
|
307
|
-
if (this.shouldDisplayDebugInformation)
|
|
308
|
-
console.log(`[Tree ${_treeId}] Filtering nodes by:`, _searchText);
|
|
309
314
|
if (isEmptyString(_searchText)) {
|
|
310
|
-
if (this.shouldDisplayDebugInformation)
|
|
311
|
-
console.log(`[Tree ${_treeId}] Search text is empty, cleaning filtered tree and setting isFiltered = false`);
|
|
312
315
|
// Clear filter when search is empty
|
|
313
316
|
filteredRoot.children = {};
|
|
314
317
|
this.isFiltered = false;
|
|
@@ -316,33 +319,23 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
316
319
|
return;
|
|
317
320
|
}
|
|
318
321
|
if (!_shouldUseInternalSearchIndex) {
|
|
319
|
-
if (this.shouldDisplayDebugInformation)
|
|
320
|
-
console.warn(`[Tree ${_treeId}] Internal search index is disabled`);
|
|
321
322
|
return;
|
|
322
323
|
}
|
|
323
324
|
perfStart(`[${_treeId}] filterNodes:search`);
|
|
324
325
|
const resultIndices = searchIndex.search(_searchText, _searchOptions);
|
|
325
326
|
const foundPaths = resultIndices.map((row) => flatTreeNodes[row].path);
|
|
326
327
|
perfEnd(`[${_treeId}] filterNodes:search`, resultIndices.length);
|
|
327
|
-
if (this.shouldDisplayDebugInformation)
|
|
328
|
-
console.warn(`[Tree ${_treeId}] Found indices:`, resultIndices, foundPaths);
|
|
329
328
|
this.createFilteredTree(foundPaths);
|
|
330
329
|
},
|
|
331
330
|
searchNodes(_searchText, _searchOptions) {
|
|
332
|
-
if (this.shouldDisplayDebugInformation)
|
|
333
|
-
console.log(`[Tree ${_treeId}] Searching nodes by:`, _searchText);
|
|
334
331
|
if (isEmptyString(_searchText)) {
|
|
335
332
|
return [];
|
|
336
333
|
}
|
|
337
334
|
if (!_shouldUseInternalSearchIndex) {
|
|
338
|
-
if (this.shouldDisplayDebugInformation)
|
|
339
|
-
console.warn(`[Tree ${_treeId}] Internal search index is disabled`);
|
|
340
335
|
return [];
|
|
341
336
|
}
|
|
342
337
|
const resultIndices = searchIndex.search(_searchText, _searchOptions);
|
|
343
338
|
const foundNodes = resultIndices.map((row) => flatTreeNodes[row]);
|
|
344
|
-
if (this.shouldDisplayDebugInformation)
|
|
345
|
-
console.warn(`[Tree ${_treeId}] Found indices:`, resultIndices, foundNodes);
|
|
346
339
|
return foundNodes;
|
|
347
340
|
},
|
|
348
341
|
createFilteredTree(targetPaths) {
|
|
@@ -358,8 +351,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
358
351
|
allRequiredPaths.add(segments.slice(0, i).join(this.treePathSeparator));
|
|
359
352
|
}
|
|
360
353
|
});
|
|
361
|
-
if (this.shouldDisplayDebugInformation)
|
|
362
|
-
console.log(`[Tree ${_treeId}] allRequiredPaths`, Array.from(allRequiredPaths));
|
|
363
354
|
// 2. Build filtered tree with only required paths
|
|
364
355
|
const pathToNode = new Map();
|
|
365
356
|
// First pass: create copies of all required nodes
|
|
@@ -411,8 +402,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
411
402
|
this.isFiltered = true;
|
|
412
403
|
this._emitTreeChanged();
|
|
413
404
|
perfEnd(`[${_treeId}] createFilteredTree`, rootNodes.length);
|
|
414
|
-
if (this.shouldDisplayDebugInformation)
|
|
415
|
-
console.log(`[Tree ${_treeId}] Created filtered tree with`, rootNodes.length, 'root nodes');
|
|
416
405
|
},
|
|
417
406
|
clearFilter() {
|
|
418
407
|
filteredRoot.children = {};
|
|
@@ -424,7 +413,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
424
413
|
perfStart(`[${_treeId}] expandAll`);
|
|
425
414
|
if (isEmptyString(nodePath))
|
|
426
415
|
flatTreeNodes.forEach((row) => {
|
|
427
|
-
row.isExpanded
|
|
416
|
+
if (!row.isExpanded) {
|
|
417
|
+
row.isExpanded = true;
|
|
418
|
+
_bumpRev(row);
|
|
419
|
+
}
|
|
428
420
|
});
|
|
429
421
|
this._emitTreeChanged();
|
|
430
422
|
perfEnd(`[${_treeId}] expandAll`, flatTreeNodes.length);
|
|
@@ -433,7 +425,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
433
425
|
perfStart(`[${_treeId}] collapseAll`);
|
|
434
426
|
if (isEmptyString(nodePath))
|
|
435
427
|
flatTreeNodes.forEach((row) => {
|
|
436
|
-
row.isExpanded
|
|
428
|
+
if (row.isExpanded) {
|
|
429
|
+
row.isExpanded = false;
|
|
430
|
+
_bumpRev(row);
|
|
431
|
+
}
|
|
437
432
|
});
|
|
438
433
|
this._emitTreeChanged();
|
|
439
434
|
perfEnd(`[${_treeId}] collapseAll`, flatTreeNodes.length);
|
|
@@ -470,13 +465,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
470
465
|
// Only mark as changed if actually changing from collapsed to expanded
|
|
471
466
|
if (!node.isExpanded) {
|
|
472
467
|
node.isExpanded = true;
|
|
468
|
+
_bumpRev(node);
|
|
473
469
|
hasChanges = true;
|
|
474
470
|
}
|
|
475
471
|
}
|
|
476
472
|
}
|
|
477
473
|
// Only emit changes if something actually changed
|
|
478
474
|
if (!noEmitChanges && hasChanges) {
|
|
479
|
-
console.log(`[Tree ${_treeId}] expandNodes triggering re-render for path: ${path}`);
|
|
480
475
|
this._emitTreeChanged();
|
|
481
476
|
}
|
|
482
477
|
perfEnd(`[${_treeId}] expandNodes`);
|
|
@@ -493,6 +488,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
493
488
|
// Only mark as changed if actually changing from expanded to collapsed
|
|
494
489
|
if (node.isExpanded) {
|
|
495
490
|
node.isExpanded = false;
|
|
491
|
+
_bumpRev(node);
|
|
496
492
|
hasChanges = true;
|
|
497
493
|
}
|
|
498
494
|
}
|
|
@@ -532,9 +528,25 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
532
528
|
return this.getSearchValueCallback(node);
|
|
533
529
|
return '[N/A]';
|
|
534
530
|
},
|
|
531
|
+
getNodeAllowedDropPositions(node) {
|
|
532
|
+
// Priority: callback > member > node property
|
|
533
|
+
if (this.getAllowedDropPositionsCallback) {
|
|
534
|
+
return this.getAllowedDropPositionsCallback(node);
|
|
535
|
+
}
|
|
536
|
+
if (!shouldCalculateAllowedDropPositions && node.data) {
|
|
537
|
+
return node.data[_allowedDropPositionsMember];
|
|
538
|
+
}
|
|
539
|
+
return node.allowedDropPositions;
|
|
540
|
+
},
|
|
535
541
|
refresh() {
|
|
536
542
|
this._emitTreeChanged();
|
|
537
543
|
},
|
|
544
|
+
getNodeSignal(id) {
|
|
545
|
+
return nodeSignals.get(id);
|
|
546
|
+
},
|
|
547
|
+
bumpNodeRev(node) {
|
|
548
|
+
_bumpNodeRev(node);
|
|
549
|
+
},
|
|
538
550
|
/**
|
|
539
551
|
* Get direct children of a node at the given path
|
|
540
552
|
* @param parentPath - Path to parent node (empty string for root)
|
|
@@ -599,9 +611,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
599
611
|
newChildren[segment] = child;
|
|
600
612
|
});
|
|
601
613
|
parent.children = newChildren;
|
|
602
|
-
|
|
603
|
-
console.log(`[Tree ${_treeId}] refreshSiblings: Re-sorted ${sorted.length} children under "${parentPath || 'root'}":`, sorted.map(s => ({ path: s.path, sortOrder: s.data?.[this.orderMember] })));
|
|
604
|
-
}
|
|
614
|
+
_bumpRev(parent);
|
|
605
615
|
this._emitTreeChanged();
|
|
606
616
|
},
|
|
607
617
|
/**
|
|
@@ -610,9 +620,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
610
620
|
* @param path - Path to the node to refresh
|
|
611
621
|
*/
|
|
612
622
|
refreshNode(path) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
623
|
+
const node = this.getNodeByPath(path);
|
|
624
|
+
if (node) {
|
|
625
|
+
_bumpNodeRev(node);
|
|
626
|
+
}
|
|
616
627
|
},
|
|
617
628
|
/**
|
|
618
629
|
* Move a node to a new location in the tree
|
|
@@ -645,6 +656,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
645
656
|
// Remove source from current parent
|
|
646
657
|
const sourceSegment = segmentPrefix + sourceNode.pathSegment;
|
|
647
658
|
delete sourceParent.children[sourceSegment];
|
|
659
|
+
_bumpRev(sourceParent);
|
|
648
660
|
// Update source parent's hasChildren
|
|
649
661
|
if (Object.keys(sourceParent.children).length === 0) {
|
|
650
662
|
sourceParent.hasChildren = false;
|
|
@@ -691,6 +703,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
691
703
|
}
|
|
692
704
|
// Update all descendants' paths recursively
|
|
693
705
|
this._updateDescendantPaths(sourceNode, oldPath, newPath);
|
|
706
|
+
_bumpRev(sourceNode);
|
|
694
707
|
// Insert into new parent
|
|
695
708
|
newParent.children[segmentPrefix + newSegment] = sourceNode;
|
|
696
709
|
newParent.hasChildren = true;
|
|
@@ -706,7 +719,9 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
706
719
|
.filter(o => o < targetOrder)
|
|
707
720
|
.sort((a, b) => b - a);
|
|
708
721
|
const belowOrder = siblingOrders[0] ?? targetOrder - 20;
|
|
709
|
-
|
|
722
|
+
const newOrder = Math.floor((belowOrder + targetOrder) / 2);
|
|
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;
|
|
710
725
|
}
|
|
711
726
|
else {
|
|
712
727
|
// Find order value just above target
|
|
@@ -716,11 +731,24 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
716
731
|
.filter(o => o > targetOrder)
|
|
717
732
|
.sort((a, b) => a - b);
|
|
718
733
|
const aboveOrder = siblingOrders[0] ?? targetOrder + 20;
|
|
719
|
-
|
|
734
|
+
const newOrder = Math.floor((targetOrder + aboveOrder) / 2);
|
|
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;
|
|
720
737
|
}
|
|
721
738
|
}
|
|
739
|
+
else if (position !== 'child') {
|
|
740
|
+
console.log(`[moveNode] position=${position}, but orderMember=${this.orderMember}, sourceNode.data=${!!sourceNode.data} — order NOT calculated`);
|
|
741
|
+
}
|
|
722
742
|
// Re-sort siblings if needed
|
|
723
743
|
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
|
+
}
|
|
724
752
|
return { success: true };
|
|
725
753
|
},
|
|
726
754
|
/**
|
|
@@ -764,6 +792,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
764
792
|
// Remove from parent
|
|
765
793
|
const segment = segmentPrefix + node.pathSegment;
|
|
766
794
|
delete parent.children[segment];
|
|
795
|
+
_bumpRev(parent);
|
|
767
796
|
// Update parent's hasChildren
|
|
768
797
|
if (Object.keys(parent.children).length === 0) {
|
|
769
798
|
parent.hasChildren = false;
|
|
@@ -824,6 +853,10 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
824
853
|
if (_pathMember && data) {
|
|
825
854
|
data[_pathMember] = newPath;
|
|
826
855
|
}
|
|
856
|
+
// Create per-node signal for O(1) reactive updates
|
|
857
|
+
if (newNode.id !== undefined) {
|
|
858
|
+
nodeSignals.set(String(newNode.id), new NodeSignal());
|
|
859
|
+
}
|
|
827
860
|
// Add to parent
|
|
828
861
|
const targetParent = parent || root;
|
|
829
862
|
targetParent.children[segmentPrefix + pathSegment] = newNode;
|
|
@@ -835,10 +868,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
835
868
|
flatTreeNodes.push(newNode);
|
|
836
869
|
// Re-sort siblings to place new node in correct position
|
|
837
870
|
this.refreshSiblings(parentPath);
|
|
838
|
-
if (this.shouldDisplayDebugInformation) {
|
|
839
|
-
const siblings = Object.values(targetParent.children);
|
|
840
|
-
console.log(`[Tree ${_treeId}] addNode: Added "${newPath}" to "${parentPath || 'root'}". Siblings after sort:`, siblings.map(s => ({ path: s.path, sortOrder: s.data?.[this.orderMember] })));
|
|
841
|
-
}
|
|
842
871
|
this._emitTreeChanged();
|
|
843
872
|
return { success: true, node: newNode };
|
|
844
873
|
},
|
|
@@ -858,9 +887,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
858
887
|
}
|
|
859
888
|
// Check if orderMember is being updated (will need re-sort)
|
|
860
889
|
const orderMemberUpdated = this.orderMember && this.orderMember in dataUpdates;
|
|
861
|
-
if (this.shouldDisplayDebugInformation) {
|
|
862
|
-
console.log(`[Tree ${_treeId}] updateNode: "${path}" updating:`, dataUpdates);
|
|
863
|
-
}
|
|
864
890
|
// Merge updates into existing data
|
|
865
891
|
node.data = { ...node.data, ...dataUpdates };
|
|
866
892
|
// Re-index for search if needed
|
|
@@ -870,11 +896,14 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
870
896
|
indexer.addItem({ node, index: flatIndex });
|
|
871
897
|
}
|
|
872
898
|
}
|
|
873
|
-
// Re-sort siblings if order was updated
|
|
899
|
+
// Re-sort siblings if order was updated (structural change)
|
|
874
900
|
if (orderMemberUpdated) {
|
|
901
|
+
_bumpRev(node);
|
|
875
902
|
this.refreshSiblings(node.parentPath || '');
|
|
903
|
+
return { success: true, node };
|
|
876
904
|
}
|
|
877
|
-
|
|
905
|
+
// Data-only change — signal only, skip _emitTreeChanged for O(1) update
|
|
906
|
+
_bumpNodeRev(node);
|
|
878
907
|
return { success: true, node };
|
|
879
908
|
},
|
|
880
909
|
/**
|
|
@@ -986,6 +1015,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
986
1015
|
// Handle positioning relative to sibling if specified
|
|
987
1016
|
if (siblingPath && position && rootNode.data) {
|
|
988
1017
|
const siblingNode = this.getNodeByPath(siblingPath);
|
|
1018
|
+
console.log(`[copyNodeWithDescendants] Positioning: siblingPath=${siblingPath}, position=${position}, siblingFound=${!!siblingNode}, orderMember=${this.orderMember}`);
|
|
989
1019
|
if (siblingNode && this.orderMember) {
|
|
990
1020
|
// Get the parent to access siblings
|
|
991
1021
|
const parent = targetParentPath ? this.getNodeByPath(targetParentPath) : root;
|
|
@@ -1000,7 +1030,9 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1000
1030
|
.filter(o => o < siblingOrder)
|
|
1001
1031
|
.sort((a, b) => b - a);
|
|
1002
1032
|
const belowOrder = siblingOrders[0] ?? siblingOrder - 20;
|
|
1003
|
-
|
|
1033
|
+
const newOrder = Math.floor((belowOrder + siblingOrder) / 2);
|
|
1034
|
+
console.log(`[copyNodeWithDescendants] position=above, siblingOrder=${siblingOrder}, belowOrder=${belowOrder}, newOrder=${newOrder}`);
|
|
1035
|
+
rootNode.data[this.orderMember] = newOrder;
|
|
1004
1036
|
}
|
|
1005
1037
|
else {
|
|
1006
1038
|
// Find order value just above sibling
|
|
@@ -1010,15 +1042,26 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1010
1042
|
.filter(o => o > siblingOrder)
|
|
1011
1043
|
.sort((a, b) => a - b);
|
|
1012
1044
|
const aboveOrder = siblingOrders[0] ?? siblingOrder + 20;
|
|
1013
|
-
|
|
1045
|
+
const newOrder = Math.floor((siblingOrder + aboveOrder) / 2);
|
|
1046
|
+
console.log(`[copyNodeWithDescendants] position=below, siblingOrder=${siblingOrder}, aboveOrder=${aboveOrder}, newOrder=${newOrder}`);
|
|
1047
|
+
rootNode.data[this.orderMember] = newOrder;
|
|
1014
1048
|
}
|
|
1015
1049
|
// Re-sort siblings
|
|
1016
1050
|
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);
|
|
1017
1054
|
}
|
|
1018
1055
|
}
|
|
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
|
+
}
|
|
1019
1062
|
}
|
|
1020
|
-
|
|
1021
|
-
console.log(`[
|
|
1063
|
+
else {
|
|
1064
|
+
console.log(`[copyNodeWithDescendants] No positioning: siblingPath=${siblingPath}, position=${position}, hasData=${!!rootNode.data}`);
|
|
1022
1065
|
}
|
|
1023
1066
|
return { success: true, rootNode, count: totalCount };
|
|
1024
1067
|
},
|
|
@@ -1049,7 +1092,11 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1049
1092
|
const pathSet = new Set(paths);
|
|
1050
1093
|
const traverse = (node) => {
|
|
1051
1094
|
if (node.path) {
|
|
1052
|
-
|
|
1095
|
+
const shouldExpand = pathSet.has(node.path);
|
|
1096
|
+
if (node.isExpanded !== shouldExpand) {
|
|
1097
|
+
node.isExpanded = shouldExpand;
|
|
1098
|
+
_bumpRev(node);
|
|
1099
|
+
}
|
|
1053
1100
|
}
|
|
1054
1101
|
for (const child of Object.values(node.children)) {
|
|
1055
1102
|
traverse(child);
|
package/dist/ltree/types.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { SearchOptions } from 'flexsearch';
|
|
2
|
-
import type { LTreeNode } from './ltree-node.svelte';
|
|
3
|
-
export type { LTreeNode } from './ltree-node.svelte';
|
|
2
|
+
import type { LTreeNode, DropPosition } from './ltree-node.svelte';
|
|
3
|
+
export type { LTreeNode, DropPosition } from './ltree-node.svelte';
|
|
4
4
|
export type Tuple<T, U> = [T, U];
|
|
5
|
-
export type DropPosition = 'above' | 'below' | 'child';
|
|
6
5
|
export type DragDropMode = 'none' | 'self' | 'cross' | 'both';
|
|
7
6
|
export type DropZoneLayout = 'around' | 'above' | 'below' | 'wave' | 'wave2';
|
|
8
7
|
export type DropOperation = 'move' | 'copy';
|
|
@@ -69,7 +68,10 @@ export interface Ltree<T> {
|
|
|
69
68
|
isSelectableMember: string | null | undefined;
|
|
70
69
|
isDraggableMember: string | null | undefined;
|
|
71
70
|
isDropAllowedMember: string | null | undefined;
|
|
71
|
+
allowedDropPositionsMember: string | null | undefined;
|
|
72
|
+
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
|
|
72
73
|
shouldDisplayDebugInformation: boolean | null | undefined;
|
|
74
|
+
getNodeAllowedDropPositions(node: LTreeNode<T>): DropPosition[] | null | undefined;
|
|
73
75
|
get tree(): LTreeNode<T>[];
|
|
74
76
|
/** Flat array of all visible nodes in render order (depth-first, respects isExpanded) */
|
|
75
77
|
get visibleFlatNodes(): LTreeNode<T>[];
|
|
@@ -98,6 +100,10 @@ export interface Ltree<T> {
|
|
|
98
100
|
getSiblings(path: string): LTreeNode<T>[];
|
|
99
101
|
refreshSiblings(parentPath: string): void;
|
|
100
102
|
refreshNode(path: string): void;
|
|
103
|
+
bumpNodeRev(node: LTreeNode<T>): void;
|
|
104
|
+
getNodeSignal(id: string): {
|
|
105
|
+
readonly value: number;
|
|
106
|
+
} | undefined;
|
|
101
107
|
moveNode(sourcePath: string, targetPath: string, position: 'above' | 'below' | 'child'): {
|
|
102
108
|
success: boolean;
|
|
103
109
|
error?: string;
|
package/dist/styles/main.scss
CHANGED
|
@@ -74,6 +74,36 @@ $light-color: #f8f9fa !default;
|
|
|
74
74
|
$border-color: #dee2e6 !default;
|
|
75
75
|
$body-color: #212529 !default;
|
|
76
76
|
|
|
77
|
+
// Mixin: expand horizontal zone layout when zones are hidden (used by "above" and "below" layouts)
|
|
78
|
+
@mixin zone-horizontal-expansion {
|
|
79
|
+
// Two zones: first present zone gets left half
|
|
80
|
+
&:not(:has(.ltree-drop-child)) .ltree-drop-above,
|
|
81
|
+
&:not(:has(.ltree-drop-below)) .ltree-drop-above,
|
|
82
|
+
&:not(:has(.ltree-drop-above)) .ltree-drop-below {
|
|
83
|
+
left: var(--drop-zone-start, 33%);
|
|
84
|
+
right: calc((100% - var(--drop-zone-start, 33%)) / 2);
|
|
85
|
+
border-radius: $drop-zone-border-radius 0 0 $drop-zone-border-radius;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Two zones: second present zone gets right half
|
|
89
|
+
&:not(:has(.ltree-drop-child)) .ltree-drop-below,
|
|
90
|
+
&:not(:has(.ltree-drop-below)) .ltree-drop-child,
|
|
91
|
+
&:not(:has(.ltree-drop-above)) .ltree-drop-child {
|
|
92
|
+
left: calc(var(--drop-zone-start, 33%) + (100% - var(--drop-zone-start, 33%)) / 2);
|
|
93
|
+
right: 0;
|
|
94
|
+
border-radius: 0 $drop-zone-border-radius $drop-zone-border-radius 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Single zone: full width
|
|
98
|
+
&:not(:has(.ltree-drop-below)):not(:has(.ltree-drop-child)) .ltree-drop-above,
|
|
99
|
+
&:not(:has(.ltree-drop-above)):not(:has(.ltree-drop-child)) .ltree-drop-below,
|
|
100
|
+
&:not(:has(.ltree-drop-above)):not(:has(.ltree-drop-below)) .ltree-drop-child {
|
|
101
|
+
left: var(--drop-zone-start, 33%);
|
|
102
|
+
right: 0;
|
|
103
|
+
border-radius: $drop-zone-border-radius;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
:root {
|
|
78
108
|
--tree-node-indent-per-level: #{$tree-node-indent-per-level};
|
|
79
109
|
--ltree-primary: #{$primary-color};
|
|
@@ -103,8 +133,15 @@ $body-color: #212529 !default;
|
|
|
103
133
|
}
|
|
104
134
|
}
|
|
105
135
|
|
|
136
|
+
// Virtual scroll container
|
|
137
|
+
.ltree-virtual-scroll {
|
|
138
|
+
overflow-y: auto !important;
|
|
139
|
+
overflow-x: visible;
|
|
140
|
+
overscroll-behavior: contain;
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
// Ensure all tree ancestors allow overflow for drop zones
|
|
107
|
-
.ltree-tree,
|
|
144
|
+
.ltree-tree:not(.ltree-virtual-scroll),
|
|
108
145
|
.ltree-node,
|
|
109
146
|
.ltree-node-row,
|
|
110
147
|
.ltree-children {
|
|
@@ -579,12 +616,8 @@ $body-color: #212529 !default;
|
|
|
579
616
|
}
|
|
580
617
|
|
|
581
618
|
// Drop Zone Container - wraps all zones with CSS variable for start position
|
|
619
|
+
// Position is set inline (fixed) by Tree.svelte for the floating overlay
|
|
582
620
|
.ltree-drop-zones {
|
|
583
|
-
position: absolute;
|
|
584
|
-
top: 0;
|
|
585
|
-
left: 0;
|
|
586
|
-
right: 0;
|
|
587
|
-
bottom: 0;
|
|
588
621
|
pointer-events: none; // Let children handle pointer events
|
|
589
622
|
overflow: visible;
|
|
590
623
|
}
|
|
@@ -668,6 +701,14 @@ $body-color: #212529 !default;
|
|
|
668
701
|
transform: translateY(100%);
|
|
669
702
|
// border-radius: 0 0 4px 0;
|
|
670
703
|
}
|
|
704
|
+
|
|
705
|
+
// Zone expansion: when a zone is hidden, remaining zones fill the gap
|
|
706
|
+
&:not(:has(.ltree-drop-child)) .ltree-drop-below {
|
|
707
|
+
right: 0;
|
|
708
|
+
}
|
|
709
|
+
&:not(:has(.ltree-drop-below)) .ltree-drop-child {
|
|
710
|
+
left: var(--drop-zone-start, 33%);
|
|
711
|
+
}
|
|
671
712
|
}
|
|
672
713
|
|
|
673
714
|
// Layout: "above" - All 3 zones in a row above the node
|
|
@@ -696,6 +737,9 @@ $body-color: #212529 !default;
|
|
|
696
737
|
right: 0;
|
|
697
738
|
border-radius: 0 $drop-zone-border-radius $drop-zone-border-radius 0;
|
|
698
739
|
}
|
|
740
|
+
|
|
741
|
+
// Zone expansion: when a zone is hidden, remaining zones fill the gap
|
|
742
|
+
@include zone-horizontal-expansion;
|
|
699
743
|
}
|
|
700
744
|
|
|
701
745
|
// Layout: "below" - All 3 zones in a row below the node
|
|
@@ -724,6 +768,9 @@ $body-color: #212529 !default;
|
|
|
724
768
|
right: 0;
|
|
725
769
|
border-radius: 0 $drop-zone-border-radius $drop-zone-border-radius 0;
|
|
726
770
|
}
|
|
771
|
+
|
|
772
|
+
// Zone expansion: when a zone is hidden, remaining zones fill the gap
|
|
773
|
+
@include zone-horizontal-expansion;
|
|
727
774
|
}
|
|
728
775
|
|
|
729
776
|
// Layout: "wave" - Zones stacked vertically (above/child/below)
|