@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.
Files changed (51) hide show
  1. package/README.md +106 -117
  2. package/ai/INDEX.txt +310 -0
  3. package/ai/advanced-patterns.txt +506 -0
  4. package/ai/basic-setup.txt +336 -0
  5. package/ai/context-menu.txt +349 -0
  6. package/ai/data-handling.txt +390 -0
  7. package/ai/drag-drop.txt +397 -0
  8. package/ai/events-callbacks.txt +382 -0
  9. package/ai/import-patterns.txt +271 -0
  10. package/ai/performance.txt +349 -0
  11. package/ai/search-features.txt +359 -0
  12. package/ai/styling-theming.txt +354 -0
  13. package/ai/tree-editing.txt +423 -0
  14. package/ai/typescript-types.txt +357 -0
  15. package/dist/components/Node.svelte +47 -40
  16. package/dist/components/Node.svelte.d.ts +1 -1
  17. package/dist/components/Tree.svelte +384 -1479
  18. package/dist/components/Tree.svelte.d.ts +30 -28
  19. package/dist/components/TreeProvider.svelte +28 -0
  20. package/dist/components/TreeProvider.svelte.d.ts +28 -0
  21. package/dist/constants.generated.d.ts +1 -1
  22. package/dist/constants.generated.js +1 -1
  23. package/dist/core/TreeController.svelte.d.ts +353 -0
  24. package/dist/core/TreeController.svelte.js +1503 -0
  25. package/dist/core/createTreeController.d.ts +9 -0
  26. package/dist/core/createTreeController.js +11 -0
  27. package/dist/global-api.d.ts +1 -1
  28. package/dist/global-api.js +5 -5
  29. package/dist/index.d.ts +10 -6
  30. package/dist/index.js +7 -3
  31. package/dist/logger.d.ts +7 -6
  32. package/dist/logger.js +0 -2
  33. package/dist/ltree/indexer.js +2 -4
  34. package/dist/ltree/ltree-node.svelte.d.ts +2 -1
  35. package/dist/ltree/ltree-node.svelte.js +1 -0
  36. package/dist/ltree/ltree.svelte.d.ts +1 -1
  37. package/dist/ltree/ltree.svelte.js +168 -175
  38. package/dist/ltree/types.d.ts +12 -8
  39. package/dist/perf-logger.d.ts +2 -1
  40. package/dist/perf-logger.js +0 -2
  41. package/dist/styles/main.scss +78 -78
  42. package/dist/styles.css +41 -41
  43. package/dist/styles.css.map +1 -1
  44. package/dist/vendor/loglevel/index.d.ts +55 -2
  45. package/dist/vendor/loglevel/prefix.d.ts +23 -2
  46. package/package.json +96 -95
  47. package/dist/ltree/ltree-demo.d.ts +0 -2
  48. package/dist/ltree/ltree-demo.js +0 -90
  49. package/dist/vendor/loglevel/loglevel-esm.d.ts +0 -2
  50. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +0 -7
  51. 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
- 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) {
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.shouldDisplayDebugInformation);
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: opts?.isSorted ?? false,
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[_idMember] : undefined;
178
- node.path = _pathMember ? row[_pathMember] : undefined;
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[_parentPathMember];
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[_levelMember];
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[_isExpandedMember];
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[_isSelectableMember];
194
+ node.isSelectable = getField(row, _isSelectableMember);
195
195
  if (!shouldCalculateIsDraggable)
196
- node.isDraggable = row[_isDraggableMember];
196
+ node.isDraggable = getField(row, _isDraggableMember);
197
+ if (!shouldCalculateIsCollapsible)
198
+ node.isCollapsible = getField(row, _isCollapsibleMember);
197
199
  if (!shouldCalculateIsDropAllowed)
198
- node.isDropAllowed = row[_isDropAllowedMember];
200
+ node.isDropAllowed = getField(row, _isDropAllowedMember);
199
201
  if (!shouldCalculateAllowedDropPositions)
200
- node.allowedDropPositions = row[_allowedDropPositionsMember];
202
+ node.allowedDropPositions = getField(row, _allowedDropPositionsMember);
201
203
  if (!shouldCalculateHasChildren)
202
- node.hasChildren = row[_hasChildrenMember];
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
- if (isEmptyString(nodePath))
415
- flatTreeNodes.forEach((row) => {
416
- if (!row.isExpanded) {
417
- row.isExpanded = true;
418
- _bumpRev(row);
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`, flatTreeNodes.length);
436
+ perfEnd(`[${_treeId}] expandAll`);
423
437
  },
424
438
  collapseAll(nodePath) {
425
439
  perfStart(`[${_treeId}] collapseAll`);
426
- if (isEmptyString(nodePath))
427
- flatTreeNodes.forEach((row) => {
428
- if (row.isExpanded) {
429
- row.isExpanded = false;
430
- _bumpRev(row);
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`, flatTreeNodes.length);
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[_displayValueMember];
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[_searchValueMember];
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[_allowedDropPositionsMember];
559
+ return getField(node.data, _allowedDropPositionsMember);
538
560
  }
539
561
  return node.allowedDropPositions;
540
562
  },
541
- refresh() {
542
- this._emitTreeChanged();
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
- getNodeSignal(id) {
545
- return nodeSignals.get(id);
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
- bumpNodeRev(node) {
548
- _bumpNodeRev(node);
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[this.orderMember] ?? 0;
598
- const bOrder = b.data[this.orderMember] ?? 0;
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
- const node = this.getNodeByPath(path);
624
- if (node) {
625
- _bumpNodeRev(node);
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: 'above', 'below', or 'child'
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 (above or below)
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
- sourceNode.parentPath = newParentPath || null;
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 above/below, calculate order
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?.[this.orderMember] ?? 0;
714
- if (position === 'above') {
715
- // Find order value just below target
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?.[this.orderMember] !== undefined)
718
- .map(s => s.data[this.orderMember])
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 belowOrder = siblingOrders[0] ?? targetOrder - 20;
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;
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 above target
757
+ // Find order value just after target
728
758
  const siblingOrders = siblings
729
- .filter(s => s !== sourceNode && s.data?.[this.orderMember] !== undefined)
730
- .map(s => s.data[this.orderMember])
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 aboveOrder = siblingOrders[0] ?? targetOrder + 20;
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;
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}${pathSegment}` : pathSegment;
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 = pathSegment;
847
- newNode.parentPath = parentPath || null;
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 (structural change)
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
- // Data-only change — signal only, skip _emitTreeChanged for O(1) update
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 ('above' or 'below')
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 siblingOrder = siblingNode.data?.[this.orderMember] ?? 0;
1025
- if (position === 'above') {
1026
- // Find order value just below sibling
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?.[this.orderMember] !== undefined)
1029
- .map(s => s.data[this.orderMember])
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 belowOrder = siblingOrders[0] ?? siblingOrder - 20;
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;
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 above sibling
1049
+ // Find order value just after sibling
1039
1050
  const siblingOrders = siblings
1040
- .filter(s => s !== rootNode && s.data?.[this.orderMember] !== undefined)
1041
- .map(s => s.data[this.orderMember])
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 aboveOrder = siblingOrders[0] ?? siblingOrder + 20;
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;
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
- const shouldExpand = pathSet.has(node.path);
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
  }