@ixfx/components 0.13.5 → 0.13.7

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/dist/miller.js CHANGED
@@ -39,6 +39,8 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
39
39
  }
40
40
  return items;
41
41
  } });
42
+ this.#selectOrigin = `user`;
43
+ this.#selectSilent = false;
42
44
  this.#onKeyDown = (event) => {
43
45
  if (this.search.isOpen) return;
44
46
  const currentDepth = this.#getTickledDepthOrDefault();
@@ -403,14 +405,47 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
403
405
  * Programmatically navigate to a node by key path, relative to the current
404
406
  * view root (rebased or full tree).
405
407
  */
406
- navigateTo(keys) {
408
+ navigateTo(keys, options = {}) {
407
409
  const display = this.#displayRoot;
408
410
  if (!display?.children) return false;
411
+ const prevOrigin = this.#selectOrigin;
412
+ const prevSilent = this.#selectSilent;
413
+ this.#selectOrigin = `programmatic`;
414
+ this.#selectSilent = options.silent === true;
415
+ try {
416
+ return this.#navigateTo(display.children, keys, options.reveal ?? `edge`);
417
+ } finally {
418
+ this.#selectOrigin = prevOrigin;
419
+ this.#selectSilent = prevSilent;
420
+ }
421
+ }
422
+ /**
423
+ * Re-sync the element with children arrays patched in place (see
424
+ * `MillerTreeController.sync()`): columns whose selected node was removed are
425
+ * closed, selection entries for removed nodes are dropped, then it re-renders.
426
+ */
427
+ invalidateStructure() {
428
+ const present = /* @__PURE__ */ new Set();
429
+ for (const col of this._columns) for (const n of col) present.add(n);
430
+ for (let depth = 0; depth < this._columns.length; depth++) {
431
+ const selected = this._selectedByDepth.get(depth);
432
+ if (selected && !this._columns[depth].includes(selected)) {
433
+ this._columns = this._columns.slice(0, depth + 1);
434
+ for (const d of [...this._selectedByDepth.keys()]) if (d >= depth) this._selectedByDepth.delete(d);
435
+ break;
436
+ }
437
+ }
438
+ for (const n of [...this._selectedNodes]) if (!present.has(n)) this._selectedNodes.delete(n);
439
+ this.requestUpdate();
440
+ }
441
+ #selectOrigin;
442
+ #selectSilent;
443
+ #navigateTo(rootColumn, keys, reveal) {
409
444
  this.#abortPending();
410
445
  this._selectedByDepth.clear();
411
446
  this.tickled.clearTickled();
412
- const newColumns = [display.children];
413
- let currentColumn = display.children;
447
+ const newColumns = [rootColumn];
448
+ let currentColumn = rootColumn;
414
449
  let success = true;
415
450
  let lastNode;
416
451
  let lastDepth = 0;
@@ -438,7 +473,7 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
438
473
  if (leafKey) this._selectedKeys = /* @__PURE__ */ new Set([leafKey]);
439
474
  }
440
475
  this.requestUpdate();
441
- this.#scrollNavigatedItemIntoView();
476
+ this.#scrollNavigatedItemIntoView(reveal === `center-if-hidden` ? `auto` : `smooth`, reveal);
442
477
  return success;
443
478
  }
444
479
  /**
@@ -718,9 +753,11 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
718
753
  return true;
719
754
  }
720
755
  #dispatchSelectEvent(previous) {
756
+ if (this.#selectSilent) return;
721
757
  const detail = {
722
758
  selected: new Set(this._selectedNodes),
723
- previous
759
+ previous,
760
+ origin: this.#selectOrigin
724
761
  };
725
762
  this.dispatchEvent(new CustomEvent(`select`, {
726
763
  detail,
@@ -990,13 +1027,26 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
990
1027
  index
991
1028
  };
992
1029
  }
993
- #scrollNavigatedItemIntoView(behavior = `smooth`) {
1030
+ #scrollNavigatedItemIntoView(behavior = `smooth`, reveal = `edge`) {
994
1031
  requestAnimationFrame(() => {
995
1032
  const tickled = this.#getTickledNodeAndDepth();
996
1033
  if (!tickled) return;
997
1034
  const container = this.shadowRoot?.querySelectorAll(`.column`)[tickled.depth]?.querySelector(`.column-content`);
998
1035
  if (!container) return;
999
- container.querySelectorAll(`.item`)[tickled.index]?.scrollIntoView({
1036
+ const item = container.querySelectorAll(`.item`)[tickled.index];
1037
+ if (!item) return;
1038
+ if (reveal === `center-if-hidden`) {
1039
+ const rect = item.getBoundingClientRect();
1040
+ const box = container.getBoundingClientRect();
1041
+ const hidden = rect.top < box.top || rect.bottom > box.bottom;
1042
+ item.scrollIntoView({
1043
+ block: hidden ? `center` : `nearest`,
1044
+ inline: `nearest`,
1045
+ behavior
1046
+ });
1047
+ return;
1048
+ }
1049
+ item.scrollIntoView({
1000
1050
  block: `nearest`,
1001
1051
  behavior
1002
1052
  });
@@ -1562,6 +1612,10 @@ var MillerTreeController = class {
1562
1612
  #options;
1563
1613
  #nodeToTree = /* @__PURE__ */ new WeakMap();
1564
1614
  #identityToMillerNode = /* @__PURE__ */ new Map();
1615
+ #keyToTreeNode = /* @__PURE__ */ new Map();
1616
+ #identityToTree = /* @__PURE__ */ new Map();
1617
+ /** `valueToItem` results per source node identity; replaced wholesale by `sync()`. */
1618
+ #itemCache = /* @__PURE__ */ new WeakMap();
1565
1619
  #connectSignal;
1566
1620
  constructor(root, element, options) {
1567
1621
  this.#root = root;
@@ -1575,18 +1629,19 @@ var MillerTreeController = class {
1575
1629
  connect() {
1576
1630
  this.disconnect();
1577
1631
  this.#connectSignal = new AbortController();
1578
- const rootValue = this.#root.getValue();
1579
- const rootItem = this.#options.valueToItem(rootValue, this.#root);
1580
- const rootChildren = this.#childrenOf(this.#root);
1581
- this.#element.root = {
1582
- item: rootItem,
1583
- children: rootChildren
1584
- };
1585
- this.#element.loadChildren = ({ node }) => {
1632
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
1633
+ this.#identityToMillerNode.clear();
1634
+ this.#identityToTree.clear();
1635
+ this.#keyToTreeNode.clear();
1636
+ const rootNode = { item: this.#itemOf(this.#root) };
1637
+ this.#nodeToTree.set(rootNode, this.#root);
1638
+ rootNode.children = this.#childrenOf(this.#root);
1639
+ this.#element.root = rootNode;
1640
+ this.#element.loadChildren = this.#options.loadChildren ?? (({ node }) => {
1586
1641
  const treeNode = this.#nodeToTree.get(node);
1587
1642
  if (!treeNode) return [];
1588
1643
  return this.#childrenOf(treeNode);
1589
- };
1644
+ });
1590
1645
  }
1591
1646
  /** Remove all event listeners and release internal state. */
1592
1647
  disconnect() {
@@ -1606,46 +1661,110 @@ var MillerTreeController = class {
1606
1661
  *
1607
1662
  * Returns `false` if no matching node is found.
1608
1663
  */
1609
- navigateToValue(predicate) {
1664
+ navigateToValue(predicate, options) {
1610
1665
  const treePath = this.#findTreePath(this.#root, predicate);
1611
1666
  if (!treePath) return false;
1667
+ return this.#navigateToPath(treePath, options);
1668
+ }
1669
+ /**
1670
+ * Navigate using item keys **relative to the element's current view root**
1671
+ * (the real root when not rebased). Ancestors with unloaded children are
1672
+ * populated first. Returns `false` if a key is missing.
1673
+ */
1674
+ navigateToKeyPath(keys, options) {
1675
+ const base = this.#element.viewRoot ?? this.#element.root;
1676
+ if (!base || keys.length === 0) return false;
1677
+ this.#ensureChildren(base);
1678
+ let column = base.children ?? [];
1679
+ for (let i = 0; i < keys.length - 1; i++) {
1680
+ const node = column.find((n) => n.item.key === keys[i]);
1681
+ if (!node) return false;
1682
+ this.#ensureChildren(node);
1683
+ column = node.children ?? [];
1684
+ }
1685
+ return this.#element.navigateTo(keys, options);
1686
+ }
1687
+ /**
1688
+ * Navigate to the source node with this identity. Returns `false` when it is
1689
+ * unknown or lies outside a rebased view (keys are relative to the view root).
1690
+ */
1691
+ navigateToIdentity(identity, options) {
1692
+ const target = this.#treeNodeByIdentity(identity);
1693
+ if (!target) return false;
1694
+ const path = [];
1695
+ const rootId = this.#root.getIdentity();
1696
+ for (let n = target; n; n = n.getParent()) {
1697
+ path.unshift(n);
1698
+ if (n.getIdentity() === rootId) return this.#navigateToPath(path, options);
1699
+ }
1700
+ return false;
1701
+ }
1702
+ /** The source node whose item key is `key`, if it has been materialised. */
1703
+ lookupByKey(key) {
1704
+ return this.#keyToTreeNode.get(key);
1705
+ }
1706
+ /**
1707
+ * Re-derive children for every already-materialised node from the source tree
1708
+ * and patch them in place. Children arrays are mutated (not replaced) so open
1709
+ * columns keep their references, and `MillerNode`s are reused by key so
1710
+ * selection survives. A rebased `viewRoot` is reset only if its node was
1711
+ * removed. `loadChildren` is left alone. Refreshes the element once.
1712
+ */
1713
+ sync() {
1714
+ const root = this.#element.root;
1715
+ if (!root) return;
1716
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
1717
+ this.#identityToMillerNode.clear();
1718
+ this.#identityToTree.clear();
1719
+ this.#keyToTreeNode.clear();
1720
+ this.#nodeToTree.set(root, this.#root);
1721
+ root.item = this.#itemOf(this.#root);
1722
+ const kept = /* @__PURE__ */ new Set([root]);
1723
+ if (root.children) this.#syncChildren(this.#root, root.children, kept);
1724
+ const view = this.#element.viewRoot;
1725
+ if (view && !kept.has(view)) this.#element.viewRoot = void 0;
1726
+ this.#element.invalidateStructure();
1727
+ }
1728
+ #navigateToPath(treePath, options) {
1612
1729
  const base = this.#element.viewRoot ?? this.#element.root;
1613
1730
  if (!base) return false;
1614
1731
  let startIndex = 1;
1615
1732
  if (this.#element.viewRoot) {
1616
1733
  const mappedBase = this.#nodeToTree.get(base);
1617
1734
  if (!mappedBase) return false;
1618
- const baseIndex = treePath.indexOf(mappedBase);
1735
+ const baseId = mappedBase.getIdentity();
1736
+ const baseIndex = treePath.findIndex((n) => n.getIdentity() === baseId);
1619
1737
  if (baseIndex < 0) return false;
1620
1738
  startIndex = baseIndex + 1;
1621
- if (base.children === void 0) base.children = this.#childrenOf(mappedBase);
1622
1739
  }
1623
- let currentColumn = base.children ?? [];
1624
- const keys = [];
1625
- for (let i = startIndex; i < treePath.length; i++) {
1626
- const treeNode = treePath[i];
1627
- const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
1628
- keys.push(key);
1629
- if (i < treePath.length - 1) {
1630
- const millerNode = currentColumn.find((n) => n.item.key === key);
1631
- if (!millerNode) return false;
1632
- if (millerNode.children === void 0) {
1633
- const mapped = this.#nodeToTree.get(millerNode);
1634
- if (mapped) millerNode.children = this.#childrenOf(mapped);
1635
- }
1636
- currentColumn = millerNode.children ?? [];
1637
- }
1740
+ if (startIndex >= treePath.length) return false;
1741
+ return this.navigateToKeyPath(treePath.slice(startIndex).map((n) => this.#itemOf(n).key), options);
1742
+ }
1743
+ #ensureChildren(node) {
1744
+ if (node.children !== void 0) return;
1745
+ const mapped = this.#nodeToTree.get(node);
1746
+ if (mapped) node.children = this.#childrenOf(mapped);
1747
+ }
1748
+ #treeNodeByIdentity(identity) {
1749
+ return this.#identityToTree.get(identity);
1750
+ }
1751
+ #itemOf(treeNode) {
1752
+ const id = treeNode.getIdentity();
1753
+ let item = this.#itemCache.get(id);
1754
+ if (!item) {
1755
+ item = this.#options.valueToItem(treeNode.getValue(), treeNode);
1756
+ this.#itemCache.set(id, item);
1638
1757
  }
1639
- return this.#element.navigateTo(keys);
1758
+ return item;
1640
1759
  }
1641
1760
  /** Convert a TraversableTree node's children to MillerNode[]. */
1642
1761
  #childrenOf(treeNode) {
1643
1762
  return Array.from(treeNode.children(), (child) => this.#toMillerNode(child));
1644
1763
  }
1645
1764
  #toMillerNode(treeNode) {
1646
- const value = treeNode.getValue();
1647
1765
  const childList = [...treeNode.children()];
1648
- const userItem = this.#options.valueToItem(value, treeNode);
1766
+ for (const child of childList) this.#identityToTree.set(child.getIdentity(), child);
1767
+ const userItem = this.#itemOf(treeNode);
1649
1768
  const isLeaf = userItem.isLeaf ?? childList.length === 0;
1650
1769
  const millerNode = {
1651
1770
  item: {
@@ -1654,10 +1773,35 @@ var MillerTreeController = class {
1654
1773
  },
1655
1774
  children: isLeaf ? [] : void 0
1656
1775
  };
1657
- this.#nodeToTree.set(millerNode, treeNode);
1658
- this.#identityToMillerNode.set(treeNode.getIdentity(), millerNode);
1776
+ this.#register(millerNode, treeNode);
1659
1777
  return millerNode;
1660
1778
  }
1779
+ #register(node, treeNode) {
1780
+ this.#nodeToTree.set(node, treeNode);
1781
+ this.#identityToMillerNode.set(treeNode.getIdentity(), node);
1782
+ this.#identityToTree.set(treeNode.getIdentity(), treeNode);
1783
+ this.#keyToTreeNode.set(node.item.key, treeNode);
1784
+ }
1785
+ /** Patch `existing` (in place) to match the source children of `treeNode`; recurse into loaded nodes. */
1786
+ #syncChildren(treeNode, existing, kept) {
1787
+ const byKey = new Map(existing.map((n) => [n.item.key, n]));
1788
+ const next = Array.from(treeNode.children(), (child) => {
1789
+ const fresh = this.#toMillerNode(child);
1790
+ const old = byKey.get(fresh.item.key);
1791
+ if (!old) {
1792
+ kept.add(fresh);
1793
+ return fresh;
1794
+ }
1795
+ old.item = fresh.item;
1796
+ this.#register(old, child);
1797
+ kept.add(old);
1798
+ if (fresh.item.isLeaf) old.children = [];
1799
+ else if (old.children !== void 0) this.#syncChildren(child, old.children, kept);
1800
+ else old.children = void 0;
1801
+ return old;
1802
+ });
1803
+ existing.splice(0, existing.length, ...next);
1804
+ }
1661
1805
  /** DFS through the ixfx tree returning the path of nodes to the first match. */
1662
1806
  #findTreePath(node, predicate, ancestors = []) {
1663
1807
  const path = [...ancestors, node];
@@ -1027,6 +1027,19 @@ function treeScrollTopToReveal(params, index, scrollTop, topInset = 0) {
1027
1027
  if (bottom > scrollTop + params.viewportHeight) return Math.max(0, bottom - params.viewportHeight);
1028
1028
  return scrollTop;
1029
1029
  }
1030
+ /**
1031
+ * `scrollTop` that centres row `index` in the viewport, but only when the row is
1032
+ * not already fully visible (below `topInset` px of overlay). Otherwise returns
1033
+ * `scrollTop` unchanged.
1034
+ */
1035
+ function treeScrollTopToCenterIfHidden(params, index, scrollTop, topInset = 0) {
1036
+ if (index < 0 || index >= params.count) return scrollTop;
1037
+ const top = treeRowTop(params, index);
1038
+ const bottom = top + params.rowHeight;
1039
+ if (top >= scrollTop + topInset && bottom <= scrollTop + params.viewportHeight) return scrollTop;
1040
+ const centred = top + params.rowHeight / 2 - (params.viewportHeight + topInset) / 2;
1041
+ return Math.max(0, centred);
1042
+ }
1030
1043
  /** Rows that fit one viewport (PageUp / PageDown step), at least 1. */
1031
1044
  function treePageSize(params) {
1032
1045
  return Math.max(1, Math.floor(params.viewportHeight / treeStride(params)) - 1);
@@ -1166,6 +1179,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1166
1179
  this.#stickyDirty = true;
1167
1180
  this.#stickyRaf = 0;
1168
1181
  this.#stickyScrollBound = false;
1182
+ this.#selectOrigin = `user`;
1183
+ this.#selectSilent = false;
1169
1184
  this.#revealToken = 0;
1170
1185
  this.#onKeyDown = (event) => {
1171
1186
  if (this.search.isOpen) return;
@@ -1467,12 +1482,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1467
1482
  * container's `scrollTop` and `_scrollTop` together, so the very next render
1468
1483
  * already contains the target row — no frame where it is missing.
1469
1484
  */
1470
- #revealIndex(index) {
1485
+ #revealIndex(index, mode = `edge`) {
1471
1486
  const container = this.#container();
1472
1487
  if (!container) return;
1473
1488
  const params = this.#layoutParams(this.#getFlat().rows.length);
1474
1489
  const inset = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
1475
- const next = treeScrollTopToReveal(params, index, container.scrollTop, inset);
1490
+ const next = (mode === `center-if-hidden` ? treeScrollTopToCenterIfHidden : treeScrollTopToReveal)(params, index, container.scrollTop, inset);
1476
1491
  if (next === container.scrollTop && next === this._scrollTop) return;
1477
1492
  container.scrollTop = next;
1478
1493
  this._scrollTop = next;
@@ -2371,8 +2386,20 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2371
2386
  this.#dispatchSelect(prev);
2372
2387
  this.requestUpdate();
2373
2388
  }
2374
- navigateTo(keys) {
2389
+ navigateTo(keys, options = {}) {
2375
2390
  if (!this.root?.children) return false;
2391
+ const prevOrigin = this.#selectOrigin;
2392
+ const prevSilent = this.#selectSilent;
2393
+ this.#selectOrigin = `programmatic`;
2394
+ this.#selectSilent = options.silent === true;
2395
+ try {
2396
+ return this.#navigateTo(keys, options.reveal ?? `edge`);
2397
+ } finally {
2398
+ this.#selectOrigin = prevOrigin;
2399
+ this.#selectSilent = prevSilent;
2400
+ }
2401
+ }
2402
+ #navigateTo(keys, reveal) {
2376
2403
  this.#abortPending();
2377
2404
  this.#clearSelectionInternal();
2378
2405
  this.tickled.clearTickled();
@@ -2393,7 +2420,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2393
2420
  }
2394
2421
  this.#stickyDirty = true;
2395
2422
  this.requestUpdate();
2396
- this.#scrollKeyIntoView(keys.at(-1));
2423
+ this.#scrollKeyIntoView(keys.at(-1), reveal);
2397
2424
  return true;
2398
2425
  }
2399
2426
  navigateToNode(predicate) {
@@ -2449,11 +2476,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2449
2476
  this._selectedNodes.clear();
2450
2477
  this.selectedNode = void 0;
2451
2478
  }
2479
+ #selectOrigin;
2480
+ #selectSilent;
2452
2481
  #dispatchSelect(previous) {
2453
2482
  this._selectionVersion++;
2483
+ if (this.#selectSilent) return;
2454
2484
  const detail = {
2455
2485
  selected: new Set(this._selectedNodes),
2456
- previous
2486
+ previous,
2487
+ origin: this.#selectOrigin
2457
2488
  };
2458
2489
  this.dispatchEvent(new CustomEvent(`select`, {
2459
2490
  detail,
@@ -2788,7 +2819,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2788
2819
  get _pendingNode() {
2789
2820
  return this.#pendingNode;
2790
2821
  }
2791
- #scrollKeyIntoView(key) {
2822
+ #scrollKeyIntoView(key, mode = `edge`) {
2823
+ if (mode === `center-if-hidden`) {
2824
+ this.updateComplete.then(() => {
2825
+ this.#measureMetrics();
2826
+ const idx = this.#getFlat().indexByKey.get(key);
2827
+ if (idx !== void 0) this.#revealIndex(idx, mode);
2828
+ });
2829
+ return;
2830
+ }
2792
2831
  if (this.#virtualActive()) {
2793
2832
  this.updateComplete.then(() => {
2794
2833
  const idx = this.#getFlat().indexByKey.get(key);
@@ -3553,6 +3592,9 @@ var TreeController = class {
3553
3592
  #options;
3554
3593
  #nodeToTree = /* @__PURE__ */ new WeakMap();
3555
3594
  #identityToTreeNode = /* @__PURE__ */ new Map();
3595
+ #keyToTreeNode = /* @__PURE__ */ new Map();
3596
+ /** `valueToItem` results per source node identity; replaced wholesale by `sync()`. */
3597
+ #itemCache = /* @__PURE__ */ new WeakMap();
3556
3598
  #connectSignal;
3557
3599
  constructor(root, element, options) {
3558
3600
  this.#root = root;
@@ -3563,13 +3605,13 @@ var TreeController = class {
3563
3605
  this.disconnect();
3564
3606
  this.#connectSignal = new AbortController();
3565
3607
  const { signal } = this.#connectSignal;
3566
- const rootValue = this.#root.getValue();
3567
- const rootItem = this.#options.valueToItem(rootValue, this.#root);
3568
- const rootChildren = this.#childrenOf(this.#root);
3569
- this.#element.root = {
3570
- item: rootItem,
3571
- children: rootChildren
3572
- };
3608
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
3609
+ this.#identityToTreeNode.clear();
3610
+ this.#keyToTreeNode.clear();
3611
+ const rootNode = { item: this.#itemOf(this.#root) };
3612
+ this.#nodeToTree.set(rootNode, this.#root);
3613
+ rootNode.children = this.#childrenOf(this.#root);
3614
+ this.#element.root = rootNode;
3573
3615
  this.#element.addEventListener(`expand`, (e) => {
3574
3616
  const { node } = e.detail;
3575
3617
  const treeNode = this.#nodeToTree.get(node);
@@ -3583,45 +3625,101 @@ var TreeController = class {
3583
3625
  this.#connectSignal?.abort();
3584
3626
  this.#connectSignal = void 0;
3585
3627
  }
3586
- navigateToValue(predicate) {
3587
- const treePath = this.#findTreePath(this.#root, predicate);
3588
- if (!treePath) return false;
3628
+ /**
3629
+ * Re-derive children for every already-materialised node from the source tree
3630
+ * and patch them in place. Existing `TreeNode` objects are reused by key, so
3631
+ * expansion and selection survive; only added and removed nodes change. Calls
3632
+ * `invalidateStructure()` once.
3633
+ */
3634
+ sync() {
3635
+ const root = this.#element.root;
3636
+ if (!root) return;
3637
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
3638
+ this.#identityToTreeNode.clear();
3639
+ this.#keyToTreeNode.clear();
3640
+ this.#nodeToTree.set(root, this.#root);
3641
+ root.item = this.#itemOf(this.#root);
3642
+ root.children = this.#syncChildren(this.#root, root.children ?? []);
3643
+ this.#element.invalidateStructure();
3644
+ }
3645
+ /** The source node whose item key is `key`, if it has been materialised. */
3646
+ lookupByKey(key) {
3647
+ return this.#keyToTreeNode.get(key);
3648
+ }
3649
+ /** Navigate using item keys (root excluded), without searching the source tree. */
3650
+ navigateToKeyPath(keys, options) {
3651
+ if (keys.length === 0) return false;
3589
3652
  let currentColumn = this.#element.root?.children ?? [];
3590
- const keys = [];
3591
- for (let i = 1; i < treePath.length; i++) {
3592
- const treeNode = treePath[i];
3593
- const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
3594
- keys.push(key);
3595
- if (i < treePath.length - 1) {
3596
- const node = currentColumn.find((n) => n.item.key === key);
3597
- if (!node) return false;
3598
- if (node.children === void 0) {
3599
- const mapped = this.#nodeToTree.get(node);
3600
- if (mapped) {
3601
- node.children = this.#childrenOf(mapped);
3602
- this.#element.invalidateStructure();
3603
- }
3653
+ for (let i = 0; i < keys.length - 1; i++) {
3654
+ const node = currentColumn.find((n) => n.item.key === keys[i]);
3655
+ if (!node) return false;
3656
+ if (node.children === void 0) {
3657
+ const mapped = this.#nodeToTree.get(node);
3658
+ if (mapped) {
3659
+ node.children = this.#childrenOf(mapped);
3660
+ this.#element.invalidateStructure();
3604
3661
  }
3605
- currentColumn = node.children ?? [];
3606
3662
  }
3663
+ currentColumn = node.children ?? [];
3664
+ }
3665
+ return this.#element.navigateTo(keys, options);
3666
+ }
3667
+ /** Navigate to the source node with this identity, walking up via `getParent()`. */
3668
+ navigateToIdentity(identity, options) {
3669
+ let node = this.#identityToTreeNode.get(identity);
3670
+ if (!node) return false;
3671
+ const keys = [];
3672
+ const rootId = this.#root.getIdentity();
3673
+ while (node && node.getIdentity() !== rootId) {
3674
+ keys.unshift(this.#itemOf(node).key);
3675
+ node = node.getParent();
3607
3676
  }
3608
- return this.#element.navigateTo(keys);
3677
+ return node !== void 0 && this.navigateToKeyPath(keys, options);
3678
+ }
3679
+ navigateToValue(predicate, options) {
3680
+ const treePath = this.#findTreePath(this.#root, predicate);
3681
+ if (!treePath) return false;
3682
+ return this.navigateToKeyPath(treePath.slice(1).map((n) => this.#itemOf(n).key), options);
3683
+ }
3684
+ #itemOf(treeNode, isLeaf) {
3685
+ const id = treeNode.getIdentity();
3686
+ let userItem = this.#itemCache.get(id);
3687
+ if (!userItem) {
3688
+ userItem = this.#options.valueToItem(treeNode.getValue(), treeNode);
3689
+ this.#itemCache.set(id, userItem);
3690
+ }
3691
+ return isLeaf === void 0 ? userItem : {
3692
+ ...userItem,
3693
+ isLeaf: userItem.isLeaf ?? isLeaf
3694
+ };
3609
3695
  }
3610
3696
  #childrenOf(treeNode) {
3611
3697
  return Array.from(treeNode.children(), (child) => this.#toTreeNode(child));
3612
3698
  }
3699
+ #register(node, treeNode) {
3700
+ this.#nodeToTree.set(node, treeNode);
3701
+ this.#identityToTreeNode.set(treeNode.getIdentity(), treeNode);
3702
+ this.#keyToTreeNode.set(node.item.key, treeNode);
3703
+ }
3613
3704
  #toTreeNode(treeNode) {
3614
- const value = treeNode.getValue();
3615
3705
  const childList = [...treeNode.children()];
3616
- const userItem = this.#options.valueToItem(value, treeNode);
3617
- const isLeaf = userItem.isLeaf ?? childList.length === 0;
3618
- const treeNodeMap = { item: {
3619
- ...userItem,
3620
- isLeaf
3621
- } };
3622
- this.#nodeToTree.set(treeNodeMap, treeNode);
3623
- this.#identityToTreeNode.set(treeNode.getIdentity(), treeNode);
3624
- return treeNodeMap;
3706
+ for (const child of childList) this.#identityToTreeNode.set(child.getIdentity(), child);
3707
+ const node = { item: this.#itemOf(treeNode, childList.length === 0) };
3708
+ this.#register(node, treeNode);
3709
+ return node;
3710
+ }
3711
+ /** Patch `existing` to match the source children of `treeNode`; recurse into materialised nodes. */
3712
+ #syncChildren(treeNode, existing) {
3713
+ const byKey = new Map(existing.map((n) => [n.item.key, n]));
3714
+ return Array.from(treeNode.children(), (child) => {
3715
+ const fresh = this.#toTreeNode(child);
3716
+ const old = byKey.get(fresh.item.key);
3717
+ if (!old) return fresh;
3718
+ old.item = fresh.item;
3719
+ this.#register(old, child);
3720
+ if (old.children !== void 0) old.children = this.#syncChildren(child, old.children);
3721
+ return old;
3722
+ });
3625
3723
  }
3626
3724
  #findTreePath(node, predicate, ancestors = []) {
3627
3725
  const path = [...ancestors, node];
@@ -213,6 +213,14 @@ type TreeDropEvent = CustomEvent<TreeDropDetail>;
213
213
  type TreeDragEndEvent = CustomEvent<{
214
214
  dropped: boolean;
215
215
  }>;
216
+ /** How `navigateTo` scrolls its target: edge-align it, or centre it when it is off screen. */
217
+ type TreeRevealMode = `edge` | `center-if-hidden`;
218
+ type TreeNavigateOptions = {
219
+ /** Scroll behaviour for the target (default `edge`). */
220
+ reveal?: TreeRevealMode;
221
+ /** Suppress the `select` event this navigation would otherwise fire. */
222
+ silent?: boolean;
223
+ };
216
224
  /**
217
225
  * Fired when the selection set changes by any means (click, keyboard, programmatic).
218
226
  * Event name: `select`
@@ -222,6 +230,11 @@ type TreeSelectDetail = {
222
230
  readonly selected: ReadonlySet<TreeNode>;
223
231
  /** The previous selection set (empty set if there was no prior selection). */
224
232
  readonly previous: ReadonlySet<TreeNode>;
233
+ /**
234
+ * What caused the change: `programmatic` for `navigateTo` and friends, `user`
235
+ * otherwise. Absent on components that do not distinguish the two.
236
+ */
237
+ readonly origin?: `user` | `programmatic`;
225
238
  };
226
239
  /**
227
240
  * Fired when an item enters cursor focus — either the pointer hovers over it, or
@@ -408,4 +421,4 @@ interface TreeSearchHost extends TreeComponent {
408
421
  readonly updateComplete: Promise<boolean>;
409
422
  }
410
423
  //#endregion
411
- export { TreeDataModelOptions as A, TreeItemDblClickEvent as C, TreeTickleDetail as D, TreeSelectEvent as E, ListDragPreviewSourceResolver as F, ListDragSource as I, ListNativeDragStartDetail as L, ListDragPreview as M, ListDragPreviewContext as N, TreeTickleEvent as O, ListDragPreviewRenderer as P, ListNativeDragStartEvent as R, TreeItemDblClickDetail as S, TreeSelectDetail as T, TreeDropTarget as _, TreeComponent as a, TreeItemClickDetail as b, TreeDragContext as c, TreeDragStartEvent as d, TreeDropApplyContext as f, TreeDropPosition as g, TreeDropEvent as h, TreeCollapseEvent as i, ListDragEntry as j, TreeDataModel as k, TreeDragEndEvent as l, TreeDropDetail as m, TreeActivateEvent as n, TreeComponentEventMap as o, TreeDropContext as p, TreeCollapseDetail as r, TreeDndMode as s, TreeActivateDetail as t, TreeDragOverEvent as u, TreeExpandDetail as v, TreeSearchHost as w, TreeItemClickEvent as x, TreeExpandEvent as y };
424
+ export { TreeTickleEvent as A, ListNativeDragStartEvent as B, TreeItemDblClickEvent as C, TreeSelectDetail as D, TreeSearchHost as E, ListDragPreviewContext as F, ListDragPreviewRenderer as I, ListDragPreviewSourceResolver as L, TreeDataModelOptions as M, ListDragEntry as N, TreeSelectEvent as O, ListDragPreview as P, ListDragSource as R, TreeItemDblClickDetail as S, TreeRevealMode as T, TreeDropTarget as _, TreeComponent as a, TreeItemClickDetail as b, TreeDragContext as c, TreeDragStartEvent as d, TreeDropApplyContext as f, TreeDropPosition as g, TreeDropEvent as h, TreeCollapseEvent as i, TreeDataModel as j, TreeTickleDetail as k, TreeDragEndEvent as l, TreeDropDetail as m, TreeActivateEvent as n, TreeComponentEventMap as o, TreeDropContext as p, TreeCollapseDetail as r, TreeDndMode as s, TreeActivateDetail as t, TreeDragOverEvent as u, TreeExpandDetail as v, TreeNavigateOptions as w, TreeItemClickEvent as x, TreeExpandEvent as y, ListNativeDragStartDetail as z };
package/dist/tree.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as TreeDataModelOptions, D as TreeTickleDetail, E as TreeSelectEvent, O as TreeTickleEvent, T as TreeSelectDetail, _ as TreeDropTarget, b as TreeItemClickDetail, c as TreeDragContext, d as TreeDragStartEvent, g as TreeDropPosition, h as TreeDropEvent, i as TreeCollapseEvent, k as TreeDataModel, l as TreeDragEndEvent, m as TreeDropDetail, n as TreeActivateEvent, o as TreeComponentEventMap, p as TreeDropContext, r as TreeCollapseDetail, s as TreeDndMode, t as TreeActivateDetail, u as TreeDragOverEvent, v as TreeExpandDetail, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-Cj8V3uar.js";
1
+ import { A as TreeTickleEvent, D as TreeSelectDetail, M as TreeDataModelOptions, O as TreeSelectEvent, T as TreeRevealMode, _ as TreeDropTarget, b as TreeItemClickDetail, c as TreeDragContext, d as TreeDragStartEvent, g as TreeDropPosition, h as TreeDropEvent, i as TreeCollapseEvent, j as TreeDataModel, k as TreeTickleDetail, l as TreeDragEndEvent, m as TreeDropDetail, n as TreeActivateEvent, o as TreeComponentEventMap, p as TreeDropContext, r as TreeCollapseDetail, s as TreeDndMode, t as TreeActivateDetail, u as TreeDragOverEvent, v as TreeExpandDetail, w as TreeNavigateOptions, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-Tslymyyl.js";
2
2
  import { a as TreeItem, n as TreeExclusivity, o as TreeNode, r as TreeFilterPredicate, s as TreeSelectionFilter, t as LoadChildrenCallback } from "./types-DQ7KOUIj.js";
3
- import { a as TreeDropVisibleRow, c as TreeListElement, d as TreeBaseElementEventMap, f as TreeVirtualize, i as TreeDropRowInfo, l as TreeCheckboxState, n as TreeDropControllerConfig, o as TreeController, p as TreeConfig, r as TreeDropResolvedDrag, s as TreeControllerOptions, t as TreeDropController, u as TreeBaseElement } from "./index-CCC0G1nO.js";
4
- export { type LoadChildrenCallback, type TreeActivateDetail, type TreeActivateEvent, TreeBaseElement, TreeBaseElementEventMap, type TreeCheckboxState, type TreeCollapseDetail, type TreeCollapseEvent, type TreeComponentEventMap, TreeConfig, TreeController, TreeControllerOptions, TreeDataModel, type TreeDataModelOptions, type TreeDndMode, type TreeDragContext, type TreeDragEndEvent, type TreeDragOverEvent, type TreeDragStartEvent, type TreeDropContext, TreeDropController, TreeDropControllerConfig, type TreeDropDetail, type TreeDropEvent, type TreeDropPosition, TreeDropResolvedDrag, TreeDropRowInfo, type TreeDropTarget, TreeDropVisibleRow, type TreeExclusivity, type TreeExpandDetail, type TreeExpandEvent, type TreeFilterPredicate, type TreeItem, type TreeItemClickDetail, type TreeItemClickEvent, TreeListElement, type TreeNode, type TreeSelectDetail, type TreeSelectEvent, type TreeSelectionFilter, type TreeTickleDetail, type TreeTickleEvent, TreeVirtualize };
3
+ import { a as TreeDropVisibleRow, c as TreeListElement, d as TreeBaseElementEventMap, f as TreeVirtualize, i as TreeDropRowInfo, l as TreeCheckboxState, n as TreeDropControllerConfig, o as TreeController, p as TreeConfig, r as TreeDropResolvedDrag, s as TreeControllerOptions, t as TreeDropController, u as TreeBaseElement } from "./index-BG8E13J5.js";
4
+ export { type LoadChildrenCallback, type TreeActivateDetail, type TreeActivateEvent, TreeBaseElement, TreeBaseElementEventMap, type TreeCheckboxState, type TreeCollapseDetail, type TreeCollapseEvent, type TreeComponentEventMap, TreeConfig, TreeController, TreeControllerOptions, TreeDataModel, type TreeDataModelOptions, type TreeDndMode, type TreeDragContext, type TreeDragEndEvent, type TreeDragOverEvent, type TreeDragStartEvent, type TreeDropContext, TreeDropController, TreeDropControllerConfig, type TreeDropDetail, type TreeDropEvent, type TreeDropPosition, TreeDropResolvedDrag, TreeDropRowInfo, type TreeDropTarget, TreeDropVisibleRow, type TreeExclusivity, type TreeExpandDetail, type TreeExpandEvent, type TreeFilterPredicate, type TreeItem, type TreeItemClickDetail, type TreeItemClickEvent, TreeListElement, type TreeNavigateOptions, type TreeNode, type TreeRevealMode, type TreeSelectDetail, type TreeSelectEvent, type TreeSelectionFilter, type TreeTickleDetail, type TreeTickleEvent, TreeVirtualize };
package/dist/tree.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-Df00opxv.js";
1
+ import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-BjrfaX7d.js";
2
2
  export { TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeListElement };
@@ -1,6 +1,6 @@
1
- import { F as ListDragPreviewSourceResolver, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent } from "./tree-component-Cj8V3uar.js";
1
+ import { B as ListNativeDragStartEvent, I as ListDragPreviewRenderer, L as ListDragPreviewSourceResolver } from "./tree-component-Tslymyyl.js";
2
2
  import { n as ListViewportChangeEvent } from "./list-viewport-types-BRZsLcvp.js";
3
- import { S as ListSearchController } from "./index-BYIRgv49.js";
3
+ import { S as ListSearchController } from "./index-CO6m-YC0.js";
4
4
  import { i as ListInteractionMode, l as ListSelectionMode, r as ListElementEventMap } from "./list-selection-types-PjKdhF-a.js";
5
5
  import "./editable-label-rzXkjf1H.js";
6
6
  import { LitElement, PropertyValues } from "lit";
@@ -1,5 +1,5 @@
1
- import { I as ListDragSource, L as ListNativeDragStartDetail, M as ListDragPreview, N as ListDragPreviewContext, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent, j as ListDragEntry } from "./tree-component-Cj8V3uar.js";
1
+ import { B as ListNativeDragStartEvent, F as ListDragPreviewContext, I as ListDragPreviewRenderer, N as ListDragEntry, P as ListDragPreview, R as ListDragSource, z as ListNativeDragStartDetail } from "./tree-component-Tslymyyl.js";
2
2
  import { n as ListViewportChangeEvent, t as ListViewportChangeDetail } from "./list-viewport-types-BRZsLcvp.js";
3
3
  import { a as ListItemClickDetail, c as ListSelectEvent, d as ListTickleEvent, i as ListInteractionMode, l as ListSelectionMode, n as ListActivateEvent, o as ListItemClickEvent, r as ListElementEventMap, s as ListSelectDetail, t as ListActivateDetail, u as ListTickleDetail } from "./list-selection-types-PjKdhF-a.js";
4
- import { a as ListContextMenuDetail, c as ListDragLeaveEvent, d as ListDropEvent, f as ListEditCancelDetail, h as ListEditCommitEvent, i as VerticalListElementEventMap, l as ListDragOverEvent, m as ListEditCommitDetail, n as CustomCheckboxOptions, o as ListContextMenuEvent, p as ListEditCancelEvent, r as VerticalListElement, s as ListDragLeaveDetail, t as AddItemOptions, u as ListDropDetail } from "./vertical-list-m7GJtXwv.js";
4
+ import { a as ListContextMenuDetail, c as ListDragLeaveEvent, d as ListDropEvent, f as ListEditCancelDetail, h as ListEditCommitEvent, i as VerticalListElementEventMap, l as ListDragOverEvent, m as ListEditCommitDetail, n as CustomCheckboxOptions, o as ListContextMenuEvent, p as ListEditCancelEvent, r as VerticalListElement, s as ListDragLeaveDetail, t as AddItemOptions, u as ListDropDetail } from "./vertical-list-B1Fq06xr.js";
5
5
  export { AddItemOptions, CustomCheckboxOptions, type ListActivateDetail, type ListActivateEvent, type ListContextMenuDetail, type ListContextMenuEvent, type ListDragEntry, type ListDragLeaveDetail, type ListDragLeaveEvent, type ListDragOverEvent, type ListDragPreview, type ListDragPreviewContext, type ListDragPreviewRenderer, type ListDragSource, type ListDropDetail, type ListDropEvent, type ListEditCancelDetail, type ListEditCancelEvent, type ListEditCommitDetail, type ListEditCommitEvent, type ListElementEventMap, type ListInteractionMode, type ListItemClickDetail, type ListItemClickEvent, type ListNativeDragStartDetail, type ListNativeDragStartEvent, type ListSelectDetail, type ListSelectEvent, type ListSelectionMode, type ListTickleDetail, type ListTickleEvent, type ListViewportChangeDetail, type ListViewportChangeEvent, VerticalListElement, VerticalListElementEventMap };