@ixfx/components 0.13.1 → 0.13.3
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/bundle/index.d.ts +100 -14
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2211 -636
- package/bundle/index.js.map +1 -1
- package/dist/ac-token.js +1 -1
- package/dist/{data-grid-Ck1Qx2H1.d.ts → data-grid-DHWJN66w.d.ts} +7 -5
- package/dist/{data-grid-Ck1Qx2H1.d.ts.map → data-grid-DHWJN66w.d.ts.map} +1 -1
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +210 -120
- package/dist/data-grid.js.map +1 -1
- package/dist/detail-list-C96B5Txz.js +2001 -0
- package/dist/detail-list-C96B5Txz.js.map +1 -0
- package/dist/{detail-list-C1xzr1z7.d.ts → detail-list-dX7jLO6b.d.ts} +12 -3
- package/dist/{detail-list-C1xzr1z7.d.ts.map → detail-list-dX7jLO6b.d.ts.map} +1 -1
- package/dist/detail-list.d.ts +2 -2
- package/dist/detail-list.js +1 -1680
- package/dist/{flat-list-DEDG6epz.d.ts → flat-list-Y98IqH00.d.ts} +5 -2
- package/dist/flat-list-Y98IqH00.d.ts.map +1 -0
- package/dist/flat-list.d.ts +1 -1
- package/dist/flat-list.js +33 -1
- package/dist/flat-list.js.map +1 -1
- package/dist/{grid-list-pefLORIr.d.ts → grid-list-CoT9e1jB.d.ts} +9 -2
- package/dist/{grid-list-pefLORIr.d.ts.map → grid-list-CoT9e1jB.d.ts.map} +1 -1
- package/dist/{grid-list-DjIl910A.js → grid-list-DeqAd9-h.js} +189 -40
- package/dist/grid-list-DeqAd9-h.js.map +1 -0
- package/dist/grid-list.d.ts +1 -1
- package/dist/grid-list.js +1 -1
- package/dist/{incr-search-D91M4Ozq.js → incr-search-JIhwB49y.js} +27 -5
- package/dist/incr-search-JIhwB49y.js.map +1 -0
- package/dist/incr-search.d.ts +1 -1
- package/dist/incr-search.js +1 -1
- package/dist/index-CnQPCywE.d.ts +325 -0
- package/dist/index-CnQPCywE.d.ts.map +1 -0
- package/dist/{index-CxZ61DO1.d.ts → index-Dlah7Zfi.d.ts} +8 -1
- package/dist/{index-CxZ61DO1.d.ts.map → index-Dlah7Zfi.d.ts.map} +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -25
- package/dist/index.js.map +1 -1
- package/dist/miller.d.ts +1 -1
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +146 -100
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.js +1 -1
- package/dist/split-layout-C5SYbDrq.d.ts.map +1 -1
- package/dist/{split-layout-CXotkdbx.js → split-layout-Deoxy47d.js} +6 -1
- package/dist/{split-layout-CXotkdbx.js.map → split-layout-Deoxy47d.js.map} +1 -1
- package/dist/split-layout.js +1 -1
- package/dist/{tab-list-v8mKX7Fu.d.ts → tab-list-D_dmImJM.d.ts} +2 -3
- package/dist/tab-list-D_dmImJM.d.ts.map +1 -0
- package/dist/tabs.d.ts +1 -1
- package/dist/tabs.js +22 -0
- package/dist/tabs.js.map +1 -1
- package/dist/{tree-DGveys4K.js → tree-DD8OaqsK.js} +1010 -230
- package/dist/tree-DD8OaqsK.js.map +1 -0
- package/dist/tree.d.ts +4 -267
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-CqBc-t8w.d.ts → vertical-list-lj_19DCx.d.ts} +3 -2
- package/dist/{vertical-list-CqBc-t8w.d.ts.map → vertical-list-lj_19DCx.d.ts.map} +1 -1
- package/dist/vertical-list.d.ts +1 -1
- package/dist/vertical-list.js +113 -24
- package/dist/vertical-list.js.map +1 -1
- package/docs-user/data-grid.md +74 -4
- package/docs-user/detail-list.md +65 -0
- package/docs-user/flat-list.md +14 -2
- package/docs-user/grid-list.md +81 -4
- package/docs-user/index.json +2 -2
- package/docs-user/miller.md +59 -3
- package/docs-user/tree.md +63 -2
- package/docs-user/vertical-list.md +9 -1
- package/package.json +6 -1
- package/dist/detail-list.js.map +0 -1
- package/dist/flat-list-DEDG6epz.d.ts.map +0 -1
- package/dist/grid-list-DjIl910A.js.map +0 -1
- package/dist/incr-search-D91M4Ozq.js.map +0 -1
- package/dist/tab-list-v8mKX7Fu.d.ts.map +0 -1
- package/dist/tree-DGveys4K.js.map +0 -1
- package/dist/tree.d.ts.map +0 -1
|
@@ -2,7 +2,7 @@ import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
|
|
|
2
2
|
import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrAHEj1q.js";
|
|
3
3
|
import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
|
|
4
4
|
import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
|
|
5
|
-
import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-
|
|
5
|
+
import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-JIhwB49y.js";
|
|
6
6
|
import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
|
|
7
7
|
import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
|
|
8
8
|
import "./interaction-ZTh4htzr.js";
|
|
@@ -643,15 +643,10 @@ var TreeDropController = class {
|
|
|
643
643
|
if (clientY > rows[rows.length - 1].rect.bottom) return rows[rows.length - 1];
|
|
644
644
|
return rows.find((r) => clientY >= r.rect.top && clientY <= r.rect.bottom) ?? rows[rows.length - 1];
|
|
645
645
|
}
|
|
646
|
-
#parentRow(rows, index) {
|
|
647
|
-
const target = rows[index];
|
|
648
|
-
for (let i = index - 1; i >= 0; i--) if (rows[i].depth === target.depth - 1) return rows[i];
|
|
649
|
-
}
|
|
650
646
|
#resolveIntent(event, rows, source) {
|
|
651
647
|
if (rows.length === 0) return void 0;
|
|
652
648
|
const row = this.#rowAtPointer(rows, event.clientY);
|
|
653
649
|
if (!row) return void 0;
|
|
654
|
-
const rowIndex = rows.indexOf(row);
|
|
655
650
|
const f = clamp((event.clientY - row.rect.top) / row.rect.height, 0, 1);
|
|
656
651
|
let position = f < .25 ? `before` : f > .75 ? `after` : `inside`;
|
|
657
652
|
const isLeaf = row.node.item.isLeaf === true;
|
|
@@ -674,18 +669,21 @@ var TreeDropController = class {
|
|
|
674
669
|
const indent = this.#config.getIndentPx() || 1;
|
|
675
670
|
const treeLeft = rows[0].rect.left;
|
|
676
671
|
const effDepth = clamp(Math.round((event.clientX - treeLeft) / indent), 0, row.depth);
|
|
677
|
-
let
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
672
|
+
let anchor = {
|
|
673
|
+
node: row.node,
|
|
674
|
+
depth: row.depth,
|
|
675
|
+
parentKey: this.#config.getRowInfo(row.key)?.parentKey
|
|
676
|
+
};
|
|
677
|
+
referenceKey = row.key;
|
|
678
|
+
for (let up = row.depth - effDepth; up > 0 && anchor.parentKey !== void 0; up--) {
|
|
679
|
+
const parentInfo = this.#config.getRowInfo(anchor.parentKey);
|
|
680
|
+
if (!parentInfo) break;
|
|
681
|
+
referenceKey = anchor.parentKey;
|
|
682
|
+
anchor = parentInfo;
|
|
682
683
|
}
|
|
683
|
-
const anchor = rows[anchorIndex];
|
|
684
|
-
referenceKey = anchor.key;
|
|
685
684
|
depth = anchor.depth;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const refIdx = (parent ? parent.node.children ?? [] : rows.filter((r) => r.depth === 0).map((r) => r.node)).findIndex((n) => n.item.key === anchor.key);
|
|
685
|
+
parentKey = anchor.parentKey;
|
|
686
|
+
const refIdx = this.#config.getChildren(parentKey).findIndex((n) => n.item.key === referenceKey);
|
|
689
687
|
index = (refIdx < 0 ? 0 : refIdx) + (position === `after` ? 1 : 0);
|
|
690
688
|
}
|
|
691
689
|
if (source === `internal`) {
|
|
@@ -760,9 +758,333 @@ var TreeDropController = class {
|
|
|
760
758
|
}
|
|
761
759
|
};
|
|
762
760
|
//#endregion
|
|
761
|
+
//#region src/tree/tree-flat.ts
|
|
762
|
+
/**
|
|
763
|
+
* Flatten the visible rows under `root` (root itself excluded).
|
|
764
|
+
*
|
|
765
|
+
* Iterative implementation using an explicit stack to avoid recursion depth
|
|
766
|
+
* limits with deep trees. `posInSet` / `setSize` count only shown siblings.
|
|
767
|
+
*
|
|
768
|
+
* @param root - The root node (never included in output; its children are depth 0)
|
|
769
|
+
* @param options - Callbacks determining expansion and visibility
|
|
770
|
+
* @returns FlatTree with rows in display order and a key→index map
|
|
771
|
+
*/
|
|
772
|
+
function flattenTree(root, options) {
|
|
773
|
+
const rows = [];
|
|
774
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
775
|
+
const stack = [];
|
|
776
|
+
const pushChildren = (children, depth, parentKey) => {
|
|
777
|
+
if (!children || children.length === 0) return;
|
|
778
|
+
const shown = children.filter((c) => options.isShown(c, depth));
|
|
779
|
+
for (let i = shown.length - 1; i >= 0; i--) stack.push({
|
|
780
|
+
node: shown[i],
|
|
781
|
+
depth,
|
|
782
|
+
parentKey,
|
|
783
|
+
posInSet: i + 1,
|
|
784
|
+
setSize: shown.length
|
|
785
|
+
});
|
|
786
|
+
};
|
|
787
|
+
pushChildren(root.children, 0, void 0);
|
|
788
|
+
while (stack.length > 0) {
|
|
789
|
+
const { node, depth, parentKey, posInSet, setSize } = stack.pop();
|
|
790
|
+
indexByKey.set(node.item.key, rows.length);
|
|
791
|
+
rows.push({
|
|
792
|
+
node,
|
|
793
|
+
depth,
|
|
794
|
+
parentKey,
|
|
795
|
+
posInSet,
|
|
796
|
+
setSize
|
|
797
|
+
});
|
|
798
|
+
if (node.children && node.children.length > 0 && options.isExpanded(node, depth)) pushChildren(node.children, depth + 1, node.item.key);
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
rows,
|
|
802
|
+
indexByKey
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Build a whole-tree index over the currently loaded nodes.
|
|
807
|
+
* This walks the entire loaded tree once and is cached on structure version.
|
|
808
|
+
*
|
|
809
|
+
* @param root - The root node
|
|
810
|
+
* @returns TreeIndex with parent map and icon depth set
|
|
811
|
+
*/
|
|
812
|
+
function indexTree(root) {
|
|
813
|
+
const parentByKey = /* @__PURE__ */ new Map();
|
|
814
|
+
const depthsWithIcons = /* @__PURE__ */ new Set();
|
|
815
|
+
const stack = [];
|
|
816
|
+
if (root.children) for (const child of root.children) {
|
|
817
|
+
parentByKey.set(child.item.key, root.item.key);
|
|
818
|
+
stack.push({
|
|
819
|
+
node: child,
|
|
820
|
+
depth: 0,
|
|
821
|
+
parentKey: root.item.key
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
while (stack.length > 0) {
|
|
825
|
+
const { node, depth, parentKey } = stack.pop();
|
|
826
|
+
parentByKey.set(node.item.key, parentKey);
|
|
827
|
+
if (node.item.icon) depthsWithIcons.add(depth);
|
|
828
|
+
if (node.children) for (let i = node.children.length - 1; i >= 0; i--) {
|
|
829
|
+
const child = node.children[i];
|
|
830
|
+
stack.push({
|
|
831
|
+
node: child,
|
|
832
|
+
depth: depth + 1,
|
|
833
|
+
parentKey: node.item.key
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
parentByKey,
|
|
839
|
+
depthsWithIcons
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Compute which nodes match a filter predicate.
|
|
844
|
+
* Returns the set of keys for nodes that match OR have a matching descendant.
|
|
845
|
+
*
|
|
846
|
+
* One iterative post-order pass; every node is visited (no short-circuiting).
|
|
847
|
+
*/
|
|
848
|
+
function computeFilterMatches(root, predicate) {
|
|
849
|
+
const matches = /* @__PURE__ */ new Set();
|
|
850
|
+
if (!root.children) return matches;
|
|
851
|
+
const stack = [];
|
|
852
|
+
const enter = (node, depth) => {
|
|
853
|
+
stack.push({
|
|
854
|
+
node,
|
|
855
|
+
depth,
|
|
856
|
+
next: 0,
|
|
857
|
+
matched: predicate(node, depth)
|
|
858
|
+
});
|
|
859
|
+
};
|
|
860
|
+
for (const child of root.children) {
|
|
861
|
+
enter(child, 0);
|
|
862
|
+
while (stack.length > 0) {
|
|
863
|
+
const frame = stack[stack.length - 1];
|
|
864
|
+
const children = frame.node.children;
|
|
865
|
+
if (children && frame.next < children.length) {
|
|
866
|
+
enter(children[frame.next++], frame.depth + 1);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
stack.pop();
|
|
870
|
+
if (frame.matched) {
|
|
871
|
+
matches.add(frame.node.item.key);
|
|
872
|
+
if (stack.length > 0) stack[stack.length - 1].matched = true;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return matches;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Merge ghost rows back near their previous position.
|
|
880
|
+
* Each ghost goes immediately before the first row that followed it last render
|
|
881
|
+
* and still exists; otherwise at the end. Ghosts sharing an anchor keep their
|
|
882
|
+
* previous relative order. O(rows + prevKeys).
|
|
883
|
+
*
|
|
884
|
+
* @param rows - Current flat rows (with node and depth)
|
|
885
|
+
* @param prevKeys - Previous render's row keys in order
|
|
886
|
+
* @param ghosts - Map of ghost keys to their row data
|
|
887
|
+
* @returns Rows with ghosts woven in at their original relative positions
|
|
888
|
+
*/
|
|
889
|
+
function weaveGhosts(rows, prevKeys, ghosts) {
|
|
890
|
+
if (ghosts.size === 0) return [...rows];
|
|
891
|
+
const present = /* @__PURE__ */ new Set();
|
|
892
|
+
for (const r of rows) present.add(r.node.item.key);
|
|
893
|
+
const END = Symbol(`end`);
|
|
894
|
+
const before = /* @__PURE__ */ new Map();
|
|
895
|
+
let nextSurvivor = END;
|
|
896
|
+
for (let i = prevKeys.length - 1; i >= 0; i--) {
|
|
897
|
+
const key = prevKeys[i];
|
|
898
|
+
if (present.has(key)) {
|
|
899
|
+
nextSurvivor = key;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const ghost = ghosts.get(key);
|
|
903
|
+
if (!ghost) continue;
|
|
904
|
+
const list = before.get(nextSurvivor);
|
|
905
|
+
if (list) list.push(ghost);
|
|
906
|
+
else before.set(nextSurvivor, [ghost]);
|
|
907
|
+
}
|
|
908
|
+
const result = [];
|
|
909
|
+
for (const row of rows) {
|
|
910
|
+
const list = before.get(row.node.item.key);
|
|
911
|
+
if (list) for (let i = list.length - 1; i >= 0; i--) result.push(list[i]);
|
|
912
|
+
result.push(row);
|
|
913
|
+
}
|
|
914
|
+
const tail = before.get(END);
|
|
915
|
+
if (tail) for (let i = tail.length - 1; i >= 0; i--) result.push(tail[i]);
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Compute checkbox states for all nodes in a single iterative post-order pass.
|
|
920
|
+
* A leaf (or a branch with no children) uses `selected.has(node)`; otherwise a
|
|
921
|
+
* branch is checked if all children are checked, unchecked if all are
|
|
922
|
+
* unchecked, else indeterminate.
|
|
923
|
+
*
|
|
924
|
+
* @param root - The root node
|
|
925
|
+
* @param selected - Set of selected nodes
|
|
926
|
+
* @returns Map from node key to checkbox state
|
|
927
|
+
*/
|
|
928
|
+
function computeCheckboxStates(root, selected) {
|
|
929
|
+
const states = /* @__PURE__ */ new Map();
|
|
930
|
+
if (!root.children) return states;
|
|
931
|
+
const stack = [];
|
|
932
|
+
for (const top of root.children) {
|
|
933
|
+
stack.push({
|
|
934
|
+
node: top,
|
|
935
|
+
next: 0,
|
|
936
|
+
checked: 0,
|
|
937
|
+
unchecked: 0
|
|
938
|
+
});
|
|
939
|
+
while (stack.length > 0) {
|
|
940
|
+
const frame = stack[stack.length - 1];
|
|
941
|
+
const { node } = frame;
|
|
942
|
+
const children = node.children;
|
|
943
|
+
const isLeafLike = node.item.isLeaf || !children?.length;
|
|
944
|
+
if (!isLeafLike && frame.next < children.length) {
|
|
945
|
+
stack.push({
|
|
946
|
+
node: children[frame.next++],
|
|
947
|
+
next: 0,
|
|
948
|
+
checked: 0,
|
|
949
|
+
unchecked: 0
|
|
950
|
+
});
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
stack.pop();
|
|
954
|
+
let state;
|
|
955
|
+
if (isLeafLike) state = selected.has(node) ? `checked` : `unchecked`;
|
|
956
|
+
else if (frame.checked === children.length) state = `checked`;
|
|
957
|
+
else if (frame.unchecked === children.length) state = `unchecked`;
|
|
958
|
+
else state = `indeterminate`;
|
|
959
|
+
states.set(node.item.key, state);
|
|
960
|
+
if (stack.length > 0) {
|
|
961
|
+
const parent = stack[stack.length - 1];
|
|
962
|
+
if (state === `checked`) parent.checked++;
|
|
963
|
+
else if (state === `unchecked`) parent.unchecked++;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return states;
|
|
968
|
+
}
|
|
969
|
+
function withKey(set, key) {
|
|
970
|
+
if (set.has(key)) return set;
|
|
971
|
+
const next = new Set(set);
|
|
972
|
+
next.add(key);
|
|
973
|
+
return next;
|
|
974
|
+
}
|
|
975
|
+
function withoutKey(set, key) {
|
|
976
|
+
if (!set.has(key)) return set;
|
|
977
|
+
const next = new Set(set);
|
|
978
|
+
next.delete(key);
|
|
979
|
+
return next;
|
|
980
|
+
}
|
|
981
|
+
/** Remove every key for which `pred` returns true. */
|
|
982
|
+
function withoutKeys(set, pred) {
|
|
983
|
+
let next;
|
|
984
|
+
for (const key of set) if (pred(key)) {
|
|
985
|
+
next ??= new Set(set);
|
|
986
|
+
next.delete(key);
|
|
987
|
+
}
|
|
988
|
+
return next ?? set;
|
|
989
|
+
}
|
|
990
|
+
function treeStride(params) {
|
|
991
|
+
return Math.max(1, params.rowHeight + params.gap);
|
|
992
|
+
}
|
|
993
|
+
/** Content-space top of the row box at `index`. */
|
|
994
|
+
function treeRowTop(params, index) {
|
|
995
|
+
return params.listOffset + index * treeStride(params) + params.gap;
|
|
996
|
+
}
|
|
997
|
+
function treeRowBottom(params, index) {
|
|
998
|
+
return treeRowTop(params, index) + params.rowHeight;
|
|
999
|
+
}
|
|
1000
|
+
/** The window of rows to render for a given scroll offset. Always `0 <= start <= end <= count`. */
|
|
1001
|
+
function computeTreeWindow(params, scrollTop, overscan = 5) {
|
|
1002
|
+
const { count } = params;
|
|
1003
|
+
if (count <= 0) return {
|
|
1004
|
+
start: 0,
|
|
1005
|
+
end: 0
|
|
1006
|
+
};
|
|
1007
|
+
const stride = treeStride(params);
|
|
1008
|
+
const top = Math.max(0, scrollTop - params.listOffset);
|
|
1009
|
+
const firstVisible = Math.floor(top / stride);
|
|
1010
|
+
const lastVisible = Math.ceil((top + params.viewportHeight) / stride);
|
|
1011
|
+
const start = Math.min(count, Math.max(0, firstVisible - overscan));
|
|
1012
|
+
return {
|
|
1013
|
+
start,
|
|
1014
|
+
end: Math.min(count, Math.max(start, lastVisible + overscan))
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* The scroll offset that brings row `index` fully into view with the least
|
|
1019
|
+
* movement, leaving `topInset` px at the top clear (the sticky-header overlay).
|
|
1020
|
+
* Returns `scrollTop` unchanged when the row is already visible.
|
|
1021
|
+
*/
|
|
1022
|
+
function treeScrollTopToReveal(params, index, scrollTop, topInset = 0) {
|
|
1023
|
+
if (index < 0 || index >= params.count) return scrollTop;
|
|
1024
|
+
const top = treeRowTop(params, index);
|
|
1025
|
+
const bottom = top + params.rowHeight;
|
|
1026
|
+
if (top < scrollTop + topInset) return index === 0 ? 0 : Math.max(0, top - topInset);
|
|
1027
|
+
if (bottom > scrollTop + params.viewportHeight) return Math.max(0, bottom - params.viewportHeight);
|
|
1028
|
+
return scrollTop;
|
|
1029
|
+
}
|
|
1030
|
+
/** Rows that fit one viewport (PageUp / PageDown step), at least 1. */
|
|
1031
|
+
function treePageSize(params) {
|
|
1032
|
+
return Math.max(1, Math.floor(params.viewportHeight / treeStride(params)) - 1);
|
|
1033
|
+
}
|
|
1034
|
+
/** Arithmetic `RowGeometryReader` over flat-row indices (the windowed counterpart of reading `offsetTop`). */
|
|
1035
|
+
function treeRowReader(params) {
|
|
1036
|
+
return {
|
|
1037
|
+
count: params.count,
|
|
1038
|
+
topOf: (i) => treeRowTop(params, i),
|
|
1039
|
+
bottomOf: (i) => treeRowBottom(params, i)
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/tree/tree-row-geometry.ts
|
|
1044
|
+
/**
|
|
1045
|
+
* First index whose bottom is greater than `y` (binary search; rows are
|
|
1046
|
+
* monotonic). Returns `reader.count` when no row qualifies.
|
|
1047
|
+
*/
|
|
1048
|
+
function firstRowBelow(reader, y) {
|
|
1049
|
+
let lo = 0;
|
|
1050
|
+
let hi = reader.count;
|
|
1051
|
+
while (lo < hi) {
|
|
1052
|
+
const mid = lo + hi >>> 1;
|
|
1053
|
+
if (reader.bottomOf(mid) > y) hi = mid;
|
|
1054
|
+
else lo = mid + 1;
|
|
1055
|
+
}
|
|
1056
|
+
return lo;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Inclusive index range of rows that overlap `[top, bottom]`, widened by
|
|
1060
|
+
* `margin` rows on each side. Returns `undefined` when there are no rows.
|
|
1061
|
+
*/
|
|
1062
|
+
function rowRangeOverlapping(reader, top, bottom, margin = 0) {
|
|
1063
|
+
if (reader.count === 0) return void 0;
|
|
1064
|
+
const firstHit = Math.min(firstRowBelow(reader, top), reader.count - 1);
|
|
1065
|
+
let lo = firstHit;
|
|
1066
|
+
let hi = reader.count;
|
|
1067
|
+
while (lo < hi) {
|
|
1068
|
+
const mid = lo + hi >>> 1;
|
|
1069
|
+
if (reader.topOf(mid) >= bottom) hi = mid;
|
|
1070
|
+
else lo = mid + 1;
|
|
1071
|
+
}
|
|
1072
|
+
const lastHit = Math.max(firstHit, lo - 1);
|
|
1073
|
+
return {
|
|
1074
|
+
first: Math.max(0, firstHit - margin),
|
|
1075
|
+
last: Math.min(reader.count - 1, lastHit + margin)
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
763
1079
|
//#region src/tree/tree-base.ts
|
|
764
1080
|
/** Above this many rows entering / leaving at once, skip the animation entirely. */
|
|
765
1081
|
const MAX_ANIMATED_ROWS = 50;
|
|
1082
|
+
const EMPTY_FLAT = {
|
|
1083
|
+
rows: [],
|
|
1084
|
+
indexByKey: /* @__PURE__ */ new Map()
|
|
1085
|
+
};
|
|
1086
|
+
/** Viewport height assumed until the container has been measured (or when it reads 0). */
|
|
1087
|
+
const FALLBACK_VIEWPORT_HEIGHT = 600;
|
|
766
1088
|
let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
767
1089
|
constructor(..._args) {
|
|
768
1090
|
super(..._args);
|
|
@@ -776,11 +1098,33 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
776
1098
|
this.dndExpandHoverMs = 500;
|
|
777
1099
|
this.reorderable = true;
|
|
778
1100
|
this.stickyHeaders = true;
|
|
1101
|
+
this.virtualize = `auto`;
|
|
1102
|
+
this.virtualizeThreshold = 200;
|
|
779
1103
|
this.applyInternalDrop = true;
|
|
780
1104
|
this._expandedKeys = /* @__PURE__ */ new Set();
|
|
1105
|
+
this.#structureVersion = 0;
|
|
1106
|
+
this._selectionVersion = 0;
|
|
1107
|
+
this._scrollTop = 0;
|
|
1108
|
+
this.#metrics = {
|
|
1109
|
+
rowHeight: 30,
|
|
1110
|
+
gap: 1,
|
|
1111
|
+
listOffset: 0
|
|
1112
|
+
};
|
|
1113
|
+
this.#viewportH = 0;
|
|
1114
|
+
this.#virtualScrollRaf = 0;
|
|
1115
|
+
this.#winStart = 0;
|
|
1116
|
+
this.#winEnd = 0;
|
|
1117
|
+
this.#scrollOnlyUpdate = false;
|
|
1118
|
+
this.#onVirtualScroll = () => {
|
|
1119
|
+
if (this.#virtualScrollRaf !== 0) return;
|
|
1120
|
+
this.#virtualScrollRaf = requestAnimationFrame(() => {
|
|
1121
|
+
this.#virtualScrollRaf = 0;
|
|
1122
|
+
this.#syncWindowToScroll();
|
|
1123
|
+
});
|
|
1124
|
+
};
|
|
781
1125
|
this.tickled = new TickledController(this, {
|
|
782
1126
|
getItems: () => {
|
|
783
|
-
return this.#
|
|
1127
|
+
return this.#getFlat().rows.map(({ node }) => ({
|
|
784
1128
|
id: node.item.key,
|
|
785
1129
|
enabled: true
|
|
786
1130
|
}));
|
|
@@ -797,6 +1141,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
797
1141
|
}) });
|
|
798
1142
|
this.#config = { indent: 20 };
|
|
799
1143
|
this.#renderedKeys = [];
|
|
1144
|
+
this.#renderedRowKeys = [];
|
|
1145
|
+
this.#renderedRowIndex = /* @__PURE__ */ new Map();
|
|
800
1146
|
this.#lastNodeByKey = /* @__PURE__ */ new Map();
|
|
801
1147
|
this.#enteringKeys = /* @__PURE__ */ new Set();
|
|
802
1148
|
this.#leaving = /* @__PURE__ */ new Map();
|
|
@@ -820,9 +1166,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
820
1166
|
this.#stickyDirty = true;
|
|
821
1167
|
this.#stickyRaf = 0;
|
|
822
1168
|
this.#stickyScrollBound = false;
|
|
1169
|
+
this.#revealToken = 0;
|
|
823
1170
|
this.#onKeyDown = (event) => {
|
|
824
1171
|
if (this.search.isOpen) return;
|
|
825
|
-
const nodes = this.#
|
|
1172
|
+
const nodes = this.#getFlat().rows;
|
|
826
1173
|
if (nodes.length === 0) return;
|
|
827
1174
|
if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
|
|
828
1175
|
event.preventDefault();
|
|
@@ -834,29 +1181,41 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
834
1181
|
`ArrowUp`,
|
|
835
1182
|
`ArrowLeft`,
|
|
836
1183
|
`ArrowRight`,
|
|
1184
|
+
`Home`,
|
|
1185
|
+
`End`,
|
|
1186
|
+
`PageUp`,
|
|
1187
|
+
`PageDown`,
|
|
837
1188
|
`Enter`,
|
|
838
1189
|
` `
|
|
839
1190
|
].includes(event.key)) this.tickled.setInputMode(`keyboard`);
|
|
840
1191
|
const currentIdx = this.#getFocusedIndex();
|
|
841
1192
|
switch (event.key) {
|
|
842
|
-
case `ArrowDown`:
|
|
1193
|
+
case `ArrowDown`:
|
|
843
1194
|
event.preventDefault();
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1195
|
+
this.#navigateToIndex(nodes, currentIdx, Math.min(currentIdx + 1, nodes.length - 1), event.shiftKey);
|
|
1196
|
+
break;
|
|
1197
|
+
case `ArrowUp`:
|
|
1198
|
+
event.preventDefault();
|
|
1199
|
+
this.#navigateToIndex(nodes, currentIdx, Math.max(currentIdx - 1, 0), event.shiftKey);
|
|
1200
|
+
break;
|
|
1201
|
+
case `Home`:
|
|
1202
|
+
event.preventDefault();
|
|
1203
|
+
this.#navigateToIndex(nodes, currentIdx, 0, event.shiftKey);
|
|
1204
|
+
break;
|
|
1205
|
+
case `End`:
|
|
1206
|
+
event.preventDefault();
|
|
1207
|
+
this.#navigateToIndex(nodes, currentIdx, nodes.length - 1, event.shiftKey);
|
|
1208
|
+
break;
|
|
1209
|
+
case `PageDown`: {
|
|
1210
|
+
event.preventDefault();
|
|
1211
|
+
const page = this.#pageSize();
|
|
1212
|
+
this.#navigateToIndex(nodes, currentIdx, Math.min(currentIdx + page, nodes.length - 1), event.shiftKey);
|
|
850
1213
|
break;
|
|
851
1214
|
}
|
|
852
|
-
case `
|
|
1215
|
+
case `PageUp`: {
|
|
853
1216
|
event.preventDefault();
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
this.#setFocusedIndex(prevIdx);
|
|
857
|
-
if (event.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, prevIdx);
|
|
858
|
-
else if (this.interactionMode === `vscode`) this.#anchor = void 0;
|
|
859
|
-
this.requestUpdate();
|
|
1217
|
+
const page = this.#pageSize();
|
|
1218
|
+
this.#navigateToIndex(nodes, currentIdx, Math.max(currentIdx - page, 0), event.shiftKey);
|
|
860
1219
|
break;
|
|
861
1220
|
}
|
|
862
1221
|
case `ArrowRight`: {
|
|
@@ -873,7 +1232,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
873
1232
|
event.preventDefault();
|
|
874
1233
|
const current = nodes[currentIdx];
|
|
875
1234
|
if (current && this._expandedKeys.has(current.node.item.key)) {
|
|
876
|
-
this._expandedKeys
|
|
1235
|
+
this.#setExpanded(withoutKey(this._expandedKeys, current.node.item.key));
|
|
877
1236
|
this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
878
1237
|
detail: {
|
|
879
1238
|
node: current.node,
|
|
@@ -927,7 +1286,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
927
1286
|
if (!li) return;
|
|
928
1287
|
const key = li.dataset.key;
|
|
929
1288
|
if (!key) return;
|
|
930
|
-
const
|
|
1289
|
+
const flat = this.#getFlat();
|
|
1290
|
+
const idx = flat.indexByKey.get(key);
|
|
1291
|
+
const node = idx === void 0 ? void 0 : flat.rows[idx];
|
|
931
1292
|
if (!node) return;
|
|
932
1293
|
const detail = {
|
|
933
1294
|
node: node.node,
|
|
@@ -948,6 +1309,206 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
948
1309
|
get selectedNodes() {
|
|
949
1310
|
return this._selectedNodes;
|
|
950
1311
|
}
|
|
1312
|
+
#setExpanded(next) {
|
|
1313
|
+
if (next === this._expandedKeys) return;
|
|
1314
|
+
this._expandedKeys = next;
|
|
1315
|
+
this.#flat = void 0;
|
|
1316
|
+
this.#stickyDirty = true;
|
|
1317
|
+
}
|
|
1318
|
+
#flat;
|
|
1319
|
+
#flatRoot;
|
|
1320
|
+
#flatFilter;
|
|
1321
|
+
#structureVersion;
|
|
1322
|
+
#treeIndexCache;
|
|
1323
|
+
#filterCache;
|
|
1324
|
+
/**
|
|
1325
|
+
* Call after editing `node.children` / `node.item` in place (outside
|
|
1326
|
+
* `TreeDataModel`). Drops the cached whole-tree indexes and re-renders.
|
|
1327
|
+
*/
|
|
1328
|
+
invalidateStructure() {
|
|
1329
|
+
this.#structureVersion++;
|
|
1330
|
+
this.#flat = void 0;
|
|
1331
|
+
this.requestUpdate();
|
|
1332
|
+
}
|
|
1333
|
+
/** Visible rows in display order (cached for the current update). */
|
|
1334
|
+
#getFlat() {
|
|
1335
|
+
if (!this.root) return EMPTY_FLAT;
|
|
1336
|
+
if (this.#flat && this.#flatRoot === this.root && this.#flatFilter === this.filterPredicate) return this.#flat;
|
|
1337
|
+
const filter = this.#getFilter();
|
|
1338
|
+
this.#flat = flattenTree(this.root, {
|
|
1339
|
+
isExpanded: (node, depth) => this._isExpanded(node, depth),
|
|
1340
|
+
isShown: filter ? (node) => filter.matches.has(node.item.key) : () => true
|
|
1341
|
+
});
|
|
1342
|
+
this.#flatRoot = this.root;
|
|
1343
|
+
this.#flatFilter = this.filterPredicate;
|
|
1344
|
+
return this.#flat;
|
|
1345
|
+
}
|
|
1346
|
+
/** Whole-loaded-tree index (parents, icon depths); cached on `(root, structureVersion)`. */
|
|
1347
|
+
#getTreeIndex() {
|
|
1348
|
+
const root = this.root;
|
|
1349
|
+
if (!root) return {
|
|
1350
|
+
parentByKey: /* @__PURE__ */ new Map(),
|
|
1351
|
+
depthsWithIcons: /* @__PURE__ */ new Set()
|
|
1352
|
+
};
|
|
1353
|
+
const cached = this.#treeIndexCache;
|
|
1354
|
+
if (cached && cached.root === root && cached.version === this.#structureVersion) return cached.index;
|
|
1355
|
+
const index = indexTree(root);
|
|
1356
|
+
this.#treeIndexCache = {
|
|
1357
|
+
root,
|
|
1358
|
+
version: this.#structureVersion,
|
|
1359
|
+
index
|
|
1360
|
+
};
|
|
1361
|
+
return index;
|
|
1362
|
+
}
|
|
1363
|
+
/** Row height / gap / list offset as last measured from CSS + layout. */
|
|
1364
|
+
#metrics;
|
|
1365
|
+
/** Last non-zero container height (a hidden dock tab reads 0). */
|
|
1366
|
+
#viewportH;
|
|
1367
|
+
#virtualScrollRaf;
|
|
1368
|
+
#virtualScrollContainer;
|
|
1369
|
+
#resizeObserver;
|
|
1370
|
+
#winStart;
|
|
1371
|
+
#winEnd;
|
|
1372
|
+
/** Set in `willUpdate` when only the scroll offset changed; consumed in `updated`. */
|
|
1373
|
+
#scrollOnlyUpdate;
|
|
1374
|
+
/** True when the current visible row count should be windowed. */
|
|
1375
|
+
#virtualActive() {
|
|
1376
|
+
if (this.virtualize === `off`) return false;
|
|
1377
|
+
if (this.virtualize === `on`) return true;
|
|
1378
|
+
return this.#getFlat().rows.length > this.virtualizeThreshold;
|
|
1379
|
+
}
|
|
1380
|
+
#layoutParams(count) {
|
|
1381
|
+
return {
|
|
1382
|
+
count,
|
|
1383
|
+
rowHeight: this.#metrics.rowHeight,
|
|
1384
|
+
gap: this.#metrics.gap,
|
|
1385
|
+
listOffset: this.#metrics.listOffset,
|
|
1386
|
+
viewportHeight: this.#viewportH > 0 ? this.#viewportH : FALLBACK_VIEWPORT_HEIGHT
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Re-read the CSS metrics and viewport height. Returns true when anything
|
|
1391
|
+
* that changes row positions moved (the caller re-renders).
|
|
1392
|
+
*/
|
|
1393
|
+
#measureMetrics() {
|
|
1394
|
+
const list = this.renderRoot.querySelector(`ul.tree-list`);
|
|
1395
|
+
const sample = list?.querySelector(`li.item:not(.leaving)`);
|
|
1396
|
+
const measuredHeight = sample?.getBoundingClientRect().height ?? 0;
|
|
1397
|
+
const rowHeight = measuredHeight > 0 ? measuredHeight : this.#getItemHeight();
|
|
1398
|
+
const measuredGap = sample ? Number.parseFloat(getComputedStyle(sample).marginTop) : NaN;
|
|
1399
|
+
const parsedGap = Number.isNaN(measuredGap) ? Number.parseFloat(getComputedStyle(this).getPropertyValue(`--tree-item-gap`)) : measuredGap;
|
|
1400
|
+
const gap = Number.isNaN(parsedGap) ? 1 : parsedGap;
|
|
1401
|
+
let listOffset = this.#metrics.listOffset;
|
|
1402
|
+
if (list) {
|
|
1403
|
+
const padTop = Number.parseFloat(getComputedStyle(list).paddingTop);
|
|
1404
|
+
listOffset = list.offsetTop + (Number.isNaN(padTop) ? 0 : padTop);
|
|
1405
|
+
}
|
|
1406
|
+
const height = this.#container()?.clientHeight ?? 0;
|
|
1407
|
+
const heightChanged = height > 0 && height !== this.#viewportH;
|
|
1408
|
+
if (height > 0) this.#viewportH = height;
|
|
1409
|
+
const prev = this.#metrics;
|
|
1410
|
+
const changed = prev.rowHeight !== rowHeight || prev.gap !== gap || prev.listOffset !== listOffset;
|
|
1411
|
+
this.#metrics = {
|
|
1412
|
+
rowHeight,
|
|
1413
|
+
gap,
|
|
1414
|
+
listOffset
|
|
1415
|
+
};
|
|
1416
|
+
return changed || heightChanged;
|
|
1417
|
+
}
|
|
1418
|
+
/** The window for the current scroll offset. */
|
|
1419
|
+
#currentWindow(count) {
|
|
1420
|
+
return computeTreeWindow(this.#layoutParams(count), this._scrollTop);
|
|
1421
|
+
}
|
|
1422
|
+
/** Pull the container's real scroll offset into state (programmatic and clamped scrolls included). */
|
|
1423
|
+
#syncScrollTopFromContainer() {
|
|
1424
|
+
const top = this.#container()?.scrollTop;
|
|
1425
|
+
if (top !== void 0 && top !== this._scrollTop) this._scrollTop = top;
|
|
1426
|
+
}
|
|
1427
|
+
#onVirtualScroll;
|
|
1428
|
+
/** Re-render only when the scroll has moved the window; scrolling inside it is free. */
|
|
1429
|
+
#syncWindowToScroll() {
|
|
1430
|
+
const container = this.#container();
|
|
1431
|
+
if (!container || !this.root || !this.#virtualActive()) return;
|
|
1432
|
+
const top = container.scrollTop;
|
|
1433
|
+
if (top === this._scrollTop) return;
|
|
1434
|
+
const next = computeTreeWindow(this.#layoutParams(this.#getFlat().rows.length), top);
|
|
1435
|
+
if (next.start !== this.#winStart || next.end !== this.#winEnd) this._scrollTop = top;
|
|
1436
|
+
}
|
|
1437
|
+
/** Bind the window's scroll listener and resize observer to the container once it exists. */
|
|
1438
|
+
#syncVirtualObservers() {
|
|
1439
|
+
const container = this.#container();
|
|
1440
|
+
if (container === this.#virtualScrollContainer) return;
|
|
1441
|
+
this.#virtualScrollContainer?.removeEventListener(`scroll`, this.#onVirtualScroll);
|
|
1442
|
+
this.#resizeObserver?.disconnect();
|
|
1443
|
+
this.#resizeObserver = void 0;
|
|
1444
|
+
this.#virtualScrollContainer = container;
|
|
1445
|
+
if (!container) return;
|
|
1446
|
+
container.addEventListener(`scroll`, this.#onVirtualScroll, { passive: true });
|
|
1447
|
+
if (typeof ResizeObserver !== `undefined`) {
|
|
1448
|
+
this.#resizeObserver = new ResizeObserver((entries) => {
|
|
1449
|
+
const height = entries.at(-1)?.contentRect.height ?? 0;
|
|
1450
|
+
if (height > 0 && height !== this.#viewportH && this.#virtualActive()) this.requestUpdate();
|
|
1451
|
+
});
|
|
1452
|
+
this.#resizeObserver.observe(container);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
#teardownVirtualObservers() {
|
|
1456
|
+
this.#virtualScrollContainer?.removeEventListener(`scroll`, this.#onVirtualScroll);
|
|
1457
|
+
this.#virtualScrollContainer = void 0;
|
|
1458
|
+
this.#resizeObserver?.disconnect();
|
|
1459
|
+
this.#resizeObserver = void 0;
|
|
1460
|
+
if (this.#virtualScrollRaf !== 0) {
|
|
1461
|
+
cancelAnimationFrame(this.#virtualScrollRaf);
|
|
1462
|
+
this.#virtualScrollRaf = 0;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Scroll flat row `index` into view (below the sticky overlay). Writes the
|
|
1467
|
+
* container's `scrollTop` and `_scrollTop` together, so the very next render
|
|
1468
|
+
* already contains the target row — no frame where it is missing.
|
|
1469
|
+
*/
|
|
1470
|
+
#revealIndex(index) {
|
|
1471
|
+
const container = this.#container();
|
|
1472
|
+
if (!container) return;
|
|
1473
|
+
const params = this.#layoutParams(this.#getFlat().rows.length);
|
|
1474
|
+
const inset = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
|
|
1475
|
+
const next = treeScrollTopToReveal(params, index, container.scrollTop, inset);
|
|
1476
|
+
if (next === container.scrollTop && next === this._scrollTop) return;
|
|
1477
|
+
container.scrollTop = next;
|
|
1478
|
+
this._scrollTop = next;
|
|
1479
|
+
}
|
|
1480
|
+
/** Filter match sets, or `undefined` when no filter is active. */
|
|
1481
|
+
#getFilter() {
|
|
1482
|
+
const predicate = this.filterPredicate;
|
|
1483
|
+
const root = this.root;
|
|
1484
|
+
if (!predicate || !root) return void 0;
|
|
1485
|
+
const cached = this.#filterCache;
|
|
1486
|
+
if (cached && cached.predicate === predicate && cached.root === root && cached.version === this.#structureVersion) return cached;
|
|
1487
|
+
const matches = computeFilterMatches(root, predicate);
|
|
1488
|
+
const { parentByKey } = this.#getTreeIndex();
|
|
1489
|
+
const autoExpanded = /* @__PURE__ */ new Set();
|
|
1490
|
+
for (const key of matches) {
|
|
1491
|
+
const parent = parentByKey.get(key);
|
|
1492
|
+
if (parent !== void 0) autoExpanded.add(parent);
|
|
1493
|
+
}
|
|
1494
|
+
const next = {
|
|
1495
|
+
predicate,
|
|
1496
|
+
root,
|
|
1497
|
+
version: this.#structureVersion,
|
|
1498
|
+
matches,
|
|
1499
|
+
autoExpanded
|
|
1500
|
+
};
|
|
1501
|
+
this.#filterCache = next;
|
|
1502
|
+
return next;
|
|
1503
|
+
}
|
|
1504
|
+
/** Structure version for cache keys in subclasses (bumped on root / model change and lazy loads). */
|
|
1505
|
+
get _structureVersion() {
|
|
1506
|
+
return this.#structureVersion;
|
|
1507
|
+
}
|
|
1508
|
+
/** Depths at which any loaded node carries an icon (stable across expansion). */
|
|
1509
|
+
get _depthsWithIcons() {
|
|
1510
|
+
return this.#getTreeIndex().depthsWithIcons;
|
|
1511
|
+
}
|
|
951
1512
|
#unregisterSearchCommands;
|
|
952
1513
|
#effectiveSearchModes() {
|
|
953
1514
|
const raw = this.searchModes?.trim();
|
|
@@ -968,6 +1529,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
968
1529
|
#ownedRegistry;
|
|
969
1530
|
#config;
|
|
970
1531
|
#renderedKeys;
|
|
1532
|
+
/** Keys in the exact DOM order of the last render (ghost rows woven in). */
|
|
1533
|
+
#renderedRowKeys;
|
|
1534
|
+
#renderedRowIndex;
|
|
971
1535
|
#lastNodeByKey;
|
|
972
1536
|
#enteringKeys;
|
|
973
1537
|
#leaving;
|
|
@@ -1002,73 +1566,65 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1002
1566
|
}
|
|
1003
1567
|
}
|
|
1004
1568
|
#onStickyScroll;
|
|
1005
|
-
/**
|
|
1006
|
-
|
|
1007
|
-
|
|
1569
|
+
/**
|
|
1570
|
+
* Row geometry over the rendered `<li>` elements (ghost rows included), in
|
|
1571
|
+
* container content coordinates. `#container` is `position: relative`, so
|
|
1572
|
+
* `offsetTop` is scroll-independent. Reads only — no layout writes.
|
|
1573
|
+
*/
|
|
1574
|
+
#rowReader(rows) {
|
|
1575
|
+
return {
|
|
1576
|
+
count: rows.length,
|
|
1577
|
+
topOf: (i) => rows[i].offsetTop,
|
|
1578
|
+
bottomOf: (i) => rows[i].offsetTop + rows[i].offsetHeight
|
|
1579
|
+
};
|
|
1008
1580
|
}
|
|
1009
1581
|
/**
|
|
1010
1582
|
* Recompute which ancestors stick to the top: the ancestors of the first
|
|
1011
1583
|
* row whose bottom clears the container top, minus any ancestor that is
|
|
1012
1584
|
* itself still visible below the overlay. Synchronous — reads current
|
|
1013
|
-
* geometry, renders nothing, expands nothing.
|
|
1585
|
+
* geometry, renders nothing, expands nothing. O(log N) offset reads + O(depth).
|
|
1014
1586
|
*/
|
|
1015
1587
|
#updateStickyHeaders() {
|
|
1016
1588
|
this.#stickyDirty = false;
|
|
1017
1589
|
const empty = /* @__PURE__ */ new Map();
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1590
|
+
const clear = () => this.#setStickyKeys([], empty);
|
|
1591
|
+
if (!this.stickyHeaders || !this.root) return clear();
|
|
1022
1592
|
const container = this.#container();
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
walk(this.root);
|
|
1040
|
-
const containerTop = container.getBoundingClientRect().top;
|
|
1041
|
-
const visibleThreshold = containerTop + (this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0);
|
|
1042
|
-
let topKey;
|
|
1043
|
-
for (const { node } of nodes) {
|
|
1044
|
-
const li = this.#stickyRowEl(node.item.key);
|
|
1045
|
-
if (!li || li.classList.contains(`leaving`)) continue;
|
|
1046
|
-
if (li.getBoundingClientRect().bottom > containerTop) {
|
|
1047
|
-
topKey = node.item.key;
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
if (topKey === void 0) {
|
|
1052
|
-
this.#setStickyKeys([], empty);
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
const rootKey = this.root.item.key;
|
|
1593
|
+
const list = this.renderRoot.querySelector(`ul.tree-list`);
|
|
1594
|
+
if (!container || !list) return clear();
|
|
1595
|
+
const flat = this.#getFlat();
|
|
1596
|
+
const virtual = this.#virtualActive();
|
|
1597
|
+
const rows = list.children;
|
|
1598
|
+
const keys = this.#renderedRowKeys;
|
|
1599
|
+
if (flat.rows.length === 0) return clear();
|
|
1600
|
+
if (!virtual && (rows.length === 0 || rows.length !== keys.length)) return clear();
|
|
1601
|
+
const reader = virtual ? treeRowReader(this.#layoutParams(flat.rows.length)) : this.#rowReader(rows);
|
|
1602
|
+
const scrollTop = container.scrollTop;
|
|
1603
|
+
const visibleThreshold = scrollTop + (this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0);
|
|
1604
|
+
let topIdx = firstRowBelow(reader, scrollTop);
|
|
1605
|
+
if (!virtual) while (topIdx < rows.length && rows[topIdx].classList.contains(`leaving`)) topIdx++;
|
|
1606
|
+
if (topIdx >= reader.count) return clear();
|
|
1607
|
+
const topFlat = virtual ? topIdx : flat.indexByKey.get(keys[topIdx]);
|
|
1608
|
+
if (topFlat === void 0) return clear();
|
|
1056
1609
|
const chain = [];
|
|
1057
|
-
let cursor =
|
|
1058
|
-
while (cursor !== void 0
|
|
1059
|
-
|
|
1060
|
-
|
|
1610
|
+
let cursor = flat.rows[topFlat].parentKey;
|
|
1611
|
+
while (cursor !== void 0) {
|
|
1612
|
+
const idx = flat.indexByKey.get(cursor);
|
|
1613
|
+
if (idx === void 0) break;
|
|
1614
|
+
chain.unshift(flat.rows[idx]);
|
|
1615
|
+
cursor = flat.rows[idx].parentKey;
|
|
1061
1616
|
}
|
|
1062
|
-
const byKey = new Map(nodes.map((entry) => [entry.node.item.key, entry]));
|
|
1063
1617
|
const entries = /* @__PURE__ */ new Map();
|
|
1064
1618
|
const stuck = [];
|
|
1065
|
-
for (const
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (!entry) continue;
|
|
1619
|
+
for (const row of chain) {
|
|
1620
|
+
const key = row.node.item.key;
|
|
1621
|
+
const domIdx = virtual ? flat.indexByKey.get(key) : this.#renderedRowIndex.get(key);
|
|
1622
|
+
if (domIdx === void 0 || reader.bottomOf(domIdx) > visibleThreshold) continue;
|
|
1070
1623
|
stuck.push(key);
|
|
1071
|
-
entries.set(key,
|
|
1624
|
+
entries.set(key, {
|
|
1625
|
+
node: row.node,
|
|
1626
|
+
depth: row.depth
|
|
1627
|
+
});
|
|
1072
1628
|
}
|
|
1073
1629
|
this.#setStickyKeys(stuck, entries);
|
|
1074
1630
|
}
|
|
@@ -1081,12 +1637,14 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1081
1637
|
#renderStickyHeaders() {
|
|
1082
1638
|
if (!this.stickyHeaders || this._stickyKeys.length === 0) return nothing;
|
|
1083
1639
|
return html`
|
|
1084
|
-
<div class="sticky-
|
|
1085
|
-
|
|
1640
|
+
<div class="sticky-anchor">
|
|
1641
|
+
<div class="sticky-headers" part="sticky-headers">
|
|
1642
|
+
${this._stickyKeys.map((key) => {
|
|
1086
1643
|
const entry = this.#stickyEntries.get(key);
|
|
1087
1644
|
if (!entry) return nothing;
|
|
1088
1645
|
return this.#renderStickyHeader(entry.node, entry.depth);
|
|
1089
1646
|
})}
|
|
1647
|
+
</div>
|
|
1090
1648
|
</div>
|
|
1091
1649
|
`;
|
|
1092
1650
|
}
|
|
@@ -1158,15 +1716,44 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1158
1716
|
}
|
|
1159
1717
|
willUpdate(changedProps) {
|
|
1160
1718
|
super.willUpdate(changedProps);
|
|
1161
|
-
this.#
|
|
1719
|
+
const scrollOnly = this.#flat !== void 0 && changedProps.size === 1 && changedProps.has(`_scrollTop`) && this.#virtualActive();
|
|
1720
|
+
this.#scrollOnlyUpdate = scrollOnly;
|
|
1721
|
+
if (scrollOnly) {
|
|
1722
|
+
this.#syncScrollTopFromContainer();
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
this.#flat = void 0;
|
|
1726
|
+
if (changedProps.has(`root`) || changedProps.has(`model`)) this.#structureVersion++;
|
|
1727
|
+
this.#diffVisibleNodes(changedProps.has(`filterPredicate`));
|
|
1728
|
+
const virtual = this.#virtualActive();
|
|
1729
|
+
this.toggleAttribute(`data-virtual`, virtual);
|
|
1730
|
+
if (virtual) {
|
|
1731
|
+
this.#measureMetrics();
|
|
1732
|
+
this.#syncScrollTopFromContainer();
|
|
1733
|
+
}
|
|
1162
1734
|
if (changedProps.has(`root`) || changedProps.has(`model`) || changedProps.has(`filterPredicate`) || changedProps.has(`stickyHeaders`)) this.#stickyDirty = true;
|
|
1163
1735
|
if (this.#stickyDirty) this.#updateStickyHeaders();
|
|
1164
1736
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1737
|
+
/**
|
|
1738
|
+
* `filterChanged`: typing in a filter adds / removes whole rows from the DOM.
|
|
1739
|
+
* That is not an expand / collapse, so skip the enter / leave animations
|
|
1740
|
+
* (and any ghosts) for this update; `#renderedKeys` is still refreshed.
|
|
1741
|
+
*/
|
|
1742
|
+
#diffVisibleNodes(filterChanged) {
|
|
1743
|
+
const nodes = this.#getFlat().rows;
|
|
1744
|
+
if (this.#virtualActive()) {
|
|
1745
|
+
this.#enteringKeys.clear();
|
|
1746
|
+
this.#cancelAllLeavingAnimations();
|
|
1747
|
+
this.#leaving.clear();
|
|
1748
|
+
this.#renderedKeys = [];
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1167
1751
|
const nowKeys = new Set(nodes.map((n) => n.node.item.key));
|
|
1168
1752
|
this.#enteringKeys.clear();
|
|
1169
|
-
if (
|
|
1753
|
+
if (filterChanged) {
|
|
1754
|
+
this.#cancelAllLeavingAnimations();
|
|
1755
|
+
this.#leaving.clear();
|
|
1756
|
+
} else if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
|
|
1170
1757
|
const prevSet = new Set(this.#renderedKeys);
|
|
1171
1758
|
for (const { node } of nodes) if (!prevSet.has(node.item.key)) this.#enteringKeys.add(node.item.key);
|
|
1172
1759
|
if (this.#enteringKeys.size > MAX_ANIMATED_ROWS) this.#enteringKeys.clear();
|
|
@@ -1189,30 +1776,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1189
1776
|
return this.#leaving.has(node.item.key);
|
|
1190
1777
|
}
|
|
1191
1778
|
/**
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1779
|
+
* Kept for subclass compatibility. Filtered-out rows are no longer rendered
|
|
1780
|
+
* at all (they are omitted from the flat list), so this is `false` for every
|
|
1781
|
+
* rendered row.
|
|
1194
1782
|
*/
|
|
1195
|
-
_isFilteredOut(
|
|
1196
|
-
return
|
|
1197
|
-
}
|
|
1198
|
-
/** Splice the fading ghost rows back into the visible list near where they were. */
|
|
1199
|
-
#weaveLeaving(nodes) {
|
|
1200
|
-
if (this.#leaving.size === 0) return nodes;
|
|
1201
|
-
const result = [...nodes];
|
|
1202
|
-
const indexOfKey = (key) => result.findIndex((e) => e.node.item.key === key);
|
|
1203
|
-
for (const [key, entry] of this.#leaving) {
|
|
1204
|
-
const prevIdx = this.#renderedKeys.indexOf(key);
|
|
1205
|
-
let insertAt = result.length;
|
|
1206
|
-
for (let j = prevIdx + 1; j < this.#renderedKeys.length; j++) {
|
|
1207
|
-
const at = indexOfKey(this.#renderedKeys[j]);
|
|
1208
|
-
if (at !== -1) {
|
|
1209
|
-
insertAt = at;
|
|
1210
|
-
break;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
result.splice(insertAt, 0, entry);
|
|
1214
|
-
}
|
|
1215
|
-
return result;
|
|
1783
|
+
_isFilteredOut(_node, _depth) {
|
|
1784
|
+
return false;
|
|
1216
1785
|
}
|
|
1217
1786
|
#runItemAnimations() {
|
|
1218
1787
|
if (this.#enteringKeys.size > 0) {
|
|
@@ -1252,7 +1821,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1252
1821
|
});
|
|
1253
1822
|
});
|
|
1254
1823
|
}
|
|
1255
|
-
const visible = this
|
|
1824
|
+
const visible = this.#getFlat().rows;
|
|
1256
1825
|
this.#renderedKeys = visible.map((n) => n.node.item.key);
|
|
1257
1826
|
this.#lastNodeByKey.clear();
|
|
1258
1827
|
for (const entry of visible) this.#lastNodeByKey.set(entry.node.item.key, entry);
|
|
@@ -1260,10 +1829,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1260
1829
|
}
|
|
1261
1830
|
updated(changedProps) {
|
|
1262
1831
|
this.#rowsCache = void 0;
|
|
1832
|
+
this.#syncVirtualObservers();
|
|
1833
|
+
if (this.#scrollOnlyUpdate) {
|
|
1834
|
+
this.#scrollOnlyUpdate = false;
|
|
1835
|
+
this.#repaintSearchHighlights();
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1263
1838
|
this.#syncStickyScroll();
|
|
1264
1839
|
if (changedProps.has(`root`) && this.root) {
|
|
1265
1840
|
this.#abortPending();
|
|
1266
|
-
this
|
|
1841
|
+
this.#setExpanded(/* @__PURE__ */ new Set());
|
|
1267
1842
|
this.#clearSelectionInternal();
|
|
1268
1843
|
this.tickled.clearTickled();
|
|
1269
1844
|
this._focusedDepth = 0;
|
|
@@ -1283,8 +1858,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1283
1858
|
this.requestUpdate();
|
|
1284
1859
|
} else if (event.type === `invalidate`) {
|
|
1285
1860
|
if (this._expandedKeys.has(event.key)) {
|
|
1286
|
-
this._expandedKeys
|
|
1287
|
-
this.#stickyDirty = true;
|
|
1861
|
+
this.#setExpanded(withoutKey(this._expandedKeys, event.key));
|
|
1288
1862
|
this.requestUpdate();
|
|
1289
1863
|
}
|
|
1290
1864
|
}
|
|
@@ -1298,7 +1872,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1298
1872
|
if (changedProps.has(`selectedNode`) && this.selectedNode) {
|
|
1299
1873
|
if (changedProps.get(`selectedNode`) !== this.selectedNode) this.tickled.setTickled(this.selectedNode.item.key);
|
|
1300
1874
|
}
|
|
1301
|
-
this.#
|
|
1875
|
+
if (this.#virtualActive()) {
|
|
1876
|
+
this.#renderedKeys = [];
|
|
1877
|
+
this.#lastNodeByKey.clear();
|
|
1878
|
+
if (this.#measureMetrics()) queueMicrotask(() => this.requestUpdate());
|
|
1879
|
+
this.#repaintSearchHighlights();
|
|
1880
|
+
} else this.#runItemAnimations();
|
|
1881
|
+
}
|
|
1882
|
+
/** Highlights are painted on rendered labels, so they move with the window. */
|
|
1883
|
+
#repaintSearchHighlights() {
|
|
1884
|
+
if (this.search.isOpen) this.search.repaintHighlights();
|
|
1302
1885
|
}
|
|
1303
1886
|
connectedCallback() {
|
|
1304
1887
|
super.connectedCallback();
|
|
@@ -1330,8 +1913,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1330
1913
|
return target.closest(`ul.tree-list li[data-key]`)?.getAttribute(`data-key`) ?? void 0;
|
|
1331
1914
|
},
|
|
1332
1915
|
prepareSelection: (originKey) => {
|
|
1333
|
-
const
|
|
1334
|
-
const
|
|
1916
|
+
const flat = this.#getFlat();
|
|
1917
|
+
const allNodes = flat.rows;
|
|
1918
|
+
const originIdx = flat.indexByKey.get(originKey);
|
|
1919
|
+
const originEntry = originIdx === void 0 ? void 0 : allNodes[originIdx];
|
|
1335
1920
|
if (!originEntry) return void 0;
|
|
1336
1921
|
const selectedNodes = new Set(this._selectedNodes);
|
|
1337
1922
|
if (!selectedNodes.has(originEntry.node)) selectedNodes.clear();
|
|
@@ -1420,6 +2005,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1420
2005
|
getDndMode: () => this.itemDnd,
|
|
1421
2006
|
getExpandHoverMs: () => this.dndExpandHoverMs,
|
|
1422
2007
|
getVisibleRows: () => this.#getVisibleRows(),
|
|
2008
|
+
getRowInfo: (key) => {
|
|
2009
|
+
const flat = this.#getFlat();
|
|
2010
|
+
const idx = flat.indexByKey.get(key);
|
|
2011
|
+
if (idx === void 0) return void 0;
|
|
2012
|
+
const { node, depth, parentKey } = flat.rows[idx];
|
|
2013
|
+
return {
|
|
2014
|
+
node,
|
|
2015
|
+
depth,
|
|
2016
|
+
parentKey
|
|
2017
|
+
};
|
|
2018
|
+
},
|
|
2019
|
+
getChildren: (parentKey) => this.#childrenOfKey(parentKey),
|
|
1423
2020
|
getIndentPx: () => this.#getIndentPx(),
|
|
1424
2021
|
getScrollContainer: () => this.#container(),
|
|
1425
2022
|
isSelfOrDescendant: (ancestorKey, key) => this.#isSelfOrDescendant(ancestorKey, key),
|
|
@@ -1492,44 +2089,121 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1492
2089
|
const parsed = Number.parseInt(getComputedStyle(this).getPropertyValue(`--tree-item-height`), 10);
|
|
1493
2090
|
return Number.isNaN(parsed) ? 30 : parsed;
|
|
1494
2091
|
}
|
|
1495
|
-
/**
|
|
1496
|
-
#
|
|
1497
|
-
if (
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
2092
|
+
/** Children of `parentKey` (the root's children when undefined), straight from the tree. */
|
|
2093
|
+
#childrenOfKey(parentKey) {
|
|
2094
|
+
if (parentKey === void 0) return this.root?.children ?? [];
|
|
2095
|
+
const flat = this.#getFlat();
|
|
2096
|
+
const idx = flat.indexByKey.get(parentKey);
|
|
2097
|
+
return idx === void 0 ? [] : flat.rows[idx].node.children ?? [];
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Windowed counterpart of the measured rows: rects come from the layout
|
|
2101
|
+
* arithmetic and one rendered row's horizontal extent, so nothing is looked
|
|
2102
|
+
* up per row and rows outside the window still resolve.
|
|
2103
|
+
*/
|
|
2104
|
+
#collectVirtualDropRows(container, list, out) {
|
|
2105
|
+
const flat = this.#getFlat();
|
|
2106
|
+
const params = this.#layoutParams(flat.rows.length);
|
|
2107
|
+
const range = rowRangeOverlapping(treeRowReader(params), container.scrollTop, container.scrollTop + container.clientHeight, 1);
|
|
2108
|
+
if (!range) return;
|
|
2109
|
+
const containerRect = container.getBoundingClientRect();
|
|
2110
|
+
const sample = list.querySelector(`li.item`)?.getBoundingClientRect() ?? containerRect;
|
|
2111
|
+
for (let i = range.first; i <= range.last; i++) {
|
|
2112
|
+
const { node, depth } = flat.rows[i];
|
|
2113
|
+
const top = containerRect.top - container.scrollTop + treeRowTop(params, i);
|
|
2114
|
+
const rect = {
|
|
2115
|
+
x: sample.left,
|
|
2116
|
+
y: top,
|
|
2117
|
+
left: sample.left,
|
|
2118
|
+
right: sample.left + sample.width,
|
|
2119
|
+
top,
|
|
2120
|
+
bottom: top + params.rowHeight,
|
|
2121
|
+
width: sample.width,
|
|
2122
|
+
height: params.rowHeight,
|
|
2123
|
+
toJSON: () => ({})
|
|
2124
|
+
};
|
|
2125
|
+
out.push({
|
|
1502
2126
|
key: node.item.key,
|
|
1503
2127
|
node,
|
|
1504
2128
|
depth,
|
|
1505
|
-
|
|
2129
|
+
index: i,
|
|
2130
|
+
rect
|
|
1506
2131
|
});
|
|
1507
2132
|
}
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Measured rows for the part of the list that intersects the scroll
|
|
2136
|
+
* container (plus one row of margin each side), cached for the duration of a
|
|
2137
|
+
* drag and invalidated on scroll / update. Each row carries its flat index;
|
|
2138
|
+
* structural lookups (parents, siblings) come from `getRowInfo` / `getChildren`.
|
|
2139
|
+
*/
|
|
2140
|
+
#getVisibleRows() {
|
|
2141
|
+
if (this.#rowsCache) return this.#rowsCache;
|
|
2142
|
+
const rows = [];
|
|
2143
|
+
const container = this.#container();
|
|
2144
|
+
const list = this.renderRoot.querySelector(`ul.tree-list`);
|
|
2145
|
+
const keys = this.#renderedRowKeys;
|
|
2146
|
+
if (container && list && this.#virtualActive()) this.#collectVirtualDropRows(container, list, rows);
|
|
2147
|
+
else if (container && list && list.children.length === keys.length) {
|
|
2148
|
+
const flat = this.#getFlat();
|
|
2149
|
+
const items = list.children;
|
|
2150
|
+
const range = rowRangeOverlapping(this.#rowReader(items), container.scrollTop, container.scrollTop + container.clientHeight, 1);
|
|
2151
|
+
if (range) for (let i = range.first; i <= range.last; i++) {
|
|
2152
|
+
const li = items[i];
|
|
2153
|
+
if (li.classList.contains(`leaving`)) continue;
|
|
2154
|
+
const flatIdx = flat.indexByKey.get(keys[i]);
|
|
2155
|
+
if (flatIdx === void 0) continue;
|
|
2156
|
+
const { node, depth } = flat.rows[flatIdx];
|
|
2157
|
+
rows.push({
|
|
2158
|
+
key: node.item.key,
|
|
2159
|
+
node,
|
|
2160
|
+
depth,
|
|
2161
|
+
index: flatIdx,
|
|
2162
|
+
rect: li.getBoundingClientRect()
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
1508
2166
|
this.#rowsCache = rows;
|
|
1509
2167
|
return rows;
|
|
1510
2168
|
}
|
|
1511
2169
|
/** Route spring-load through the real expand path so lazy children load and `expand` fires with the right depth. */
|
|
1512
2170
|
#springLoadExpand(key) {
|
|
1513
2171
|
if (this._expandedKeys.has(key)) return;
|
|
1514
|
-
const
|
|
2172
|
+
const flat = this.#getFlat();
|
|
2173
|
+
const idx = flat.indexByKey.get(key);
|
|
2174
|
+
const entry = idx === void 0 ? void 0 : flat.rows[idx];
|
|
1515
2175
|
if (!entry || entry.node.item.isLeaf === true) return;
|
|
1516
2176
|
this.#toggleExpand(entry.node, entry.depth);
|
|
1517
2177
|
this.#rowsCache = void 0;
|
|
1518
2178
|
this.requestUpdate();
|
|
1519
2179
|
}
|
|
2180
|
+
/** O(depth): walk up the whole-tree parent map. */
|
|
1520
2181
|
#isSelfOrDescendant(ancestorKey, key) {
|
|
1521
2182
|
if (ancestorKey === key) return true;
|
|
1522
|
-
|
|
1523
|
-
let
|
|
1524
|
-
while (
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
parent = this.#findParentByKey(node.item.key);
|
|
2183
|
+
const { parentByKey } = this.#getTreeIndex();
|
|
2184
|
+
let cursor = parentByKey.get(key);
|
|
2185
|
+
while (cursor !== void 0) {
|
|
2186
|
+
if (cursor === ancestorKey) return true;
|
|
2187
|
+
cursor = parentByKey.get(cursor);
|
|
1528
2188
|
}
|
|
1529
2189
|
return false;
|
|
1530
2190
|
}
|
|
1531
2191
|
#findParentByKey(key) {
|
|
1532
|
-
|
|
2192
|
+
const parentKey = this.#getTreeIndex().parentByKey.get(key);
|
|
2193
|
+
if (parentKey === void 0 || parentKey === this.root?.item.key) return void 0;
|
|
2194
|
+
return this.#nodeByKey(parentKey);
|
|
2195
|
+
}
|
|
2196
|
+
/** Node for a key anywhere in the loaded tree (via the flat list when visible). */
|
|
2197
|
+
#nodeByKey(key) {
|
|
2198
|
+
const flat = this.#getFlat();
|
|
2199
|
+
const idx = flat.indexByKey.get(key);
|
|
2200
|
+
if (idx !== void 0) return flat.rows[idx].node;
|
|
2201
|
+
const { parentByKey } = this.#getTreeIndex();
|
|
2202
|
+
const path = [];
|
|
2203
|
+
for (let cursor = key; cursor !== void 0 && cursor !== this.root?.item.key; cursor = parentByKey.get(cursor)) path.unshift(cursor);
|
|
2204
|
+
let node = this.root;
|
|
2205
|
+
for (const k of path) node = node?.children?.find((c) => c.item.key === k);
|
|
2206
|
+
return node;
|
|
1533
2207
|
}
|
|
1534
2208
|
/**
|
|
1535
2209
|
* Decide whether a DataTransfer is acceptable and resolve the dragged set.
|
|
@@ -1553,7 +2227,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1553
2227
|
return {
|
|
1554
2228
|
source: `internal`,
|
|
1555
2229
|
keys,
|
|
1556
|
-
nodes: keys.map((k) => this.#
|
|
2230
|
+
nodes: keys.map((k) => this.#nodeByKey(k)).filter((n) => n !== void 0)
|
|
1557
2231
|
};
|
|
1558
2232
|
}
|
|
1559
2233
|
if (phase === `drop`) try {
|
|
@@ -1597,7 +2271,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1597
2271
|
}
|
|
1598
2272
|
#anyRowDraggable() {
|
|
1599
2273
|
if (this.itemDnd === `none`) return false;
|
|
1600
|
-
const allNodes = this.#
|
|
2274
|
+
const allNodes = this.#getFlat().rows;
|
|
1601
2275
|
if (allNodes.length === 0) return false;
|
|
1602
2276
|
if (!this.canDrag) return true;
|
|
1603
2277
|
return allNodes.some(({ node }) => this.canDrag({
|
|
@@ -1619,6 +2293,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1619
2293
|
this.#postDragSuppressTimer = void 0;
|
|
1620
2294
|
}
|
|
1621
2295
|
this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
|
|
2296
|
+
this.#teardownVirtualObservers();
|
|
1622
2297
|
if (this.#stickyScrollBound) {
|
|
1623
2298
|
this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
|
|
1624
2299
|
this.#stickyScrollContainer = void 0;
|
|
@@ -1672,12 +2347,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1672
2347
|
}
|
|
1673
2348
|
selectAll() {
|
|
1674
2349
|
const prev = new Set(this._selectedNodes);
|
|
1675
|
-
if (this.interactionMode === `checked`) for (const { node } of this.#
|
|
2350
|
+
if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
|
|
1676
2351
|
if (!this.#canSelect(node)) continue;
|
|
1677
2352
|
this.#applyCheckedToggle(node, true);
|
|
1678
2353
|
}
|
|
1679
2354
|
else {
|
|
1680
|
-
for (const { node } of this.#
|
|
2355
|
+
for (const { node } of this.#getFlat().rows) {
|
|
1681
2356
|
if (!this.#canSelect(node)) continue;
|
|
1682
2357
|
this._selectedNodes.add(node);
|
|
1683
2358
|
}
|
|
@@ -1688,7 +2363,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1688
2363
|
}
|
|
1689
2364
|
deselectAll() {
|
|
1690
2365
|
const prev = new Set(this._selectedNodes);
|
|
1691
|
-
if (this.interactionMode === `checked`) for (const { node } of this.#
|
|
2366
|
+
if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
|
|
1692
2367
|
if (!this.#canSelect(node)) continue;
|
|
1693
2368
|
this.#applyCheckedToggle(node, false);
|
|
1694
2369
|
}
|
|
@@ -1709,7 +2384,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1709
2384
|
const found = children.find((n) => n.item.key === key);
|
|
1710
2385
|
if (!found) return false;
|
|
1711
2386
|
this.#applyExclusivity(found.item.key);
|
|
1712
|
-
this._expandedKeys
|
|
2387
|
+
this.#setExpanded(withKey(this._expandedKeys, found.item.key));
|
|
1713
2388
|
if (key === keys.at(-1)) {
|
|
1714
2389
|
this.select(found);
|
|
1715
2390
|
this.tickled.setTickled(found.item.key);
|
|
@@ -1734,7 +2409,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1734
2409
|
if (!path) return false;
|
|
1735
2410
|
for (let i = 0; i < path.length - 1; i++) {
|
|
1736
2411
|
this.#applyExclusivity(path[i]);
|
|
1737
|
-
this._expandedKeys
|
|
2412
|
+
this.#setExpanded(withKey(this._expandedKeys, path[i]));
|
|
1738
2413
|
}
|
|
1739
2414
|
this.tickled.setTickled(key);
|
|
1740
2415
|
this.#stickyDirty = true;
|
|
@@ -1770,10 +2445,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1770
2445
|
return true;
|
|
1771
2446
|
}
|
|
1772
2447
|
#clearSelectionInternal() {
|
|
2448
|
+
this._selectionVersion++;
|
|
1773
2449
|
this._selectedNodes.clear();
|
|
1774
2450
|
this.selectedNode = void 0;
|
|
1775
2451
|
}
|
|
1776
2452
|
#dispatchSelect(previous) {
|
|
2453
|
+
this._selectionVersion++;
|
|
1777
2454
|
const detail = {
|
|
1778
2455
|
selected: new Set(this._selectedNodes),
|
|
1779
2456
|
previous
|
|
@@ -1879,9 +2556,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1879
2556
|
return;
|
|
1880
2557
|
}
|
|
1881
2558
|
if (isShift && this.#anchor) {
|
|
1882
|
-
const
|
|
1883
|
-
const
|
|
1884
|
-
const
|
|
2559
|
+
const flat = this.#getFlat();
|
|
2560
|
+
const visible = flat.rows;
|
|
2561
|
+
const anchorIdx = flat.indexByKey.get(this.#anchor.node.item.key) ?? -1;
|
|
2562
|
+
const targetIdx = flat.indexByKey.get(node.item.key) ?? -1;
|
|
1885
2563
|
if (anchorIdx >= 0 && targetIdx >= 0) {
|
|
1886
2564
|
const prev = new Set(this._selectedNodes);
|
|
1887
2565
|
const from = Math.min(anchorIdx, targetIdx);
|
|
@@ -1931,9 +2609,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1931
2609
|
return;
|
|
1932
2610
|
}
|
|
1933
2611
|
if (isShift && this.#anchor) {
|
|
1934
|
-
const
|
|
1935
|
-
const
|
|
1936
|
-
const
|
|
2612
|
+
const flat = this.#getFlat();
|
|
2613
|
+
const visible = flat.rows;
|
|
2614
|
+
const anchorIdx = flat.indexByKey.get(this.#anchor.node.item.key) ?? -1;
|
|
2615
|
+
const targetIdx = flat.indexByKey.get(node.item.key) ?? -1;
|
|
1937
2616
|
if (anchorIdx >= 0 && targetIdx >= 0) {
|
|
1938
2617
|
const prev = new Set(this._selectedNodes);
|
|
1939
2618
|
const from = Math.min(anchorIdx, targetIdx);
|
|
@@ -1976,7 +2655,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1976
2655
|
const isExpanded = this._expandedKeys.has(node.item.key);
|
|
1977
2656
|
this.#stickyDirty = true;
|
|
1978
2657
|
if (isExpanded) {
|
|
1979
|
-
this._expandedKeys
|
|
2658
|
+
this.#setExpanded(withoutKey(this._expandedKeys, node.item.key));
|
|
1980
2659
|
this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
1981
2660
|
detail: {
|
|
1982
2661
|
node,
|
|
@@ -1995,7 +2674,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
1995
2674
|
}
|
|
1996
2675
|
} else {
|
|
1997
2676
|
this.#applyExclusivity(node.item.key);
|
|
1998
|
-
this._expandedKeys
|
|
2677
|
+
this.#setExpanded(withKey(this._expandedKeys, node.item.key));
|
|
1999
2678
|
this.dispatchEvent(new CustomEvent(`expand`, {
|
|
2000
2679
|
detail: {
|
|
2001
2680
|
node,
|
|
@@ -2019,7 +2698,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2019
2698
|
if (node.children) for (const child of node.children) this.#applyCheckedToggle(child, selected);
|
|
2020
2699
|
}
|
|
2021
2700
|
#getFlatIndexOf(node) {
|
|
2022
|
-
return this.#
|
|
2701
|
+
return this.#getFlat().indexByKey.get(node.item.key) ?? -1;
|
|
2023
2702
|
}
|
|
2024
2703
|
#extendRangeSelection(nodes, fromIdx, toIdx) {
|
|
2025
2704
|
if (!this.#anchor) {
|
|
@@ -2030,7 +2709,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2030
2709
|
};
|
|
2031
2710
|
}
|
|
2032
2711
|
if (!this.#anchor) return;
|
|
2033
|
-
const anchorIdx =
|
|
2712
|
+
const anchorIdx = this.#getFlat().indexByKey.get(this.#anchor.node.item.key) ?? -1;
|
|
2034
2713
|
if (anchorIdx < 0) return;
|
|
2035
2714
|
const from = Math.min(anchorIdx, toIdx);
|
|
2036
2715
|
const to = Math.max(anchorIdx, toIdx);
|
|
@@ -2066,8 +2745,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2066
2745
|
}, controller.signal), controller.signal)];
|
|
2067
2746
|
if (controller.signal.aborted) return;
|
|
2068
2747
|
node.children = children;
|
|
2748
|
+
this.#structureVersion++;
|
|
2069
2749
|
this.#applyExclusivity(node.item.key);
|
|
2070
|
-
this._expandedKeys
|
|
2750
|
+
this.#setExpanded(withKey(this._expandedKeys, node.item.key));
|
|
2751
|
+
this.#flat = void 0;
|
|
2071
2752
|
this.#stickyDirty = true;
|
|
2072
2753
|
this.#pendingNode = void 0;
|
|
2073
2754
|
this.#pendingController = void 0;
|
|
@@ -2083,6 +2764,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2083
2764
|
} catch {
|
|
2084
2765
|
if (controller.signal.aborted) return;
|
|
2085
2766
|
node.children = [];
|
|
2767
|
+
this.#structureVersion++;
|
|
2768
|
+
this.#flat = void 0;
|
|
2086
2769
|
this.#pendingNode = void 0;
|
|
2087
2770
|
this.#pendingController = void 0;
|
|
2088
2771
|
this.requestUpdate();
|
|
@@ -2094,33 +2777,44 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2094
2777
|
_isFocused(node) {
|
|
2095
2778
|
return this.tickled.isTickled(node.item.key);
|
|
2096
2779
|
}
|
|
2097
|
-
_isExpanded(node,
|
|
2780
|
+
_isExpanded(node, _depth = 0) {
|
|
2098
2781
|
if (this._expandedKeys.has(node.item.key)) return true;
|
|
2099
|
-
|
|
2100
|
-
return false;
|
|
2101
|
-
}
|
|
2102
|
-
#matchesFilter(node, depth) {
|
|
2103
|
-
if (!this.filterPredicate) return true;
|
|
2104
|
-
if (this.filterPredicate(node, depth)) return true;
|
|
2105
|
-
return node.children?.some((child) => this.#matchesFilter(child, depth + 1)) ?? false;
|
|
2782
|
+
return this.#getFilter()?.autoExpanded.has(node.item.key) ?? false;
|
|
2106
2783
|
}
|
|
2107
2784
|
get _pendingNode() {
|
|
2108
2785
|
return this.#pendingNode;
|
|
2109
2786
|
}
|
|
2110
2787
|
#scrollKeyIntoView(key) {
|
|
2788
|
+
if (this.#virtualActive()) {
|
|
2789
|
+
this.updateComplete.then(() => {
|
|
2790
|
+
const idx = this.#getFlat().indexByKey.get(key);
|
|
2791
|
+
if (idx !== void 0) this.#revealIndex(idx);
|
|
2792
|
+
});
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const token = ++this.#revealToken;
|
|
2111
2796
|
requestAnimationFrame(() => {
|
|
2112
|
-
|
|
2113
|
-
const
|
|
2114
|
-
if (
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
const relativeTop = elRect.top - containerRect.top;
|
|
2118
|
-
const relativeBottom = relativeTop + elRect.height;
|
|
2119
|
-
const overlayHeight = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
|
|
2120
|
-
if (relativeTop < overlayHeight) container.scrollTop += relativeTop - overlayHeight;
|
|
2121
|
-
else if (relativeBottom > container.clientHeight) container.scrollTop += relativeBottom - container.clientHeight;
|
|
2797
|
+
this.#revealRowElement(key);
|
|
2798
|
+
const running = this.#container()?.getAnimations?.({ subtree: true }) ?? [];
|
|
2799
|
+
if (running.length > 0) Promise.allSettled(running.map((a) => a.finished)).then(() => {
|
|
2800
|
+
if (token === this.#revealToken) this.#revealRowElement(key);
|
|
2801
|
+
});
|
|
2122
2802
|
});
|
|
2123
2803
|
}
|
|
2804
|
+
#revealToken;
|
|
2805
|
+
/** Scroll the rendered row for `key` into the container, below the sticky overlay. No-op if it isn't rendered or is already visible. */
|
|
2806
|
+
#revealRowElement(key) {
|
|
2807
|
+
const container = this.#container();
|
|
2808
|
+
const el = this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(key)}"]`);
|
|
2809
|
+
if (!el || !container) return;
|
|
2810
|
+
const elRect = el.getBoundingClientRect();
|
|
2811
|
+
const containerRect = container.getBoundingClientRect();
|
|
2812
|
+
const relativeTop = elRect.top - containerRect.top;
|
|
2813
|
+
const relativeBottom = relativeTop + elRect.height;
|
|
2814
|
+
const overlayHeight = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
|
|
2815
|
+
if (relativeTop < overlayHeight) container.scrollTop += relativeTop - overlayHeight;
|
|
2816
|
+
else if (relativeBottom > container.clientHeight) container.scrollTop += relativeBottom - container.clientHeight;
|
|
2817
|
+
}
|
|
2124
2818
|
#abortPending() {
|
|
2125
2819
|
if (this.#pendingController) {
|
|
2126
2820
|
this.#pendingController.abort();
|
|
@@ -2134,14 +2828,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2134
2828
|
if (this.exclusivity === `depth`) this.#collapseSiblingsAtDepth(expandKey);
|
|
2135
2829
|
else if (this.exclusivity === `global`) {
|
|
2136
2830
|
const ancestorKeys = this.#findAncestorKeys(expandKey);
|
|
2137
|
-
|
|
2831
|
+
this.#setExpanded(withoutKeys(this._expandedKeys, (key) => !ancestorKeys.has(key) && key !== expandKey));
|
|
2138
2832
|
}
|
|
2139
2833
|
}
|
|
2140
2834
|
#collapseSiblingsAtDepth(expandKey) {
|
|
2141
2835
|
if (!this.root) return;
|
|
2142
2836
|
const parent = this.#findParent(this.root, expandKey);
|
|
2143
2837
|
if (!parent?.children) return;
|
|
2144
|
-
|
|
2838
|
+
const siblingKeys = new Set(parent.children.map((c) => c.item.key));
|
|
2839
|
+
this.#setExpanded(withoutKeys(this._expandedKeys, (key) => key !== expandKey && siblingKeys.has(key)));
|
|
2145
2840
|
}
|
|
2146
2841
|
#findAncestorKeys(targetKey) {
|
|
2147
2842
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -2165,30 +2860,13 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2165
2860
|
if (found) return found;
|
|
2166
2861
|
}
|
|
2167
2862
|
}
|
|
2168
|
-
#getVisibleNodes(node, depth) {
|
|
2169
|
-
const result = [];
|
|
2170
|
-
const visit = (n, d) => {
|
|
2171
|
-
result.push({
|
|
2172
|
-
node: n,
|
|
2173
|
-
depth: d
|
|
2174
|
-
});
|
|
2175
|
-
if (n.children && this._isExpanded(n, d)) for (const child of n.children) visit(child, d + 1);
|
|
2176
|
-
};
|
|
2177
|
-
if (node.children) for (const child of node.children) visit(child, depth + 1);
|
|
2178
|
-
return result;
|
|
2179
|
-
}
|
|
2180
|
-
#getAllNodes() {
|
|
2181
|
-
if (!this.root) return [];
|
|
2182
|
-
return this.#getVisibleNodes(this.root, -1);
|
|
2183
|
-
}
|
|
2184
2863
|
#getFocusedIndex() {
|
|
2185
|
-
const nodes = this.#getAllNodes();
|
|
2186
2864
|
const tickledKey = this.tickled.tickledId;
|
|
2187
2865
|
if (!tickledKey) return 0;
|
|
2188
|
-
return
|
|
2866
|
+
return this.#getFlat().indexByKey.get(tickledKey) ?? 0;
|
|
2189
2867
|
}
|
|
2190
2868
|
#setFocusedIndex(index) {
|
|
2191
|
-
const nodes = this.#
|
|
2869
|
+
const nodes = this.#getFlat().rows;
|
|
2192
2870
|
if (index < 0 || index >= nodes.length) return;
|
|
2193
2871
|
const { node, depth } = nodes[index];
|
|
2194
2872
|
this.tickled.setTickled(node.item.key);
|
|
@@ -2202,6 +2880,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2202
2880
|
composed: true
|
|
2203
2881
|
}));
|
|
2204
2882
|
this.requestUpdate();
|
|
2883
|
+
if (this.#virtualActive()) {
|
|
2884
|
+
this.#revealIndex(index);
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2205
2887
|
this.updateComplete.then(() => this.#scrollTickledIntoView());
|
|
2206
2888
|
}
|
|
2207
2889
|
#scrollTickledIntoView() {
|
|
@@ -2212,12 +2894,25 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2212
2894
|
behavior: `smooth`
|
|
2213
2895
|
});
|
|
2214
2896
|
}
|
|
2897
|
+
/** Rows per PageUp / PageDown: one viewport, from the current CSS metrics. */
|
|
2898
|
+
#pageSize() {
|
|
2899
|
+
this.#measureMetrics();
|
|
2900
|
+
return treePageSize(this.#layoutParams(this.#getFlat().rows.length));
|
|
2901
|
+
}
|
|
2902
|
+
/** Move the cursor to `nextIdx`, applying the interaction mode's selection side effects. */
|
|
2903
|
+
#navigateToIndex(nodes, currentIdx, nextIdx, shiftKey) {
|
|
2904
|
+
if (this.interactionMode === `implicit`) this.select(nodes[nextIdx].node);
|
|
2905
|
+
this.#setFocusedIndex(nextIdx);
|
|
2906
|
+
if (shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, nextIdx);
|
|
2907
|
+
else if (this.interactionMode === `vscode`) this.#anchor = void 0;
|
|
2908
|
+
this.requestUpdate();
|
|
2909
|
+
}
|
|
2215
2910
|
#onKeyDown;
|
|
2216
2911
|
renderLoadingIndicator() {
|
|
2217
2912
|
return html`<span class="loading-spinner"></span>`;
|
|
2218
2913
|
}
|
|
2219
2914
|
render() {
|
|
2220
|
-
const nodes = this.#
|
|
2915
|
+
const nodes = this.#getFlat().rows;
|
|
2221
2916
|
return html`
|
|
2222
2917
|
<div
|
|
2223
2918
|
id="container"
|
|
@@ -2240,12 +2935,51 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2240
2935
|
${this.renderLoadingIndicator()}
|
|
2241
2936
|
</div>
|
|
2242
2937
|
`;
|
|
2938
|
+
if (this.#virtualActive()) return this.#renderWindow(nodes);
|
|
2939
|
+
const woven = weaveGhosts(nodes, this.#renderedRowKeys, this.#leaving);
|
|
2940
|
+
this.#renderedRowKeys = woven.map((e) => e.node.item.key);
|
|
2941
|
+
this.#renderedRowIndex = new Map(this.#renderedRowKeys.map((key, i) => [key, i]));
|
|
2243
2942
|
return html`
|
|
2244
2943
|
<ul class="tree-list" role="tree">
|
|
2245
|
-
${repeat(
|
|
2944
|
+
${repeat(woven, ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
|
|
2945
|
+
</ul>
|
|
2946
|
+
`;
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Windowed render: the rows near the viewport between two spacers that hold
|
|
2950
|
+
* the rest of the scroll height. Keyed, so a row that stays in the window
|
|
2951
|
+
* keeps its element while others enter and leave at the edges.
|
|
2952
|
+
*/
|
|
2953
|
+
#renderWindow(nodes) {
|
|
2954
|
+
const params = this.#layoutParams(nodes.length);
|
|
2955
|
+
const { start, end } = this.#currentWindow(nodes.length);
|
|
2956
|
+
this.#winStart = start;
|
|
2957
|
+
this.#winEnd = end;
|
|
2958
|
+
this.#renderedRowKeys = [];
|
|
2959
|
+
this.#renderedRowIndex = /* @__PURE__ */ new Map();
|
|
2960
|
+
const stride = treeStride(params);
|
|
2961
|
+
return html`
|
|
2962
|
+
<ul class="tree-list virtual" role="tree" aria-rowcount=${nodes.length}>
|
|
2963
|
+
<li class="virtual-spacer" role="presentation" aria-hidden="true" style=${`height: ${start * stride}px`}></li>
|
|
2964
|
+
${repeat(nodes.slice(start, end), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
|
|
2965
|
+
<li class="virtual-spacer" role="presentation" aria-hidden="true" style=${`height: ${(nodes.length - end) * stride}px`}></li>
|
|
2246
2966
|
</ul>
|
|
2247
2967
|
`;
|
|
2248
2968
|
}
|
|
2969
|
+
/**
|
|
2970
|
+
* `{ posinset, setsize }` for `aria-posinset` / `aria-setsize`. A windowed
|
|
2971
|
+
* list has most siblings out of the DOM, so assistive tech cannot count them.
|
|
2972
|
+
*/
|
|
2973
|
+
_ariaSetInfo(node) {
|
|
2974
|
+
const flat = this.#getFlat();
|
|
2975
|
+
const idx = flat.indexByKey.get(node.item.key);
|
|
2976
|
+
if (idx === void 0) return void 0;
|
|
2977
|
+
const { posInSet, setSize } = flat.rows[idx];
|
|
2978
|
+
return {
|
|
2979
|
+
posInSet,
|
|
2980
|
+
setSize
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2249
2983
|
/** Shared drag-and-drop render state for a row, used by every `_renderNode` variant. */
|
|
2250
2984
|
_dndRowState(node) {
|
|
2251
2985
|
const draggable = this.itemDnd !== `none` && (this.root?.children?.length ?? 0) > 0 && (this.itemDnd === `internal` || this.itemDnd === `both` || this.itemDnd === `external`) && (this.canDrag?.({
|
|
@@ -2269,17 +3003,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2269
3003
|
const isBranch = node.item.isLeaf !== true;
|
|
2270
3004
|
const isExpanded = this._isExpanded(node, depth);
|
|
2271
3005
|
const isPending = this.#pendingNode === node;
|
|
2272
|
-
const isFiltered = this._isFilteredOut(node, depth);
|
|
2273
3006
|
const isLeaving = this._isLeaving(node);
|
|
2274
3007
|
const dnd = this._dndRowState(node);
|
|
2275
3008
|
return html`
|
|
2276
3009
|
<li
|
|
2277
|
-
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${
|
|
3010
|
+
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
|
|
2278
3011
|
role="treeitem"
|
|
2279
3012
|
aria-selected=${selected}
|
|
2280
3013
|
aria-expanded=${isBranch ? isExpanded : nothing}
|
|
2281
3014
|
aria-hidden=${isLeaving ? `true` : nothing}
|
|
2282
3015
|
aria-level=${depth + 1}
|
|
3016
|
+
aria-setsize=${this._ariaSetInfo(node)?.setSize ?? nothing}
|
|
3017
|
+
aria-posinset=${this._ariaSetInfo(node)?.posInSet ?? nothing}
|
|
2283
3018
|
data-key=${node.item.key}
|
|
2284
3019
|
style=${`--depth: ${depth}`}
|
|
2285
3020
|
draggable=${dnd.draggable ? `true` : nothing}
|
|
@@ -2320,6 +3055,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2320
3055
|
--tree-muted-text: var(--surface-muted-text);
|
|
2321
3056
|
--tree-indent: 1ch;
|
|
2322
3057
|
--tree-item-height: 30px;
|
|
3058
|
+
/* Margin above each row. A windowed list is positioned by (height + gap), so keep it in px. */
|
|
3059
|
+
--tree-item-gap: 1px;
|
|
2323
3060
|
--tree-caret-size: 30px;
|
|
2324
3061
|
--tree-radius: var(--radius-s);
|
|
2325
3062
|
--tree-drop-line: var(--accent);
|
|
@@ -2343,6 +3080,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2343
3080
|
The scroll lives here, not on :host, so the search popover (a sibling of
|
|
2344
3081
|
#container, positioned against :host) stays put while the list scrolls. */
|
|
2345
3082
|
#container {
|
|
3083
|
+
/* offsetParent for rows: row offsets are scroll-independent (sticky / DnD geometry). */
|
|
3084
|
+
position: relative;
|
|
2346
3085
|
outline: none;
|
|
2347
3086
|
min-height: 100%;
|
|
2348
3087
|
max-height: 100%;
|
|
@@ -2389,7 +3128,40 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2389
3128
|
font-size: var(--text-m);
|
|
2390
3129
|
border-radius: var(--tree-radius);
|
|
2391
3130
|
transition: background var(--transition, 0.15s ease-out);
|
|
2392
|
-
margin:
|
|
3131
|
+
margin: var(--tree-item-gap) 0;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
/* Windowed list: rows are placed purely by stride arithmetic — a margin above
|
|
3135
|
+
each row only (no collapsing), spacers hold the offscreen height. */
|
|
3136
|
+
.tree-list.virtual .item {
|
|
3137
|
+
margin: var(--tree-item-gap) 0 0;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
.virtual-spacer {
|
|
3141
|
+
display: block;
|
|
3142
|
+
margin: 0;
|
|
3143
|
+
padding: 0;
|
|
3144
|
+
list-style: none;
|
|
3145
|
+
pointer-events: none;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
.sticky-anchor {
|
|
3149
|
+
display: contents;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
:host([data-virtual]) .sticky-anchor {
|
|
3153
|
+
position: sticky;
|
|
3154
|
+
top: 0;
|
|
3155
|
+
z-index: 5;
|
|
3156
|
+
display: block;
|
|
3157
|
+
height: 0;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
:host([data-virtual]) .sticky-headers {
|
|
3161
|
+
position: absolute;
|
|
3162
|
+
top: 0;
|
|
3163
|
+
left: 0;
|
|
3164
|
+
right: 0;
|
|
2393
3165
|
}
|
|
2394
3166
|
|
|
2395
3167
|
.tree-caret {
|
|
@@ -2444,10 +3216,6 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
|
|
|
2444
3216
|
to { transform: rotate(360deg); }
|
|
2445
3217
|
}
|
|
2446
3218
|
|
|
2447
|
-
.item.filtered {
|
|
2448
|
-
display: none;
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
3219
|
/* Fading-out ghost row (see #runItemAnimations) — inert while it collapses. */
|
|
2452
3220
|
.item.leaving {
|
|
2453
3221
|
pointer-events: none;
|
|
@@ -2558,6 +3326,14 @@ __decorate([property({
|
|
|
2558
3326
|
toAttribute: (value) => value ? null : `false`
|
|
2559
3327
|
}
|
|
2560
3328
|
})], TreeBaseElement.prototype, "stickyHeaders", void 0);
|
|
3329
|
+
__decorate([property({
|
|
3330
|
+
reflect: true,
|
|
3331
|
+
attribute: `virtualize`
|
|
3332
|
+
})], TreeBaseElement.prototype, "virtualize", void 0);
|
|
3333
|
+
__decorate([property({
|
|
3334
|
+
type: Number,
|
|
3335
|
+
attribute: `virtualize-threshold`
|
|
3336
|
+
})], TreeBaseElement.prototype, "virtualizeThreshold", void 0);
|
|
2561
3337
|
__decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewRenderer", void 0);
|
|
2562
3338
|
__decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewSource", void 0);
|
|
2563
3339
|
__decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrag", void 0);
|
|
@@ -2565,33 +3341,33 @@ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrop
|
|
|
2565
3341
|
__decorate([property({ attribute: false })], TreeBaseElement.prototype, "parseExternalDrop", void 0);
|
|
2566
3342
|
__decorate([property({ attribute: false })], TreeBaseElement.prototype, "applyInternalDrop", void 0);
|
|
2567
3343
|
__decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
|
|
3344
|
+
__decorate([state()], TreeBaseElement.prototype, "_scrollTop", void 0);
|
|
2568
3345
|
__decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
|
|
2569
3346
|
__decorate([state()], TreeBaseElement.prototype, "_stickyKeys", void 0);
|
|
2570
3347
|
TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
|
|
2571
3348
|
//#endregion
|
|
2572
3349
|
//#region src/tree/tree-list.ts
|
|
2573
|
-
function computeCheckboxState(node, selectedNodes) {
|
|
2574
|
-
if (node.item.isLeaf || !node.children?.length) return selectedNodes.has(node) ? `checked` : `unchecked`;
|
|
2575
|
-
const childStates = node.children.map((c) => computeCheckboxState(c, selectedNodes));
|
|
2576
|
-
const allChecked = childStates.every((s) => s === `checked`);
|
|
2577
|
-
const allUnchecked = childStates.every((s) => s === `unchecked`);
|
|
2578
|
-
if (allChecked) return `checked`;
|
|
2579
|
-
if (allUnchecked) return `unchecked`;
|
|
2580
|
-
return `indeterminate`;
|
|
2581
|
-
}
|
|
2582
3350
|
let TreeListElement = class TreeListElement extends TreeBaseElement {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
3351
|
+
/** Checkbox tri-states for the whole loaded tree, rebuilt only when the structure or selection changes. */
|
|
3352
|
+
#checkboxCache;
|
|
3353
|
+
willUpdate(changedProps) {
|
|
3354
|
+
super.willUpdate(changedProps);
|
|
3355
|
+
this.#syncCheckboxStates();
|
|
2587
3356
|
}
|
|
2588
|
-
#
|
|
2589
|
-
this
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
3357
|
+
#syncCheckboxStates() {
|
|
3358
|
+
const root = this.root;
|
|
3359
|
+
if (this.interactionMode !== `checked` || !root) {
|
|
3360
|
+
this.#checkboxCache = void 0;
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
const c = this.#checkboxCache;
|
|
3364
|
+
if (c && c.root === root && c.structureVersion === this._structureVersion && c.selectionVersion === this._selectionVersion) return;
|
|
3365
|
+
this.#checkboxCache = {
|
|
3366
|
+
root,
|
|
3367
|
+
structureVersion: this._structureVersion,
|
|
3368
|
+
selectionVersion: this._selectionVersion,
|
|
3369
|
+
states: computeCheckboxStates(root, this._selectedNodes)
|
|
2593
3370
|
};
|
|
2594
|
-
if (this.root) visit(this.root, -1);
|
|
2595
3371
|
}
|
|
2596
3372
|
renderNodeContent(node, depth) {
|
|
2597
3373
|
const isBranch = node.item.isLeaf !== true;
|
|
@@ -2609,7 +3385,7 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
|
|
|
2609
3385
|
</span>
|
|
2610
3386
|
`;
|
|
2611
3387
|
if (this.interactionMode === `checked`) {
|
|
2612
|
-
const state =
|
|
3388
|
+
const state = this.#checkboxCache?.states.get(node.item.key) ?? `unchecked`;
|
|
2613
3389
|
return html`${this.#renderCheckbox(node, depth, state)}${caretAndLabel}`;
|
|
2614
3390
|
}
|
|
2615
3391
|
return caretAndLabel;
|
|
@@ -2643,19 +3419,20 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
|
|
|
2643
3419
|
const isBranch = node.item.isLeaf !== true;
|
|
2644
3420
|
const isExpanded = this._isExpanded(node, depth);
|
|
2645
3421
|
const isPending = this._pendingNode === node;
|
|
2646
|
-
const hasIconAtDepth = this
|
|
3422
|
+
const hasIconAtDepth = this._depthsWithIcons.has(depth);
|
|
2647
3423
|
const isLeaving = this._isLeaving(node);
|
|
2648
|
-
const isFiltered = this._isFilteredOut(node, depth);
|
|
2649
3424
|
const dnd = this._dndRowState(node);
|
|
2650
3425
|
return html`
|
|
2651
3426
|
<li
|
|
2652
|
-
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${
|
|
3427
|
+
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
|
|
2653
3428
|
data-key=${node.item.key}
|
|
2654
3429
|
role="treeitem"
|
|
2655
3430
|
aria-selected=${selected}
|
|
2656
3431
|
aria-expanded=${isBranch ? isExpanded : nothing}
|
|
2657
3432
|
aria-hidden=${isLeaving ? `true` : nothing}
|
|
2658
3433
|
aria-level=${depth + 1}
|
|
3434
|
+
aria-setsize=${this._ariaSetInfo(node)?.setSize ?? nothing}
|
|
3435
|
+
aria-posinset=${this._ariaSetInfo(node)?.posInSet ?? nothing}
|
|
2659
3436
|
style=${`--depth: ${depth}`}
|
|
2660
3437
|
draggable=${dnd.draggable ? `true` : nothing}
|
|
2661
3438
|
part=${dnd.dropTarget ? `drop-target` : nothing}
|
|
@@ -2794,7 +3571,7 @@ var TreeController = class {
|
|
|
2794
3571
|
const treeNode = this.#nodeToTree.get(node);
|
|
2795
3572
|
if (!treeNode) return;
|
|
2796
3573
|
node.children = this.#childrenOf(treeNode);
|
|
2797
|
-
this.#element.
|
|
3574
|
+
this.#element.invalidateStructure();
|
|
2798
3575
|
}, { signal });
|
|
2799
3576
|
this.#element.addEventListener(`collapse`, (_e) => {}, { signal });
|
|
2800
3577
|
}
|
|
@@ -2816,7 +3593,10 @@ var TreeController = class {
|
|
|
2816
3593
|
if (!node) return false;
|
|
2817
3594
|
if (node.children === void 0) {
|
|
2818
3595
|
const mapped = this.#nodeToTree.get(node);
|
|
2819
|
-
if (mapped)
|
|
3596
|
+
if (mapped) {
|
|
3597
|
+
node.children = this.#childrenOf(mapped);
|
|
3598
|
+
this.#element.invalidateStructure();
|
|
3599
|
+
}
|
|
2820
3600
|
}
|
|
2821
3601
|
currentColumn = node.children ?? [];
|
|
2822
3602
|
}
|
|
@@ -2851,4 +3631,4 @@ var TreeController = class {
|
|
|
2851
3631
|
//#endregion
|
|
2852
3632
|
export { TreeDataModel as a, pruneDraggedDescendants as c, TreeDropController as i, TreeListElement as n, applyTreeDrop as o, TreeBaseElement as r, dropTargetFor as s, TreeController as t };
|
|
2853
3633
|
|
|
2854
|
-
//# sourceMappingURL=tree-
|
|
3634
|
+
//# sourceMappingURL=tree-DD8OaqsK.js.map
|