@ixfx/components 0.15.5 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.d.ts +917 -44
- package/bundle/index.js +702 -696
- package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
- package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
- package/dist/crumbs.d.ts +2 -115
- package/dist/crumbs.js +1 -1179
- package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
- package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
- package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
- package/dist/detail-list.d.ts +4 -4
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-JPgQSmLW.d.ts} +27 -4
- package/dist/flat-list.d.ts +2 -2
- package/dist/flat-list.js +123 -4
- package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
- package/dist/{grid-list-0e7snW6t.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
- package/dist/grid-list.d.ts +3 -3
- package/dist/grid-list.js +1 -1
- package/dist/incr-search.d.ts +1 -1
- package/dist/index-9ttcdwlL.d.ts +71 -0
- package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
- package/dist/index-CYRc6Dah.d.ts +475 -0
- package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
- package/dist/index.d.ts +334 -23
- package/dist/index.js +2247 -867
- package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
- package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
- package/dist/miller-BFRfn0ve.js +2237 -0
- package/dist/miller.d.ts +4 -437
- package/dist/miller.js +1 -1815
- package/dist/pending-BGidHppL.js +91 -0
- package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
- package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
- package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
- package/dist/split-layout.d.ts +1 -1
- package/dist/split-layout.js +1 -1
- package/dist/sunburst-tree.d.ts +2 -2
- package/dist/sunburst-tree.js +1 -1
- package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_VxOHlo.js} +298 -79
- package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
- package/dist/tree-CftHwtEb.js +146 -0
- package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
- package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
- package/dist/tree.d.ts +3 -3
- package/dist/tree.js +1 -1
- package/dist/types-LU1u0CB8.d.ts +87 -0
- package/dist/vertical-list-1V1Vg2O5.js +1993 -0
- package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
- package/dist/vertical-list.d.ts +3 -3
- package/dist/vertical-list.js +1 -1789
- package/dist/view-state-controller-BdyXWUUM.js +305 -0
- package/docs-user/README.md +2 -0
- package/docs-user/crumbs.md +27 -0
- package/docs-user/data-grid.md +66 -1
- package/docs-user/detail-list.md +54 -0
- package/docs-user/dock.md +54 -12
- package/docs-user/flat-list.md +27 -0
- package/docs-user/grid-list.md +64 -11
- package/docs-user/hyperbolic-tree.md +31 -4
- package/docs-user/icicle-tree.md +36 -0
- package/docs-user/index.json +12 -2
- package/docs-user/llms.txt +2 -0
- package/docs-user/miller.md +52 -21
- package/docs-user/multicol-list.md +50 -8
- package/docs-user/split-layout.md +45 -17
- package/docs-user/sunburst-tree.md +32 -2
- package/docs-user/tree.md +45 -6
- package/docs-user/treemap.md +32 -0
- package/docs-user/twosplit-layout.md +22 -0
- package/docs-user/user-catalog.md +1 -1
- package/docs-user/vertical-list.md +55 -7
- package/llms.txt +2 -0
- package/package.json +1 -1
package/dist/miller.js
CHANGED
|
@@ -1,1816 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrAHEj1q.js";
|
|
3
|
-
import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
|
|
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-JIhwB49y.js";
|
|
6
|
-
import "./interaction-E-uMGHkJ.js";
|
|
7
|
-
import { t as collectDataResult } from "./data-provider-35TyWclX.js";
|
|
8
|
-
import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
|
|
9
|
-
import { LitElement, css, html, nothing } from "lit";
|
|
10
|
-
import { property, state } from "lit/decorators.js";
|
|
11
|
-
import { createCommandRegistry } from "@clinth/ui-commands";
|
|
12
|
-
//#region src/miller/miller-base.ts
|
|
13
|
-
const EXPAND_DEBOUNCE_MS = 150;
|
|
14
|
-
let MillerBaseElement = class MillerBaseElement extends LitElement {
|
|
15
|
-
constructor(..._args) {
|
|
16
|
-
super(..._args);
|
|
17
|
-
this.headers = false;
|
|
18
|
-
this.stretchLastColumn = false;
|
|
19
|
-
this.interactionMode = `standard`;
|
|
20
|
-
this.selectionFilter = `leaf`;
|
|
21
|
-
this.exclusivity = `none`;
|
|
22
|
-
this._columns = [];
|
|
23
|
-
this._selectedByDepth = /* @__PURE__ */ new Map();
|
|
24
|
-
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
25
|
-
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
26
|
-
this.#columnWidths = /* @__PURE__ */ new Map();
|
|
27
|
-
this.#resizingDepth = -1;
|
|
28
|
-
this.#resizeStartX = 0;
|
|
29
|
-
this.#resizeStartWidth = 0;
|
|
30
|
-
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
31
|
-
labelSelector: `.label`,
|
|
32
|
-
highlightKey: `incr-search-highlight`
|
|
33
|
-
}) });
|
|
34
|
-
this.tickled = new TickledController(this, { getItems: () => {
|
|
35
|
-
const items = [];
|
|
36
|
-
for (let depth = 0; depth < this._columns.length; depth++) {
|
|
37
|
-
const col = this._columns[depth];
|
|
38
|
-
if (col) for (const node of col) items.push({ id: this.#buildTickledId(node, depth) });
|
|
39
|
-
}
|
|
40
|
-
return items;
|
|
41
|
-
} });
|
|
42
|
-
this.#selectOrigin = `user`;
|
|
43
|
-
this.#selectSilent = false;
|
|
44
|
-
this.#onKeyDown = (event) => {
|
|
45
|
-
if (this.search.isOpen) return;
|
|
46
|
-
const currentDepth = this.#getTickledDepthOrDefault();
|
|
47
|
-
const col = this._columns[currentDepth];
|
|
48
|
-
if (!col || col.length === 0) {
|
|
49
|
-
const depth0Col = this._columns[0];
|
|
50
|
-
if (depth0Col && depth0Col.length > 0) {
|
|
51
|
-
this.tickled.setTickled(this.#buildTickledId(depth0Col[0], 0));
|
|
52
|
-
this.requestUpdate();
|
|
53
|
-
}
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
switch (event.key) {
|
|
57
|
-
case `ArrowDown`:
|
|
58
|
-
event.preventDefault();
|
|
59
|
-
this.#moveCursor(col, currentDepth, event, (current) => Math.min(current + 1, col.length - 1));
|
|
60
|
-
break;
|
|
61
|
-
case `ArrowUp`:
|
|
62
|
-
event.preventDefault();
|
|
63
|
-
this.#moveCursor(col, currentDepth, event, (current) => Math.max(current - 1, 0));
|
|
64
|
-
break;
|
|
65
|
-
case `Home`:
|
|
66
|
-
event.preventDefault();
|
|
67
|
-
this.#moveCursor(col, currentDepth, event, () => 0, true);
|
|
68
|
-
break;
|
|
69
|
-
case `End`:
|
|
70
|
-
event.preventDefault();
|
|
71
|
-
this.#moveCursor(col, currentDepth, event, () => col.length - 1, true);
|
|
72
|
-
break;
|
|
73
|
-
case `PageUp`:
|
|
74
|
-
event.preventDefault();
|
|
75
|
-
this.#moveCursor(col, currentDepth, event, (current) => Math.max(current - this.#pageSize(currentDepth), 0), true);
|
|
76
|
-
break;
|
|
77
|
-
case `PageDown`:
|
|
78
|
-
event.preventDefault();
|
|
79
|
-
this.#moveCursor(col, currentDepth, event, (current) => Math.min(current + this.#pageSize(currentDepth), col.length - 1), true);
|
|
80
|
-
break;
|
|
81
|
-
case `ArrowRight`: {
|
|
82
|
-
event.preventDefault();
|
|
83
|
-
const tickled = this.#getTickledNodeAndDepth();
|
|
84
|
-
if (!tickled) {
|
|
85
|
-
if (col.length > 0) {
|
|
86
|
-
this.#setTickledNode(col[0], currentDepth);
|
|
87
|
-
this.requestUpdate();
|
|
88
|
-
}
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const { node, depth } = tickled;
|
|
92
|
-
if (this.interactionMode === `vscode` || this.interactionMode === `standard`) {
|
|
93
|
-
this._selectedByDepth.set(depth, node);
|
|
94
|
-
this.#expandNode(node, depth);
|
|
95
|
-
} else this.#onItemClick(node, depth);
|
|
96
|
-
if (this._columns.length > depth + 1) {
|
|
97
|
-
const nextCol = this._columns[depth + 1];
|
|
98
|
-
if (nextCol && nextCol.length > 0) this.#setTickledNode(nextCol[0], depth + 1);
|
|
99
|
-
}
|
|
100
|
-
this.requestUpdate();
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case `ArrowLeft`:
|
|
104
|
-
event.preventDefault();
|
|
105
|
-
if (currentDepth > 0) {
|
|
106
|
-
this.#abortPending();
|
|
107
|
-
this._columns = this._columns.slice(0, currentDepth);
|
|
108
|
-
const prevCol = this._columns[currentDepth - 1];
|
|
109
|
-
if (prevCol && prevCol.length > 0) {
|
|
110
|
-
const selectedNode = this._selectedByDepth.get(currentDepth - 1);
|
|
111
|
-
const targetNode = selectedNode && prevCol.includes(selectedNode) ? selectedNode : prevCol[0];
|
|
112
|
-
this.#setTickledNode(targetNode, currentDepth - 1);
|
|
113
|
-
}
|
|
114
|
-
this.requestUpdate();
|
|
115
|
-
}
|
|
116
|
-
break;
|
|
117
|
-
case `Enter`: {
|
|
118
|
-
event.preventDefault();
|
|
119
|
-
const tickled = this.#getTickledNodeAndDepth();
|
|
120
|
-
if (!tickled) return;
|
|
121
|
-
const { node, depth, index } = tickled;
|
|
122
|
-
if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
|
|
123
|
-
if (event.shiftKey && this.#anchor?.depth === depth) {
|
|
124
|
-
const from = Math.min(this.#anchor.index, index);
|
|
125
|
-
const to = Math.max(this.#anchor.index, index);
|
|
126
|
-
this._selectedKeys = new Set(col.slice(from, to + 1).map((n) => n.item.key));
|
|
127
|
-
} else {
|
|
128
|
-
this._selectedKeys.add(node.item.key);
|
|
129
|
-
this.#anchor = {
|
|
130
|
-
key: node.item.key,
|
|
131
|
-
depth,
|
|
132
|
-
index
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
} else this._selectedByDepth.set(depth, node);
|
|
136
|
-
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
137
|
-
detail: {
|
|
138
|
-
node,
|
|
139
|
-
depth
|
|
140
|
-
},
|
|
141
|
-
bubbles: true,
|
|
142
|
-
composed: true
|
|
143
|
-
}));
|
|
144
|
-
this.requestUpdate();
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case `Escape`:
|
|
148
|
-
event.preventDefault();
|
|
149
|
-
this.#abortPending();
|
|
150
|
-
if (this._columns.length > 1) {
|
|
151
|
-
this._columns = this._columns.slice(0, -1);
|
|
152
|
-
const newDepth = Math.max(0, currentDepth - 1);
|
|
153
|
-
const prevCol = this._columns[newDepth];
|
|
154
|
-
if (prevCol && prevCol.length > 0) {
|
|
155
|
-
const selectedNode = this._selectedByDepth.get(newDepth);
|
|
156
|
-
const targetNode = selectedNode && prevCol.includes(selectedNode) ? selectedNode : prevCol[0];
|
|
157
|
-
this.#setTickledNode(targetNode, newDepth);
|
|
158
|
-
}
|
|
159
|
-
this.requestUpdate();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
this.#onResizeStart = (event, depth) => {
|
|
164
|
-
event.preventDefault();
|
|
165
|
-
event.stopPropagation();
|
|
166
|
-
this.#resizingDepth = depth;
|
|
167
|
-
this.#resizeStartX = event.clientX;
|
|
168
|
-
const el = this.shadowRoot?.querySelectorAll(`.column`)[depth];
|
|
169
|
-
this.#resizeStartWidth = el ? el.getBoundingClientRect().width : 200;
|
|
170
|
-
window.addEventListener(`pointermove`, this.#onResizeMove);
|
|
171
|
-
window.addEventListener(`pointerup`, this.#onResizeEnd);
|
|
172
|
-
};
|
|
173
|
-
this.#resizeRaf = null;
|
|
174
|
-
this.#resizePendingWidth = 0;
|
|
175
|
-
this.#onResizeMove = (event) => {
|
|
176
|
-
if (this.#resizingDepth < 0) return;
|
|
177
|
-
this.#resizePendingWidth = Math.max(32, this.#resizeStartWidth + (event.clientX - this.#resizeStartX));
|
|
178
|
-
this.#resizeRaf ??= requestAnimationFrame(this.#applyResizeWidth);
|
|
179
|
-
};
|
|
180
|
-
this.#applyResizeWidth = () => {
|
|
181
|
-
this.#resizeRaf = null;
|
|
182
|
-
if (this.#resizingDepth < 0) return;
|
|
183
|
-
(this.shadowRoot?.querySelectorAll(`.column`)[this.#resizingDepth])?.setAttribute(`style`, this.#explicitWidthStyle(this.#resizePendingWidth));
|
|
184
|
-
};
|
|
185
|
-
this.#onResizeEnd = () => {
|
|
186
|
-
if (this.#resizeRaf !== null) {
|
|
187
|
-
cancelAnimationFrame(this.#resizeRaf);
|
|
188
|
-
this.#resizeRaf = null;
|
|
189
|
-
}
|
|
190
|
-
if (this.#resizingDepth >= 0 && this.#resizePendingWidth > 0) {
|
|
191
|
-
this.#columnWidths.set(this.#resizingDepth, this.#resizePendingWidth);
|
|
192
|
-
this.requestUpdate();
|
|
193
|
-
}
|
|
194
|
-
this.#resizePendingWidth = 0;
|
|
195
|
-
this.#resizingDepth = -1;
|
|
196
|
-
window.removeEventListener(`pointermove`, this.#onResizeMove);
|
|
197
|
-
window.removeEventListener(`pointerup`, this.#onResizeEnd);
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
#lastSelectedNode;
|
|
201
|
-
#anchor;
|
|
202
|
-
#columnWidths;
|
|
203
|
-
#pendingNode;
|
|
204
|
-
#pendingController;
|
|
205
|
-
#expandTimer;
|
|
206
|
-
#resizingDepth;
|
|
207
|
-
#resizeStartX;
|
|
208
|
-
#resizeStartWidth;
|
|
209
|
-
#modelUnsubscribe;
|
|
210
|
-
#searchRegistry;
|
|
211
|
-
#unregisterSearchCommands;
|
|
212
|
-
/** Command registry carrying this component's `list.search.*` commands. */
|
|
213
|
-
get registry() {
|
|
214
|
-
return this.#searchRegistry;
|
|
215
|
-
}
|
|
216
|
-
#effectiveSearchModes() {
|
|
217
|
-
const raw = this.searchModes?.trim();
|
|
218
|
-
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
219
|
-
const requested = new Set(raw.split(/\s+/));
|
|
220
|
-
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
221
|
-
}
|
|
222
|
-
#syncSearchCommands() {
|
|
223
|
-
if (!this.#searchRegistry) return;
|
|
224
|
-
this.search.setModes(this.#effectiveSearchModes());
|
|
225
|
-
this.#unregisterSearchCommands?.();
|
|
226
|
-
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
227
|
-
}
|
|
228
|
-
#buildTickledId(node, depth) {
|
|
229
|
-
return `${depth}:${node.item.key}`;
|
|
230
|
-
}
|
|
231
|
-
/** The node column 0 renders the children of: `viewRoot` when rebased, else `root`. */
|
|
232
|
-
get #displayRoot() {
|
|
233
|
-
return this.viewRoot ?? this.root;
|
|
234
|
-
}
|
|
235
|
-
#getTickledDepth() {
|
|
236
|
-
const id = this.tickled.tickledId;
|
|
237
|
-
if (!id) return void 0;
|
|
238
|
-
const colonIndex = id.indexOf(`:`);
|
|
239
|
-
if (colonIndex < 0) return void 0;
|
|
240
|
-
return Number(id.substring(0, colonIndex));
|
|
241
|
-
}
|
|
242
|
-
updated(changedProps) {
|
|
243
|
-
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
244
|
-
if (changedProps.has(`root`) && this.root) {
|
|
245
|
-
this.#abortPending();
|
|
246
|
-
if (this.viewRoot !== void 0) this.viewRoot = void 0;
|
|
247
|
-
this._selectedByDepth.clear();
|
|
248
|
-
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
249
|
-
this._selectedNodes.clear();
|
|
250
|
-
this.#lastSelectedNode = void 0;
|
|
251
|
-
this.#anchor = void 0;
|
|
252
|
-
this.tickled.clearTickled();
|
|
253
|
-
const display = this.#displayRoot;
|
|
254
|
-
this._columns = display?.children ? [display.children] : [];
|
|
255
|
-
}
|
|
256
|
-
if (changedProps.has(`viewRoot`)) this.#applyViewRoot();
|
|
257
|
-
if (changedProps.has(`model`)) {
|
|
258
|
-
this.#modelUnsubscribe?.();
|
|
259
|
-
this.#modelUnsubscribe = void 0;
|
|
260
|
-
const model = this.model;
|
|
261
|
-
if (model) {
|
|
262
|
-
this.root = model.root;
|
|
263
|
-
this.#modelUnsubscribe = model._subscribe((event) => {
|
|
264
|
-
if (event.type === `root`) {
|
|
265
|
-
this.root = event.root;
|
|
266
|
-
this.requestUpdate();
|
|
267
|
-
} else if (event.type === `invalidate`) {
|
|
268
|
-
for (let depth = 0; depth < this._columns.length - 1; depth++) if (this._columns[depth]?.some((n) => n.item.key === event.key)) {
|
|
269
|
-
this._columns = this._columns.slice(0, depth + 1);
|
|
270
|
-
this.requestUpdate();
|
|
271
|
-
break;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (this.#pendingNode?.children !== void 0 && this._loadingDepth !== void 0) {
|
|
278
|
-
const loadingDepth = this._loadingDepth;
|
|
279
|
-
const resolvedChildren = this.#pendingNode.children;
|
|
280
|
-
this.#pendingNode = void 0;
|
|
281
|
-
this.#pendingController = void 0;
|
|
282
|
-
this._loadingDepth = void 0;
|
|
283
|
-
const cols = this._columns.slice(0, loadingDepth);
|
|
284
|
-
if (resolvedChildren.length > 0) cols.push(resolvedChildren);
|
|
285
|
-
this._columns = cols;
|
|
286
|
-
}
|
|
287
|
-
const prevColumns = changedProps.get(`_columns`);
|
|
288
|
-
const columnAdded = changedProps.has(`_columns`) && (prevColumns?.length ?? 0) < this._columns.length;
|
|
289
|
-
const loadingAppeared = changedProps.has(`_loadingDepth`) && this._loadingDepth !== void 0;
|
|
290
|
-
if (columnAdded || loadingAppeared) {
|
|
291
|
-
const container = this.shadowRoot?.querySelector(`#container`);
|
|
292
|
-
container?.scrollTo({
|
|
293
|
-
left: container.scrollWidth,
|
|
294
|
-
behavior: `smooth`
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
connectedCallback() {
|
|
299
|
-
super.connectedCallback();
|
|
300
|
-
this.#searchRegistry ??= createCommandRegistry();
|
|
301
|
-
this.#syncSearchCommands();
|
|
302
|
-
}
|
|
303
|
-
disconnectedCallback() {
|
|
304
|
-
super.disconnectedCallback();
|
|
305
|
-
this.#modelUnsubscribe?.();
|
|
306
|
-
this.#modelUnsubscribe = void 0;
|
|
307
|
-
this.#unregisterSearchCommands?.();
|
|
308
|
-
this.#unregisterSearchCommands = void 0;
|
|
309
|
-
this.#searchRegistry = void 0;
|
|
310
|
-
}
|
|
311
|
-
getConfig() {
|
|
312
|
-
const columnWidths = {};
|
|
313
|
-
for (const [depth, width] of this.#columnWidths) columnWidths[depth] = width;
|
|
314
|
-
return { columnWidths };
|
|
315
|
-
}
|
|
316
|
-
setConfig(config) {
|
|
317
|
-
this.#columnWidths.clear();
|
|
318
|
-
for (const [depth, width] of Object.entries(config.columnWidths)) this.#columnWidths.set(Number(depth), width);
|
|
319
|
-
this.requestUpdate();
|
|
320
|
-
}
|
|
321
|
-
/** Read current selection as an immutable set of item keys. */
|
|
322
|
-
get selectedKeys() {
|
|
323
|
-
return this._selectedKeys;
|
|
324
|
-
}
|
|
325
|
-
/** Replace entire selection and trigger a re-render. */
|
|
326
|
-
set selectedKeys(keys) {
|
|
327
|
-
this._selectedKeys = new Set(keys);
|
|
328
|
-
this.requestUpdate();
|
|
329
|
-
}
|
|
330
|
-
/** The most recently selected node. */
|
|
331
|
-
get selectedNode() {
|
|
332
|
-
return this.#lastSelectedNode;
|
|
333
|
-
}
|
|
334
|
-
set selectedNode(node) {
|
|
335
|
-
this.#lastSelectedNode = node;
|
|
336
|
-
if (node) {
|
|
337
|
-
if (this.interactionMode === `implicit` || this.interactionMode === `standard`) this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
|
|
338
|
-
}
|
|
339
|
-
this.requestUpdate();
|
|
340
|
-
}
|
|
341
|
-
/** Full selection set (implements TreeComponent interface). */
|
|
342
|
-
get selectedNodes() {
|
|
343
|
-
return this._selectedNodes;
|
|
344
|
-
}
|
|
345
|
-
/** Add `node` to the selection (respects selectionFilter). Dispatches `select` event. */
|
|
346
|
-
select(node) {
|
|
347
|
-
if (!this.#canSelect(node)) return;
|
|
348
|
-
const prev = new Set(this._selectedNodes);
|
|
349
|
-
this._selectedNodes.add(node);
|
|
350
|
-
this._selectedKeys.add(node.item.key);
|
|
351
|
-
this.#lastSelectedNode = node;
|
|
352
|
-
this.#dispatchSelectEvent(prev);
|
|
353
|
-
this.requestUpdate();
|
|
354
|
-
}
|
|
355
|
-
/** Remove `node` from the selection. Dispatches `select` event. */
|
|
356
|
-
deselect(node) {
|
|
357
|
-
if (!this._selectedNodes.has(node)) return;
|
|
358
|
-
const prev = new Set(this._selectedNodes);
|
|
359
|
-
this._selectedNodes.delete(node);
|
|
360
|
-
this._selectedKeys.delete(node.item.key);
|
|
361
|
-
if (this.#lastSelectedNode === node) this.#lastSelectedNode = [...this._selectedNodes].at(-1);
|
|
362
|
-
this.#dispatchSelectEvent(prev);
|
|
363
|
-
this.requestUpdate();
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Clear all selection and trigger a re-render.
|
|
367
|
-
*/
|
|
368
|
-
clearSelection() {
|
|
369
|
-
const prev = new Set(this._selectedNodes);
|
|
370
|
-
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
371
|
-
this._selectedNodes.clear();
|
|
372
|
-
this.#lastSelectedNode = void 0;
|
|
373
|
-
if (prev.size > 0) this.#dispatchSelectEvent(prev);
|
|
374
|
-
this.requestUpdate();
|
|
375
|
-
}
|
|
376
|
-
/** Not implemented on miller-list. */
|
|
377
|
-
selectAll() {}
|
|
378
|
-
/** Not implemented on miller-list. */
|
|
379
|
-
deselectAll() {}
|
|
380
|
-
/**
|
|
381
|
-
* Select or deselect a single node by key.
|
|
382
|
-
* In `checked` interactionMode this also recursively selects/deselects all loaded descendants.
|
|
383
|
-
*/
|
|
384
|
-
setSelected(node, selected) {
|
|
385
|
-
if (this.interactionMode === `checked`) this.#applyCheckedToggle(node, selected);
|
|
386
|
-
else if (selected) {
|
|
387
|
-
this._selectedKeys.add(node.item.key);
|
|
388
|
-
this._selectedNodes.add(node);
|
|
389
|
-
} else {
|
|
390
|
-
this._selectedKeys.delete(node.item.key);
|
|
391
|
-
this._selectedNodes.delete(node);
|
|
392
|
-
}
|
|
393
|
-
this.requestUpdate();
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Return all MillerNodes whose key is in `_selectedKeys`, searching only
|
|
397
|
-
* the currently visible columns.
|
|
398
|
-
*/
|
|
399
|
-
getSelected() {
|
|
400
|
-
const results = [];
|
|
401
|
-
for (const col of this._columns) for (const node of col) if (this._selectedKeys.has(node.item.key)) results.push(node);
|
|
402
|
-
return results;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Programmatically navigate to a node by key path, relative to the current
|
|
406
|
-
* view root (rebased or full tree).
|
|
407
|
-
*/
|
|
408
|
-
navigateTo(keys, options = {}) {
|
|
409
|
-
const display = this.#displayRoot;
|
|
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) {
|
|
444
|
-
this.#abortPending();
|
|
445
|
-
this._selectedByDepth.clear();
|
|
446
|
-
this.tickled.clearTickled();
|
|
447
|
-
const newColumns = [rootColumn];
|
|
448
|
-
let currentColumn = rootColumn;
|
|
449
|
-
let success = true;
|
|
450
|
-
let lastNode;
|
|
451
|
-
let lastDepth = 0;
|
|
452
|
-
for (let depth = 0; depth < keys.length; depth++) {
|
|
453
|
-
const idx = currentColumn.findIndex((n) => n.item.key === keys[depth]);
|
|
454
|
-
if (idx < 0) {
|
|
455
|
-
success = false;
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
const node = currentColumn[idx];
|
|
459
|
-
this._selectedByDepth.set(depth, node);
|
|
460
|
-
lastNode = node;
|
|
461
|
-
lastDepth = depth;
|
|
462
|
-
if (node.item.isLeaf || !node.children || node.children.length === 0) {
|
|
463
|
-
if (depth < keys.length - 1) success = false;
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
newColumns.push(node.children);
|
|
467
|
-
currentColumn = node.children;
|
|
468
|
-
}
|
|
469
|
-
this._columns = newColumns;
|
|
470
|
-
if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
|
|
471
|
-
if (this.interactionMode !== `implicit` && this.interactionMode !== `manual`) {
|
|
472
|
-
const leafKey = keys.at(-1);
|
|
473
|
-
if (leafKey) this._selectedKeys = /* @__PURE__ */ new Set([leafKey]);
|
|
474
|
-
}
|
|
475
|
-
this.requestUpdate();
|
|
476
|
-
this.#scrollNavigatedItemIntoView(reveal === `center-if-hidden` ? `auto` : `smooth`, reveal);
|
|
477
|
-
return success;
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Programmatically navigate to the first node (breadth-first) matching a predicate.
|
|
481
|
-
*/
|
|
482
|
-
navigateToNode(predicate) {
|
|
483
|
-
const display = this.#displayRoot;
|
|
484
|
-
if (!display?.children) return false;
|
|
485
|
-
const keyPath = this.#findNodePath(display.children, predicate);
|
|
486
|
-
if (!keyPath) return false;
|
|
487
|
-
return this.navigateTo(keyPath);
|
|
488
|
-
}
|
|
489
|
-
/** Open the columns down to `key` and move the cursor to it, without changing the selection (search "navigate" mode). */
|
|
490
|
-
revealKey(key) {
|
|
491
|
-
const display = this.#displayRoot;
|
|
492
|
-
if (!display?.children) return false;
|
|
493
|
-
const path = this.#findNodePath(display.children, (n) => n.item.key === key);
|
|
494
|
-
if (!path) return false;
|
|
495
|
-
const columns = [display.children];
|
|
496
|
-
let current = display.children;
|
|
497
|
-
let lastNode;
|
|
498
|
-
let lastDepth = 0;
|
|
499
|
-
for (let depth = 0; depth < path.length; depth++) {
|
|
500
|
-
const node = current.find((n) => n.item.key === path[depth]);
|
|
501
|
-
if (!node) return false;
|
|
502
|
-
lastNode = node;
|
|
503
|
-
lastDepth = depth;
|
|
504
|
-
if (node.children && node.children.length > 0) {
|
|
505
|
-
columns.push(node.children);
|
|
506
|
-
current = node.children;
|
|
507
|
-
} else break;
|
|
508
|
-
}
|
|
509
|
-
this._columns = columns;
|
|
510
|
-
if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
|
|
511
|
-
this.requestUpdate();
|
|
512
|
-
this.#scrollNavigatedItemIntoView();
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
/** Replace the selection with `nodes` (search "select" mode). */
|
|
516
|
-
selectMany(nodes) {
|
|
517
|
-
const arr = this.selectionFilter === `none` ? [] : [...nodes];
|
|
518
|
-
const prev = new Set(this._selectedNodes);
|
|
519
|
-
this._selectedNodes = new Set(arr);
|
|
520
|
-
this._selectedKeys = new Set(arr.map((n) => n.item.key));
|
|
521
|
-
this.#lastSelectedNode = arr.at(-1);
|
|
522
|
-
this.#dispatchSelectEvent(prev);
|
|
523
|
-
this.requestUpdate();
|
|
524
|
-
}
|
|
525
|
-
getNodes() {
|
|
526
|
-
const display = this.#displayRoot;
|
|
527
|
-
if (!display?.children) return [];
|
|
528
|
-
const result = [];
|
|
529
|
-
const visit = (node, depth) => {
|
|
530
|
-
result.push({
|
|
531
|
-
node,
|
|
532
|
-
depth
|
|
533
|
-
});
|
|
534
|
-
if (node.children) for (const child of node.children) visit(child, depth + 1);
|
|
535
|
-
};
|
|
536
|
-
for (const child of display.children) visit(child, 0);
|
|
537
|
-
return result;
|
|
538
|
-
}
|
|
539
|
-
#findNodePath(nodes, predicate, prefix = []) {
|
|
540
|
-
for (const node of nodes) {
|
|
541
|
-
const path = [...prefix, node.item.key];
|
|
542
|
-
if (predicate(node)) return path;
|
|
543
|
-
if (node.children?.length) {
|
|
544
|
-
const result = this.#findNodePath(node.children, predicate, path);
|
|
545
|
-
if (result) return result;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Re-render the columns for the current `#displayRoot`, clearing selection.
|
|
551
|
-
* Runs on every `viewRoot` change.
|
|
552
|
-
*/
|
|
553
|
-
#applyViewRoot() {
|
|
554
|
-
this.#abortPending();
|
|
555
|
-
const prev = new Set(this._selectedNodes);
|
|
556
|
-
this._selectedByDepth.clear();
|
|
557
|
-
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
558
|
-
this._selectedNodes.clear();
|
|
559
|
-
this.#lastSelectedNode = void 0;
|
|
560
|
-
this.#anchor = void 0;
|
|
561
|
-
this.tickled.clearTickled();
|
|
562
|
-
const display = this.#displayRoot;
|
|
563
|
-
if (display?.children !== void 0) {
|
|
564
|
-
this._columns = display.children ? [display.children] : [];
|
|
565
|
-
this._loadingDepth = void 0;
|
|
566
|
-
} else if (display) {
|
|
567
|
-
this._columns = [];
|
|
568
|
-
this.#loadUnloadedChildren(display, -1, false);
|
|
569
|
-
} else {
|
|
570
|
-
this._columns = [];
|
|
571
|
-
this._loadingDepth = void 0;
|
|
572
|
-
}
|
|
573
|
-
if (prev.size > 0) this.#dispatchSelectEvent(prev);
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Rebase the view to show `target`'s children in column 0 (Finder column-view
|
|
577
|
-
* behavior on double-click), keeping the full tree in `root`.
|
|
578
|
-
*
|
|
579
|
-
* - Pass a `MillerNode` (used as-is) or a key (looked up in the full tree).
|
|
580
|
-
* - Pass `undefined` to restore the full-tree view.
|
|
581
|
-
* - Rebasing to the true root is equivalent to restoring the full view.
|
|
582
|
-
* - Leaves are refused (returns `false`).
|
|
583
|
-
*
|
|
584
|
-
* Selection is cleared; all depths, headers, and key paths are relative to
|
|
585
|
-
* the new view root. Fires a `rebase` event when the view actually changes.
|
|
586
|
-
* Returns `false` when nothing changed (unknown key, leaf, or same view).
|
|
587
|
-
*/
|
|
588
|
-
rebaseTo(target) {
|
|
589
|
-
if (target === void 0) return this.#clearViewRoot();
|
|
590
|
-
const node = typeof target === `string` ? this.#findNodeByKey(target) : target;
|
|
591
|
-
if (!node || node.item.isLeaf === true) return false;
|
|
592
|
-
if (node === this.viewRoot || node.item.key === this.viewRoot?.item.key) return false;
|
|
593
|
-
if (node === this.root || !this.viewRoot && node.item.key === this.root?.item.key) return this.#clearViewRoot();
|
|
594
|
-
this.viewRoot = node;
|
|
595
|
-
this.#dispatchRebaseEvent();
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Rebase one level up from the current view root (no-op at the full tree).
|
|
600
|
-
* Fires a `rebase` event when the view actually changes.
|
|
601
|
-
*/
|
|
602
|
-
rebaseUp() {
|
|
603
|
-
if (!this.viewRoot) return false;
|
|
604
|
-
const parent = this.#findParentOf(this.viewRoot);
|
|
605
|
-
if (!parent) return false;
|
|
606
|
-
if (parent === this.root) return this.#clearViewRoot();
|
|
607
|
-
this.viewRoot = parent;
|
|
608
|
-
this.#dispatchRebaseEvent();
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
#clearViewRoot() {
|
|
612
|
-
if (this.viewRoot === void 0) return false;
|
|
613
|
-
this.viewRoot = void 0;
|
|
614
|
-
this.#dispatchRebaseEvent();
|
|
615
|
-
return true;
|
|
616
|
-
}
|
|
617
|
-
#dispatchRebaseEvent() {
|
|
618
|
-
const viewRoot = this.viewRoot;
|
|
619
|
-
const detail = {
|
|
620
|
-
viewRoot,
|
|
621
|
-
path: viewRoot ? this.#pathToRoot(viewRoot) : []
|
|
622
|
-
};
|
|
623
|
-
this.dispatchEvent(new CustomEvent(`rebase`, {
|
|
624
|
-
detail,
|
|
625
|
-
bubbles: true,
|
|
626
|
-
composed: true
|
|
627
|
-
}));
|
|
628
|
-
}
|
|
629
|
-
#findNodeByKey(key) {
|
|
630
|
-
if (!this.root) return void 0;
|
|
631
|
-
if (this.root.item.key === key) return this.root;
|
|
632
|
-
return this.#findNodeIn(this.root.children ?? [], key);
|
|
633
|
-
}
|
|
634
|
-
#findNodeIn(nodes, key) {
|
|
635
|
-
for (const node of nodes) {
|
|
636
|
-
if (node.item.key === key) return node;
|
|
637
|
-
if (node.children) {
|
|
638
|
-
const found = this.#findNodeIn(node.children, key);
|
|
639
|
-
if (found) return found;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
/** Find the parent of `target` (by reference, falling back to key) in the full tree. */
|
|
644
|
-
#findParentOf(target) {
|
|
645
|
-
if (!this.root || target === this.root) return void 0;
|
|
646
|
-
return this.#findParentIn(this.root, target);
|
|
647
|
-
}
|
|
648
|
-
#findParentIn(node, target) {
|
|
649
|
-
if (!node.children) return void 0;
|
|
650
|
-
for (const child of node.children) {
|
|
651
|
-
if (child === target || child.item.key === target.item.key) return node;
|
|
652
|
-
const found = this.#findParentIn(child, target);
|
|
653
|
-
if (found) return found;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
/** Ancestry chain from the true root down to and including `target`. */
|
|
657
|
-
#pathToRoot(target) {
|
|
658
|
-
const chain = [target];
|
|
659
|
-
let current = this.#findParentOf(target);
|
|
660
|
-
while (current) {
|
|
661
|
-
chain.unshift(current);
|
|
662
|
-
if (current === this.root) break;
|
|
663
|
-
current = this.#findParentOf(current);
|
|
664
|
-
}
|
|
665
|
-
return chain;
|
|
666
|
-
}
|
|
667
|
-
renderColumnContent(nodes, depth) {
|
|
668
|
-
console.warn(`ixfx-miller-base: renderColumnContent not overridden. depth=${depth}, nodes=${nodes.length}`);
|
|
669
|
-
return nothing;
|
|
670
|
-
}
|
|
671
|
-
_handleItemClick(node, depth, event) {
|
|
672
|
-
this.#onItemClick(node, depth, event);
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Called by subclasses (in `checked` interactionMode) when a checkbox is clicked.
|
|
676
|
-
*/
|
|
677
|
-
_handleCheckboxClick(event, node, _depth) {
|
|
678
|
-
event.stopPropagation();
|
|
679
|
-
this.#onCheckboxClick(node);
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Called by subclasses when an item is double-clicked.
|
|
683
|
-
*/
|
|
684
|
-
_handleItemDblClick(node, depth, _event) {
|
|
685
|
-
this.dispatchEvent(new CustomEvent(`item-dblclick`, {
|
|
686
|
-
detail: {
|
|
687
|
-
node,
|
|
688
|
-
depth
|
|
689
|
-
},
|
|
690
|
-
bubbles: true,
|
|
691
|
-
composed: true
|
|
692
|
-
}));
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Called by subclasses when an item is right-clicked.
|
|
696
|
-
*/
|
|
697
|
-
_handleItemRightClick(node, depth, event) {
|
|
698
|
-
this.dispatchEvent(new CustomEvent(`item-rightclick`, {
|
|
699
|
-
detail: {
|
|
700
|
-
node,
|
|
701
|
-
depth,
|
|
702
|
-
mouseEvent: event
|
|
703
|
-
},
|
|
704
|
-
bubbles: true,
|
|
705
|
-
composed: true
|
|
706
|
-
}));
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Called by subclasses when a column header is right-clicked.
|
|
710
|
-
*/
|
|
711
|
-
_handleHeaderRightClick(event, depth) {
|
|
712
|
-
const label = this.#getColumnHeader(depth);
|
|
713
|
-
const path = this.#buildPath(depth);
|
|
714
|
-
this.dispatchEvent(new CustomEvent(`header-rightclick`, {
|
|
715
|
-
detail: {
|
|
716
|
-
depth,
|
|
717
|
-
label,
|
|
718
|
-
path,
|
|
719
|
-
mouseEvent: event
|
|
720
|
-
},
|
|
721
|
-
bubbles: true,
|
|
722
|
-
composed: true
|
|
723
|
-
}));
|
|
724
|
-
}
|
|
725
|
-
_isSelected(node, depth) {
|
|
726
|
-
if (this.interactionMode === `implicit`) return this._selectedByDepth.get(depth) === node;
|
|
727
|
-
return this._selectedKeys.has(node.item.key);
|
|
728
|
-
}
|
|
729
|
-
#isAncestor(node, depth) {
|
|
730
|
-
const nextCol = this._columns[depth + 1];
|
|
731
|
-
return nextCol !== void 0 && node.children === nextCol;
|
|
732
|
-
}
|
|
733
|
-
_isOpen(node, depth) {
|
|
734
|
-
return this.#isAncestor(node, depth);
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Return the display state for an item identified by key at a given depth.
|
|
738
|
-
*/
|
|
739
|
-
getItemState(key, depth) {
|
|
740
|
-
const node = this._columns[depth]?.find((n) => n.item.key === key);
|
|
741
|
-
if (!node) return void 0;
|
|
742
|
-
return {
|
|
743
|
-
selected: this._isSelected(node, depth),
|
|
744
|
-
open: this.#isAncestor(node, depth),
|
|
745
|
-
focused: this._isFocused(node, depth),
|
|
746
|
-
isLeaf: node.item.isLeaf ?? false
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
#canSelect(node) {
|
|
750
|
-
if (this.selectionFilter === `none`) return false;
|
|
751
|
-
if (this.selectionFilter === `leaf`) return node.item.isLeaf !== false;
|
|
752
|
-
if (this.selectionFilter === `branch`) return node.item.isLeaf === false || node.children !== void 0 && node.children.length > 0;
|
|
753
|
-
return true;
|
|
754
|
-
}
|
|
755
|
-
#dispatchSelectEvent(previous) {
|
|
756
|
-
if (this.#selectSilent) return;
|
|
757
|
-
const detail = {
|
|
758
|
-
selected: new Set(this._selectedNodes),
|
|
759
|
-
previous,
|
|
760
|
-
origin: this.#selectOrigin
|
|
761
|
-
};
|
|
762
|
-
this.dispatchEvent(new CustomEvent(`select`, {
|
|
763
|
-
detail,
|
|
764
|
-
bubbles: true,
|
|
765
|
-
composed: true
|
|
766
|
-
}));
|
|
767
|
-
}
|
|
768
|
-
#abortPending() {
|
|
769
|
-
if (this.#expandTimer !== void 0) {
|
|
770
|
-
clearTimeout(this.#expandTimer);
|
|
771
|
-
this.#expandTimer = void 0;
|
|
772
|
-
}
|
|
773
|
-
if (this.#pendingController) {
|
|
774
|
-
const node = this.#pendingNode;
|
|
775
|
-
const depth = this._loadingDepth;
|
|
776
|
-
this.#pendingController.abort();
|
|
777
|
-
this.#pendingController = void 0;
|
|
778
|
-
this.#pendingNode = void 0;
|
|
779
|
-
this._loadingDepth = void 0;
|
|
780
|
-
if (node && depth !== void 0) this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
781
|
-
detail: {
|
|
782
|
-
node,
|
|
783
|
-
depth
|
|
784
|
-
},
|
|
785
|
-
bubbles: true,
|
|
786
|
-
composed: true
|
|
787
|
-
}));
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Build the ancestry path for a given column depth.
|
|
792
|
-
*/
|
|
793
|
-
#buildPath(depth) {
|
|
794
|
-
const path = [];
|
|
795
|
-
if (this.#displayRoot) path.push(this.#displayRoot);
|
|
796
|
-
for (let d = 0; d < depth; d++) {
|
|
797
|
-
const sel = this._selectedByDepth.get(d);
|
|
798
|
-
if (sel) path.push(sel);
|
|
799
|
-
}
|
|
800
|
-
return path;
|
|
801
|
-
}
|
|
802
|
-
#onHeaderClick(depth) {
|
|
803
|
-
const detail = {
|
|
804
|
-
depth,
|
|
805
|
-
label: this.#getColumnHeader(depth),
|
|
806
|
-
path: this.#buildPath(depth)
|
|
807
|
-
};
|
|
808
|
-
this.dispatchEvent(new CustomEvent(`header-click`, {
|
|
809
|
-
detail,
|
|
810
|
-
bubbles: true,
|
|
811
|
-
composed: true
|
|
812
|
-
}));
|
|
813
|
-
}
|
|
814
|
-
#onItemClick(node, depth, event) {
|
|
815
|
-
if (this._loadingDepth !== void 0 && this._loadingDepth > depth) this.#abortPending();
|
|
816
|
-
this.tickled.setTickled(this.#buildTickledId(node, depth));
|
|
817
|
-
this.dispatchEvent(new CustomEvent(`item-click`, {
|
|
818
|
-
detail: {
|
|
819
|
-
node,
|
|
820
|
-
depth
|
|
821
|
-
},
|
|
822
|
-
bubbles: true,
|
|
823
|
-
composed: true
|
|
824
|
-
}));
|
|
825
|
-
switch (this.interactionMode) {
|
|
826
|
-
case `implicit`:
|
|
827
|
-
this.#handleImplicitClick(node, depth);
|
|
828
|
-
break;
|
|
829
|
-
case `standard`:
|
|
830
|
-
this.#handleStandardClick(node, depth, event);
|
|
831
|
-
break;
|
|
832
|
-
case `vscode`:
|
|
833
|
-
this.#handleStandardClick(node, depth, event);
|
|
834
|
-
break;
|
|
835
|
-
case `manual`: break;
|
|
836
|
-
case `checked`: this.#expandNode(node, depth, true);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
#handleImplicitClick(node, depth) {
|
|
840
|
-
const previous = new Set(this._selectedNodes);
|
|
841
|
-
this._selectedByDepth.set(depth, node);
|
|
842
|
-
this.#lastSelectedNode = node;
|
|
843
|
-
this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
|
|
844
|
-
this._selectedNodes.clear();
|
|
845
|
-
this._selectedNodes.add(node);
|
|
846
|
-
this.#expandNode(node, depth, true);
|
|
847
|
-
if (!previous.has(node)) this.#dispatchSelectEvent(previous);
|
|
848
|
-
}
|
|
849
|
-
#handleStandardClick(node, depth, event) {
|
|
850
|
-
const col = this._columns[depth];
|
|
851
|
-
const index = col ? col.indexOf(node) : -1;
|
|
852
|
-
const isMeta = event ? event.metaKey || event.ctrlKey : false;
|
|
853
|
-
const isShift = event ? event.shiftKey : false;
|
|
854
|
-
if (isMeta) {
|
|
855
|
-
if (this._selectedKeys.has(node.item.key)) {
|
|
856
|
-
this._selectedKeys.delete(node.item.key);
|
|
857
|
-
this._selectedNodes.delete(node);
|
|
858
|
-
} else {
|
|
859
|
-
this._selectedKeys.add(node.item.key);
|
|
860
|
-
this._selectedNodes.add(node);
|
|
861
|
-
}
|
|
862
|
-
this.#anchor = index >= 0 ? {
|
|
863
|
-
key: node.item.key,
|
|
864
|
-
depth,
|
|
865
|
-
index
|
|
866
|
-
} : this.#anchor;
|
|
867
|
-
this.requestUpdate();
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
if (isShift && this.#anchor && this.#anchor.depth === depth && col) {
|
|
871
|
-
const from = Math.min(this.#anchor.index, index);
|
|
872
|
-
const to = Math.max(this.#anchor.index, index);
|
|
873
|
-
const rangeNodes = col.slice(from, to + 1);
|
|
874
|
-
this._selectedKeys = new Set(rangeNodes.map((n) => n.item.key));
|
|
875
|
-
this._selectedNodes = new Set(rangeNodes);
|
|
876
|
-
this.requestUpdate();
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
const previous = new Set(this._selectedNodes);
|
|
880
|
-
this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
|
|
881
|
-
this._selectedNodes = /* @__PURE__ */ new Set([node]);
|
|
882
|
-
this.#anchor = index >= 0 ? {
|
|
883
|
-
key: node.item.key,
|
|
884
|
-
depth,
|
|
885
|
-
index
|
|
886
|
-
} : void 0;
|
|
887
|
-
this._selectedByDepth.set(depth, node);
|
|
888
|
-
this.#lastSelectedNode = node;
|
|
889
|
-
this.#expandNode(node, depth, true);
|
|
890
|
-
if (!previous.has(node) || previous.size !== 1) this.#dispatchSelectEvent(previous);
|
|
891
|
-
}
|
|
892
|
-
/**
|
|
893
|
-
* Open the column for `node`. `immediate` skips the lazy-load debounce: use
|
|
894
|
-
* it for deliberate clicks, where the user has committed to this node. The
|
|
895
|
-
* debounce is kept for keyboard navigation, which can sweep over many
|
|
896
|
-
* branches in quick succession.
|
|
897
|
-
*/
|
|
898
|
-
#expandNode(node, depth, immediate = false) {
|
|
899
|
-
if (node.item.isLeaf) {
|
|
900
|
-
this._columns = this._columns.slice(0, depth + 1);
|
|
901
|
-
this.requestUpdate();
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true, immediate);
|
|
905
|
-
else {
|
|
906
|
-
const newColumns = this._columns.slice(0, depth + 1);
|
|
907
|
-
newColumns.push(node.children);
|
|
908
|
-
this._columns = newColumns;
|
|
909
|
-
this.dispatchEvent(new CustomEvent(`expand`, {
|
|
910
|
-
detail: {
|
|
911
|
-
node,
|
|
912
|
-
depth
|
|
913
|
-
},
|
|
914
|
-
bubbles: true,
|
|
915
|
-
composed: true
|
|
916
|
-
}));
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Fetch children for a node whose `children === undefined` and show them as
|
|
921
|
-
* the column after `parentDepth` (with the loading placeholder meanwhile).
|
|
922
|
-
* Shared by branch expansion (`parentDepth` = the branch's column) and view
|
|
923
|
-
* rebasing (`parentDepth` = -1, so the children become column 0).
|
|
924
|
-
*
|
|
925
|
-
* The `depth` reported to `loadChildren`/`model.getChildren` is the node's
|
|
926
|
-
* own depth: its column for expansions, -1 for a view root (it sits above
|
|
927
|
-
* column 0, so loaders computing child depth as `depth + 1` stay correct).
|
|
928
|
-
*/
|
|
929
|
-
#loadUnloadedChildren(node, parentDepth, fireExpandEvent, immediate = false) {
|
|
930
|
-
const nodeDepth = parentDepth + 1;
|
|
931
|
-
this._columns = this._columns.slice(0, nodeDepth);
|
|
932
|
-
this._loadingDepth = nodeDepth;
|
|
933
|
-
this.#pendingController?.abort();
|
|
934
|
-
this.#pendingNode = node;
|
|
935
|
-
const controller = new AbortController();
|
|
936
|
-
this.#pendingController = controller;
|
|
937
|
-
if (fireExpandEvent) this.dispatchEvent(new CustomEvent(`expand`, {
|
|
938
|
-
detail: {
|
|
939
|
-
node,
|
|
940
|
-
depth: parentDepth
|
|
941
|
-
},
|
|
942
|
-
bubbles: true,
|
|
943
|
-
composed: true
|
|
944
|
-
}));
|
|
945
|
-
const applyChildren = (children) => {
|
|
946
|
-
if (controller.signal.aborted) return;
|
|
947
|
-
node.children = children;
|
|
948
|
-
this.#pendingNode = void 0;
|
|
949
|
-
this.#pendingController = void 0;
|
|
950
|
-
this._loadingDepth = void 0;
|
|
951
|
-
const cols = this._columns.slice(0, nodeDepth);
|
|
952
|
-
if (children.length > 0) cols.push(children);
|
|
953
|
-
this._columns = cols;
|
|
954
|
-
};
|
|
955
|
-
const handleError = (err) => {
|
|
956
|
-
if (err.name !== `AbortError`) console.error(err);
|
|
957
|
-
node.children = [];
|
|
958
|
-
this.#pendingNode = void 0;
|
|
959
|
-
this.#pendingController = void 0;
|
|
960
|
-
this._loadingDepth = void 0;
|
|
961
|
-
this.requestUpdate();
|
|
962
|
-
};
|
|
963
|
-
if (this.model || this.loadChildren) {
|
|
964
|
-
if (this.#expandTimer !== void 0) {
|
|
965
|
-
clearTimeout(this.#expandTimer);
|
|
966
|
-
this.#expandTimer = void 0;
|
|
967
|
-
}
|
|
968
|
-
const load = () => {
|
|
969
|
-
this.#expandTimer = void 0;
|
|
970
|
-
if (this.#pendingNode !== node) return;
|
|
971
|
-
(this.model ? this.model.getChildren(node, nodeDepth - 1, controller.signal) : collectDataResult(this.loadChildren({
|
|
972
|
-
node,
|
|
973
|
-
depth: nodeDepth - 1
|
|
974
|
-
}, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
|
|
975
|
-
};
|
|
976
|
-
if (immediate) load();
|
|
977
|
-
else this.#expandTimer = setTimeout(load, EXPAND_DEBOUNCE_MS);
|
|
978
|
-
} else {
|
|
979
|
-
this._loadingDepth = void 0;
|
|
980
|
-
this.#pendingNode = void 0;
|
|
981
|
-
this.#pendingController = void 0;
|
|
982
|
-
const newColumns = this._columns.slice(0, nodeDepth);
|
|
983
|
-
newColumns.push([]);
|
|
984
|
-
this._columns = newColumns;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
#onCheckboxClick(node) {
|
|
988
|
-
const previous = new Set(this._selectedNodes);
|
|
989
|
-
const currentlySelected = this._selectedKeys.has(node.item.key);
|
|
990
|
-
if (!!node.children?.length ? this.#allDescendantsSelected(node) : currentlySelected) this.#applyCheckedToggle(node, false);
|
|
991
|
-
else this.#applyCheckedToggle(node, true);
|
|
992
|
-
this.requestUpdate();
|
|
993
|
-
this.#dispatchSelectEvent(previous);
|
|
994
|
-
}
|
|
995
|
-
#allDescendantsSelected(node) {
|
|
996
|
-
if (!this._selectedKeys.has(node.item.key)) return false;
|
|
997
|
-
if (!node.children?.length) return true;
|
|
998
|
-
return node.children.every((child) => this.#allDescendantsSelected(child));
|
|
999
|
-
}
|
|
1000
|
-
#applyCheckedToggle(node, selected) {
|
|
1001
|
-
if (selected) {
|
|
1002
|
-
this._selectedKeys.add(node.item.key);
|
|
1003
|
-
this._selectedNodes.add(node);
|
|
1004
|
-
} else {
|
|
1005
|
-
this._selectedKeys.delete(node.item.key);
|
|
1006
|
-
this._selectedNodes.delete(node);
|
|
1007
|
-
}
|
|
1008
|
-
if (node.children) for (const child of node.children) this.#applyCheckedToggle(child, selected);
|
|
1009
|
-
}
|
|
1010
|
-
#getTickledDepthOrDefault() {
|
|
1011
|
-
return this.#getTickledDepth() ?? 0;
|
|
1012
|
-
}
|
|
1013
|
-
#getTickledNodeAndDepth() {
|
|
1014
|
-
const id = this.tickled.tickledId;
|
|
1015
|
-
if (!id) return void 0;
|
|
1016
|
-
const colonIndex = id.indexOf(`:`);
|
|
1017
|
-
if (colonIndex < 0) return void 0;
|
|
1018
|
-
const depth = Number(id.substring(0, colonIndex));
|
|
1019
|
-
const key = id.substring(colonIndex + 1);
|
|
1020
|
-
const col = this._columns[depth];
|
|
1021
|
-
if (!col) return void 0;
|
|
1022
|
-
const index = col.findIndex((n) => n.item.key === key);
|
|
1023
|
-
if (index < 0) return void 0;
|
|
1024
|
-
return {
|
|
1025
|
-
node: col[index],
|
|
1026
|
-
depth,
|
|
1027
|
-
index
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
#scrollNavigatedItemIntoView(behavior = `smooth`, reveal = `edge`) {
|
|
1031
|
-
requestAnimationFrame(() => {
|
|
1032
|
-
const tickled = this.#getTickledNodeAndDepth();
|
|
1033
|
-
if (!tickled) return;
|
|
1034
|
-
const container = this.shadowRoot?.querySelectorAll(`.column`)[tickled.depth]?.querySelector(`.column-content`);
|
|
1035
|
-
if (!container) return;
|
|
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({
|
|
1050
|
-
block: `nearest`,
|
|
1051
|
-
behavior
|
|
1052
|
-
});
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
#setTickledNode(node, depth) {
|
|
1056
|
-
this.tickled.setTickled(this.#buildTickledId(node, depth));
|
|
1057
|
-
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
1058
|
-
detail: {
|
|
1059
|
-
node,
|
|
1060
|
-
depth
|
|
1061
|
-
},
|
|
1062
|
-
bubbles: true,
|
|
1063
|
-
composed: true
|
|
1064
|
-
}));
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Number of items a PageUp/PageDown moves: how many rows fit in the column's
|
|
1068
|
-
* visible area, less one so the previous page's last row stays in view.
|
|
1069
|
-
*/
|
|
1070
|
-
#pageSize(depth) {
|
|
1071
|
-
const content = this.shadowRoot?.querySelectorAll(`.column`)[depth]?.querySelector(`.column-content`);
|
|
1072
|
-
const item = content?.querySelector(`.item`);
|
|
1073
|
-
if (!content || !item || item.offsetHeight === 0) return 10;
|
|
1074
|
-
return Math.max(1, Math.floor(content.clientHeight / item.offsetHeight) - 1);
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Move the cursor within column `col` to the index returned by `target`
|
|
1078
|
-
* (given the current cursor index, or -1 when the cursor isn't in this
|
|
1079
|
-
* column, which behaves as "before the first item"). Handles Shift range
|
|
1080
|
-
* extension and anchor seating for `standard` / `vscode` modes.
|
|
1081
|
-
* `scroll` brings the new item into view (for jumps larger than one row).
|
|
1082
|
-
*/
|
|
1083
|
-
#moveCursor(col, currentDepth, event, target, scroll = false) {
|
|
1084
|
-
const current = this.#getTickledNodeAndDepth();
|
|
1085
|
-
const targetIndex = target(current !== void 0 && current.depth === currentDepth ? current.index : -1);
|
|
1086
|
-
const targetNode = col[targetIndex];
|
|
1087
|
-
if (!targetNode) return;
|
|
1088
|
-
this.#setTickledNode(targetNode, currentDepth);
|
|
1089
|
-
if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
|
|
1090
|
-
const prevIndex = current?.index ?? 0;
|
|
1091
|
-
if (event.shiftKey) {
|
|
1092
|
-
if (!this.#anchor || this.#anchor.depth !== currentDepth) {
|
|
1093
|
-
const anchorNode = col[prevIndex];
|
|
1094
|
-
if (anchorNode) this.#anchor = {
|
|
1095
|
-
key: anchorNode.item.key,
|
|
1096
|
-
depth: currentDepth,
|
|
1097
|
-
index: prevIndex,
|
|
1098
|
-
selectionSnapshot: this.interactionMode === `vscode` ? new Set(this._selectedKeys) : void 0
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
if (this.#anchor) {
|
|
1102
|
-
const from = Math.min(this.#anchor.index, targetIndex);
|
|
1103
|
-
const to = Math.max(this.#anchor.index, targetIndex);
|
|
1104
|
-
const rangeKeys = col.slice(from, to + 1).map((n) => n.item.key);
|
|
1105
|
-
if (this.interactionMode === `vscode`) {
|
|
1106
|
-
const snap = this.#anchor.selectionSnapshot;
|
|
1107
|
-
for (const k of rangeKeys) if (snap.has(k)) this._selectedKeys.delete(k);
|
|
1108
|
-
else this._selectedKeys.add(k);
|
|
1109
|
-
} else this._selectedKeys = new Set(rangeKeys);
|
|
1110
|
-
}
|
|
1111
|
-
} else if (this.interactionMode === `standard`) this.#anchor = {
|
|
1112
|
-
key: targetNode.item.key,
|
|
1113
|
-
depth: currentDepth,
|
|
1114
|
-
index: targetIndex
|
|
1115
|
-
};
|
|
1116
|
-
else this.#anchor = void 0;
|
|
1117
|
-
}
|
|
1118
|
-
this.requestUpdate();
|
|
1119
|
-
if (scroll) this.#scrollNavigatedItemIntoView(`auto`);
|
|
1120
|
-
}
|
|
1121
|
-
#onKeyDown;
|
|
1122
|
-
#onResizeStart;
|
|
1123
|
-
#resizeRaf;
|
|
1124
|
-
#resizePendingWidth;
|
|
1125
|
-
#onResizeMove;
|
|
1126
|
-
#applyResizeWidth;
|
|
1127
|
-
#onResizeEnd;
|
|
1128
|
-
#getColumnHeader(depth) {
|
|
1129
|
-
if (depth === 0) return this.#displayRoot?.item.label ?? ``;
|
|
1130
|
-
const explicit = this._selectedByDepth.get(depth - 1);
|
|
1131
|
-
if (explicit) return explicit.item.label;
|
|
1132
|
-
const prevCol = this._columns[depth - 1];
|
|
1133
|
-
const thisCol = this._columns[depth];
|
|
1134
|
-
if (prevCol && thisCol) {
|
|
1135
|
-
const parent = prevCol.find((n) => n.children === thisCol);
|
|
1136
|
-
if (parent) return parent.item.label;
|
|
1137
|
-
}
|
|
1138
|
-
return ``;
|
|
1139
|
-
}
|
|
1140
|
-
#explicitWidthStyle(width) {
|
|
1141
|
-
return `width:${width}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
|
|
1142
|
-
}
|
|
1143
|
-
#columnStyle(depth, isLast) {
|
|
1144
|
-
const w = this.#columnWidths.get(depth);
|
|
1145
|
-
if (w !== void 0) return this.#explicitWidthStyle(w);
|
|
1146
|
-
if (this.stretchLastColumn && isLast) return `width:max-content;flex-grow:1;flex-shrink:0;flex-basis:auto;min-width:0`;
|
|
1147
|
-
return `width:max-content;flex-grow:0;flex-shrink:0;flex-basis:auto`;
|
|
1148
|
-
}
|
|
1149
|
-
#isFocused(node, depth) {
|
|
1150
|
-
return this.tickled.isTickled(this.#buildTickledId(node, depth));
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Override to provide a custom loading indicator.
|
|
1154
|
-
*/
|
|
1155
|
-
renderLoadingIndicator() {
|
|
1156
|
-
return html`<div class="loading-spinner"></div>`;
|
|
1157
|
-
}
|
|
1158
|
-
#renderLoadingColumn() {
|
|
1159
|
-
const depth = this._loadingDepth;
|
|
1160
|
-
const stretched = this.stretchLastColumn && this.#columnWidths.get(depth) === void 0;
|
|
1161
|
-
return html`
|
|
1162
|
-
<div class="column loading-column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, true)}>
|
|
1163
|
-
${this.headers ? html`
|
|
1164
|
-
<div class="column-header">${this.#getColumnHeader(depth)}</div>
|
|
1165
|
-
` : nothing}
|
|
1166
|
-
<div class="column-content loading-content">
|
|
1167
|
-
${this.renderLoadingIndicator()}
|
|
1168
|
-
</div>
|
|
1169
|
-
</div>
|
|
1170
|
-
`;
|
|
1171
|
-
}
|
|
1172
|
-
render() {
|
|
1173
|
-
return html`
|
|
1174
|
-
<div id="container" @keydown=${this.#onKeyDown} tabindex="0">
|
|
1175
|
-
${this._columns.map((nodes, depth) => {
|
|
1176
|
-
const visibleNodes = this.filterPredicate ? nodes.filter((n) => this.filterPredicate(n, depth)) : nodes;
|
|
1177
|
-
const isLast = depth === this._columns.length - 1 && this._loadingDepth === void 0;
|
|
1178
|
-
const stretched = this.stretchLastColumn && isLast && this.#columnWidths.get(depth) === void 0;
|
|
1179
|
-
return html`
|
|
1180
|
-
<div class="column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, isLast)}>
|
|
1181
|
-
${this.headers ? html`
|
|
1182
|
-
<div class="column-header"
|
|
1183
|
-
@click=${() => this.#onHeaderClick(depth)}
|
|
1184
|
-
@contextmenu=${(e) => this._handleHeaderRightClick(e, depth)}>
|
|
1185
|
-
${this.#getColumnHeader(depth)}
|
|
1186
|
-
</div>
|
|
1187
|
-
` : nothing}
|
|
1188
|
-
<div class="column-content">
|
|
1189
|
-
${this.renderColumnContent(visibleNodes, depth)}
|
|
1190
|
-
</div>
|
|
1191
|
-
<div class="resize-handle"
|
|
1192
|
-
@pointerdown=${(e) => this.#onResizeStart(e, depth)}>
|
|
1193
|
-
</div>
|
|
1194
|
-
</div>
|
|
1195
|
-
`;
|
|
1196
|
-
})}
|
|
1197
|
-
${this._loadingDepth !== void 0 ? this.#renderLoadingColumn() : nothing}
|
|
1198
|
-
</div>
|
|
1199
|
-
${this.search.renderField()}
|
|
1200
|
-
`;
|
|
1201
|
-
}
|
|
1202
|
-
_getFocusedIndex(depth) {
|
|
1203
|
-
const tickled = this.#getTickledNodeAndDepth();
|
|
1204
|
-
if (tickled && tickled.depth === depth) return tickled.index;
|
|
1205
|
-
return 0;
|
|
1206
|
-
}
|
|
1207
|
-
_isFocused(node, depth) {
|
|
1208
|
-
return this.#isFocused(node, depth);
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Called by subclasses when pointer enters an item.
|
|
1212
|
-
* Fires tickle event and switches to mouse mode.
|
|
1213
|
-
*/
|
|
1214
|
-
_handleItemPointerEnter(node, depth) {
|
|
1215
|
-
this.tickled.handlePointerEnter(this.#buildTickledId(node, depth));
|
|
1216
|
-
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
1217
|
-
detail: {
|
|
1218
|
-
node,
|
|
1219
|
-
depth
|
|
1220
|
-
},
|
|
1221
|
-
bubbles: true,
|
|
1222
|
-
composed: true
|
|
1223
|
-
}));
|
|
1224
|
-
}
|
|
1225
|
-
/**
|
|
1226
|
-
* Called by subclasses when pointer leaves the column container.
|
|
1227
|
-
*/
|
|
1228
|
-
_handleColumnPointerLeave() {
|
|
1229
|
-
this.tickled.handlePointerLeave(new PointerEvent(`pointerleave`));
|
|
1230
|
-
}
|
|
1231
|
-
static {
|
|
1232
|
-
this.styles = [
|
|
1233
|
-
themeFallbacks,
|
|
1234
|
-
tickledStyles,
|
|
1235
|
-
listSearchStyles,
|
|
1236
|
-
css`
|
|
1237
|
-
:host {
|
|
1238
|
-
--column-bg: var(--surface-3);
|
|
1239
|
-
--column-header-bg: var(--surface-2);
|
|
1240
|
-
--column-header-text: var(--surface-muted-text);
|
|
1241
|
-
--column-separator: var(--surface-5);
|
|
1242
|
-
--column-resize-handle: var(--surface-5);
|
|
1243
|
-
--column-resize-handle-hover: var(--accent);
|
|
1244
|
-
--column-resize-width: 4px;
|
|
1245
|
-
--item-hover-bg: var(--item-bg-tickled);
|
|
1246
|
-
--item-open-bg: color-mix(in oklch, var(--accent, oklch(60% 0.15 140)) 15%, transparent);
|
|
1247
|
-
--item-selected-bg: var(--item-bg-selected);
|
|
1248
|
-
--item-selected-text: var(--item-text-selected);
|
|
1249
|
-
|
|
1250
|
-
display: block;
|
|
1251
|
-
overflow: hidden;
|
|
1252
|
-
position: relative;
|
|
1253
|
-
box-sizing: border-box;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
#container {
|
|
1257
|
-
display: flex;
|
|
1258
|
-
height: 100%;
|
|
1259
|
-
overflow-x: auto;
|
|
1260
|
-
outline: none;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
.column {
|
|
1264
|
-
display: flex;
|
|
1265
|
-
flex-direction: column;
|
|
1266
|
-
position: relative;
|
|
1267
|
-
background: var(--column-bg);
|
|
1268
|
-
border-right: 1px solid var(--column-separator);
|
|
1269
|
-
flex-shrink: 0;
|
|
1270
|
-
/* Content-sized columns never grow wider than 80% of the available
|
|
1271
|
-
width — pathological labels truncate with ellipsis instead of
|
|
1272
|
-
pushing every other column out of view */
|
|
1273
|
-
max-width: 80%;
|
|
1274
|
-
/* Smoothly animate the last column stretching to fill remaining
|
|
1275
|
-
space and shrinking back to content width when it stops being last */
|
|
1276
|
-
transition: flex-grow var(--transition, 0.2s ease-out);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
.column.stretch-last {
|
|
1280
|
-
min-width: 0;
|
|
1281
|
-
/* Stretching fills remaining space by definition, so it is exempt
|
|
1282
|
-
from the 80% auto-size cap */
|
|
1283
|
-
max-width: none;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
.column-header {
|
|
1287
|
-
background: var(--column-header-bg);
|
|
1288
|
-
color: var(--column-header-text);
|
|
1289
|
-
font-size: var(--text-s, 11px);
|
|
1290
|
-
font-family: var(--font-family, system-ui, sans-serif);
|
|
1291
|
-
font-weight: var(--font-weight-bold, 600);
|
|
1292
|
-
padding: var(--space-xs, 3px) var(--space-m, 8px);
|
|
1293
|
-
border-bottom: 1px solid var(--column-separator);
|
|
1294
|
-
white-space: nowrap;
|
|
1295
|
-
overflow: hidden;
|
|
1296
|
-
text-overflow: ellipsis;
|
|
1297
|
-
flex-shrink: 0;
|
|
1298
|
-
cursor: pointer;
|
|
1299
|
-
user-select: none;
|
|
1300
|
-
transition: background var(--transition, 0.15s ease-out),
|
|
1301
|
-
color var(--transition, 0.15s ease-out);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
.column-header:hover {
|
|
1305
|
-
background: var(--item-hover-bg);
|
|
1306
|
-
color: var(--surface-2-text, #fff);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
.column-content {
|
|
1310
|
-
flex: 1;
|
|
1311
|
-
overflow-y: auto;
|
|
1312
|
-
overflow-x: visible;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
.resize-handle {
|
|
1316
|
-
position: absolute;
|
|
1317
|
-
top: 0;
|
|
1318
|
-
right: 0;
|
|
1319
|
-
width: var(--column-resize-width);
|
|
1320
|
-
height: 100%;
|
|
1321
|
-
background: var(--column-resize-handle);
|
|
1322
|
-
cursor: ew-resize;
|
|
1323
|
-
z-index: 1;
|
|
1324
|
-
transition: background var(--transition, 0.15s ease-out);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
.resize-handle:hover {
|
|
1328
|
-
background: var(--column-resize-handle-hover);
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
.loading-column {
|
|
1332
|
-
min-width: 60px;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
.loading-content {
|
|
1336
|
-
display: flex;
|
|
1337
|
-
align-items: center;
|
|
1338
|
-
justify-content: center;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
.loading-spinner {
|
|
1342
|
-
width: 22px;
|
|
1343
|
-
height: 22px;
|
|
1344
|
-
border: 2px solid var(--column-separator);
|
|
1345
|
-
border-top-color: var(--accent, oklch(60% 0.15 140));
|
|
1346
|
-
border-radius: 50%;
|
|
1347
|
-
animation: miller-spin 0.7s linear infinite;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
@keyframes miller-spin {
|
|
1351
|
-
to { transform: rotate(360deg); }
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
::highlight(incr-search-highlight) {
|
|
1355
|
-
background: var(--search-highlight-bg, var(--accent));
|
|
1356
|
-
color: var(--search-highlight-color, var(--accent-text, #fff));
|
|
1357
|
-
border-radius: 2px;
|
|
1358
|
-
padding: 0 1px;
|
|
1359
|
-
}
|
|
1360
|
-
`
|
|
1361
|
-
];
|
|
1362
|
-
}
|
|
1363
|
-
};
|
|
1364
|
-
__decorate([property({ type: Object })], MillerBaseElement.prototype, "root", void 0);
|
|
1365
|
-
__decorate([property({ attribute: false })], MillerBaseElement.prototype, "model", void 0);
|
|
1366
|
-
__decorate([property({ type: Boolean })], MillerBaseElement.prototype, "headers", void 0);
|
|
1367
|
-
__decorate([property({
|
|
1368
|
-
type: Boolean,
|
|
1369
|
-
attribute: `stretch-last-column`,
|
|
1370
|
-
reflect: true
|
|
1371
|
-
})], MillerBaseElement.prototype, "stretchLastColumn", void 0);
|
|
1372
|
-
__decorate([property({ attribute: false })], MillerBaseElement.prototype, "viewRoot", void 0);
|
|
1373
|
-
__decorate([property({ type: String })], MillerBaseElement.prototype, "interactionMode", void 0);
|
|
1374
|
-
__decorate([property({
|
|
1375
|
-
reflect: true,
|
|
1376
|
-
attribute: `selection-filter`
|
|
1377
|
-
})], MillerBaseElement.prototype, "selectionFilter", void 0);
|
|
1378
|
-
__decorate([property({
|
|
1379
|
-
reflect: true,
|
|
1380
|
-
attribute: `exclusivity`
|
|
1381
|
-
})], MillerBaseElement.prototype, "exclusivity", void 0);
|
|
1382
|
-
__decorate([property({ type: Object })], MillerBaseElement.prototype, "loadChildren", void 0);
|
|
1383
|
-
__decorate([property({ attribute: false })], MillerBaseElement.prototype, "filterPredicate", void 0);
|
|
1384
|
-
__decorate([property({
|
|
1385
|
-
type: String,
|
|
1386
|
-
attribute: `search-modes`,
|
|
1387
|
-
reflect: true
|
|
1388
|
-
})], MillerBaseElement.prototype, "searchModes", void 0);
|
|
1389
|
-
__decorate([state()], MillerBaseElement.prototype, "_columns", void 0);
|
|
1390
|
-
__decorate([state()], MillerBaseElement.prototype, "_loadingDepth", void 0);
|
|
1391
|
-
__decorate([state()], MillerBaseElement.prototype, "_selectedNodes", void 0);
|
|
1392
|
-
MillerBaseElement = __decorate([safeCustomElement(`ixfx-miller-base`)], MillerBaseElement);
|
|
1393
|
-
//#endregion
|
|
1394
|
-
//#region src/miller/miller-list.ts
|
|
1395
|
-
const HAS_CHECKED = 1;
|
|
1396
|
-
const HAS_UNCHECKED = 2;
|
|
1397
|
-
/**
|
|
1398
|
-
* Walk the loaded subtree under `node`, returning which kinds of leaf were
|
|
1399
|
-
* seen (`HAS_CHECKED` | `HAS_UNCHECKED`). Stops as soon as both have been
|
|
1400
|
-
* seen, since the result can no longer change.
|
|
1401
|
-
*/
|
|
1402
|
-
function scanCheckState(node, selectedKeys) {
|
|
1403
|
-
if (node.item.isLeaf || !node.children?.length) return selectedKeys.has(node.item.key) ? HAS_CHECKED : HAS_UNCHECKED;
|
|
1404
|
-
let seen = 0;
|
|
1405
|
-
for (const child of node.children) {
|
|
1406
|
-
seen |= scanCheckState(child, selectedKeys);
|
|
1407
|
-
if (seen === 3) break;
|
|
1408
|
-
}
|
|
1409
|
-
return seen;
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Compute the checkbox state for a node based on `selectedKeys`.
|
|
1413
|
-
*
|
|
1414
|
-
* - Unloaded branch (`children === undefined`): treated as a leaf — we can only
|
|
1415
|
-
* report on what is known.
|
|
1416
|
-
* - Leaf or confirmed-empty branch: checked iff its key is in `selectedKeys`.
|
|
1417
|
-
* - Loaded branch: checked if ALL descendants are checked; unchecked if NONE are;
|
|
1418
|
-
* indeterminate otherwise.
|
|
1419
|
-
*/
|
|
1420
|
-
function computeCheckboxState(node, selectedKeys) {
|
|
1421
|
-
const seen = scanCheckState(node, selectedKeys);
|
|
1422
|
-
if (seen === HAS_CHECKED) return `checked`;
|
|
1423
|
-
if (seen === HAS_UNCHECKED) return `unchecked`;
|
|
1424
|
-
return `indeterminate`;
|
|
1425
|
-
}
|
|
1426
|
-
let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
1427
|
-
renderColumnContent(nodes, depth) {
|
|
1428
|
-
const hasIcons = nodes.some((node) => !!node.item.icon);
|
|
1429
|
-
const isCheckedMode = this.interactionMode === `checked`;
|
|
1430
|
-
return html`
|
|
1431
|
-
<ul
|
|
1432
|
-
class="item-list ${hasIcons ? `has-icons` : ``} ${isCheckedMode ? `checked-mode` : ``}"
|
|
1433
|
-
role="listbox"
|
|
1434
|
-
aria-label="Column ${depth}"
|
|
1435
|
-
@pointerleave=${() => this._handleColumnPointerLeave()}
|
|
1436
|
-
>
|
|
1437
|
-
${nodes.map((node) => this.#renderListItem(node, depth))}
|
|
1438
|
-
</ul>
|
|
1439
|
-
`;
|
|
1440
|
-
}
|
|
1441
|
-
#renderListItem(node, depth) {
|
|
1442
|
-
const selected = this._isSelected(node, depth);
|
|
1443
|
-
const tickled = this._isFocused(node, depth);
|
|
1444
|
-
const open = this._isOpen(node, depth);
|
|
1445
|
-
const isBranch = node.item.isLeaf !== true;
|
|
1446
|
-
const icon = node.item.icon;
|
|
1447
|
-
const iconColor = appearanceColor(node.item.iconColor);
|
|
1448
|
-
const textColor = appearanceColor(node.item.textColor);
|
|
1449
|
-
const badge = appearanceAbsent(node.item.badge) ? void 0 : node.item.badge;
|
|
1450
|
-
const tooltip = node.item.tooltip;
|
|
1451
|
-
const isCheckedMode = this.interactionMode === `checked`;
|
|
1452
|
-
let checkboxTpl = nothing;
|
|
1453
|
-
if (isCheckedMode) {
|
|
1454
|
-
const state = computeCheckboxState(node, this._selectedKeys);
|
|
1455
|
-
checkboxTpl = html`
|
|
1456
|
-
<input
|
|
1457
|
-
type="checkbox"
|
|
1458
|
-
class="item-checkbox"
|
|
1459
|
-
.checked=${state === `checked`}
|
|
1460
|
-
.indeterminate=${state === `indeterminate`}
|
|
1461
|
-
aria-label="Select ${node.item.label}"
|
|
1462
|
-
@click=${(e) => this._handleCheckboxClick(e, node, depth)}
|
|
1463
|
-
/>
|
|
1464
|
-
`;
|
|
1465
|
-
}
|
|
1466
|
-
return html`
|
|
1467
|
-
<li
|
|
1468
|
-
class="item ${selected ? `selected` : ``} ${open ? `open` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`}"
|
|
1469
|
-
role="option"
|
|
1470
|
-
aria-selected=${selected}
|
|
1471
|
-
@click=${(e) => this._handleItemClick(node, depth, e)}
|
|
1472
|
-
@dblclick=${(e) => this._handleItemDblClick(node, depth, e)}
|
|
1473
|
-
@contextmenu=${(e) => this._handleItemRightClick(node, depth, e)}
|
|
1474
|
-
@pointerenter=${() => this._handleItemPointerEnter(node, depth)}
|
|
1475
|
-
>
|
|
1476
|
-
${checkboxTpl}
|
|
1477
|
-
${icon ? html`<ixfx-icon class="item-icon" name="${icon}" style=${iconColor ? `color:${iconColor}` : nothing}></ixfx-icon>` : nothing}
|
|
1478
|
-
<span class="label" style=${textColor ? `color:${textColor}` : nothing}>${node.item.label}</span>
|
|
1479
|
-
${badge === void 0 ? nothing : html`<span class="item-badge" part="badge">${badge}</span>`}
|
|
1480
|
-
${tooltip ? html`<ixfx-tooltip class="item-tooltip" anchor="--tooltip-miller-${depth}-${node.item.key}" .value=${tooltip}></ixfx-tooltip>` : nothing}
|
|
1481
|
-
${isBranch ? html`<span class="caret" aria-hidden="true">›</span>` : nothing}
|
|
1482
|
-
</li>
|
|
1483
|
-
`;
|
|
1484
|
-
}
|
|
1485
|
-
static {
|
|
1486
|
-
this.styles = [
|
|
1487
|
-
themeFallbacks,
|
|
1488
|
-
...MillerBaseElement.styles,
|
|
1489
|
-
tickledItemStyles,
|
|
1490
|
-
css`
|
|
1491
|
-
.item-list {
|
|
1492
|
-
list-style: none;
|
|
1493
|
-
margin: 0;
|
|
1494
|
-
padding: var(--space-xs, 3px) 0;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
.item {
|
|
1498
|
-
display: flex;
|
|
1499
|
-
align-items: center;
|
|
1500
|
-
justify-content: space-between;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
.item.selected {
|
|
1504
|
-
background: var(--item-selected-bg);
|
|
1505
|
-
color: var(--item-selected-text, #fff);
|
|
1506
|
-
outline: 2px dashed var(--surface-3, #333);
|
|
1507
|
-
outline-offset: -2px;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
/* Item is on the currently open path (ancestor of deepest column) */
|
|
1511
|
-
.item.open:not(.selected) {
|
|
1512
|
-
background: var(--item-open-bg);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
.item-icon {
|
|
1516
|
-
flex-shrink: 0;
|
|
1517
|
-
width: var(--icon-size, 1em);
|
|
1518
|
-
height: var(--icon-size, 1em);
|
|
1519
|
-
margin-right: var(--icon-spacing, 0.3em);
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
/* Align text on icon-less rows with icon rows in a mixed list */
|
|
1523
|
-
.item-list.has-icons .item:not(:has(.item-icon)) .label {
|
|
1524
|
-
margin-left: calc(var(--icon-size, 1em) + var(--icon-spacing, 0.3em));
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
.label {
|
|
1528
|
-
flex: 1;
|
|
1529
|
-
overflow: hidden;
|
|
1530
|
-
text-overflow: ellipsis;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
/* Finder-style badge: sits between the label and the caret, flush to
|
|
1534
|
-
the right edge of the row. The label is flex:1, so the gap opens up
|
|
1535
|
-
ahead of the badge automatically. */
|
|
1536
|
-
.item-badge {
|
|
1537
|
-
flex-shrink: 0;
|
|
1538
|
-
display: inline-flex;
|
|
1539
|
-
align-items: center;
|
|
1540
|
-
margin-inline-start: auto;
|
|
1541
|
-
padding-inline-start: var(--miller-list-badge-gap, 0.5em);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
.item-tooltip {
|
|
1545
|
-
position: absolute;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
.caret {
|
|
1549
|
-
flex-shrink: 0;
|
|
1550
|
-
margin-left: var(--space-s, 4px);
|
|
1551
|
-
color: var(--surface-muted-text);
|
|
1552
|
-
font-size: var(--text-l, 13px);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
.item.selected .caret {
|
|
1556
|
-
color: var(--item-selected-text, #fff);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
/* ---- checked mode ---- */
|
|
1560
|
-
|
|
1561
|
-
.item-checkbox {
|
|
1562
|
-
flex-shrink: 0;
|
|
1563
|
-
margin-right: var(--space-xs, 3px);
|
|
1564
|
-
cursor: pointer;
|
|
1565
|
-
accent-color: var(--accent);
|
|
1566
|
-
/* Prevent the checkbox click from also triggering item styles */
|
|
1567
|
-
pointer-events: auto;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
/* In checked mode the item row itself is not highlighted on selection —
|
|
1571
|
-
the checkbox communicates state instead. */
|
|
1572
|
-
.item-list.checked-mode .item.selected {
|
|
1573
|
-
background: transparent;
|
|
1574
|
-
color: var(--surface-3-text, #fff);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
`
|
|
1579
|
-
];
|
|
1580
|
-
}
|
|
1581
|
-
};
|
|
1582
|
-
MillerListElement = __decorate([safeCustomElement(`ixfx-miller-list`)], MillerListElement);
|
|
1583
|
-
//#endregion
|
|
1584
|
-
//#region src/miller/miller-tree-controller.ts
|
|
1585
|
-
/**
|
|
1586
|
-
* Bridges an `ixfx` `TraversableTree<T>` with an `ixfx-miller-*` component.
|
|
1587
|
-
*
|
|
1588
|
-
* Usage:
|
|
1589
|
-
* ```ts
|
|
1590
|
-
* import { Trees } from '@ixfx/collections';
|
|
1591
|
-
* import { MillerTreeController } from 'ixfx-components/miller';
|
|
1592
|
-
*
|
|
1593
|
-
* const root = Trees.Mutable.createNode({ key: 'root', label: 'Root' });
|
|
1594
|
-
* Trees.Mutable.addValue({ key: 'a', label: 'Alpha' }, root);
|
|
1595
|
-
*
|
|
1596
|
-
* const ctrl = new MillerTreeController(
|
|
1597
|
-
* Trees.Mutable.asDynamicTraversable(root),
|
|
1598
|
-
* document.querySelector('ixfx-miller-list'),
|
|
1599
|
-
* { valueToItem: v => ({ key: v.key, label: v.label }) }
|
|
1600
|
-
* );
|
|
1601
|
-
* ctrl.connect();
|
|
1602
|
-
* ```
|
|
1603
|
-
*
|
|
1604
|
-
* Children are populated lazily: the controller provides a `loadChildren`
|
|
1605
|
-
* callback that synchronously reads from the underlying tree.
|
|
1606
|
-
*
|
|
1607
|
-
* Call `disconnect()` to remove all event listeners and release resources.
|
|
1608
|
-
*/
|
|
1609
|
-
var MillerTreeController = class {
|
|
1610
|
-
#root;
|
|
1611
|
-
#element;
|
|
1612
|
-
#options;
|
|
1613
|
-
#nodeToTree = /* @__PURE__ */ new WeakMap();
|
|
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();
|
|
1619
|
-
#connectSignal;
|
|
1620
|
-
constructor(root, element, options) {
|
|
1621
|
-
this.#root = root;
|
|
1622
|
-
this.#element = element;
|
|
1623
|
-
this.#options = options;
|
|
1624
|
-
}
|
|
1625
|
-
/**
|
|
1626
|
-
* Wire up the controller: sets `element.root` and provides `loadChildren` callback.
|
|
1627
|
-
* Safe to call again after `disconnect()`.
|
|
1628
|
-
*/
|
|
1629
|
-
connect() {
|
|
1630
|
-
this.disconnect();
|
|
1631
|
-
this.#connectSignal = new AbortController();
|
|
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 }) => {
|
|
1641
|
-
const treeNode = this.#nodeToTree.get(node);
|
|
1642
|
-
if (!treeNode) return [];
|
|
1643
|
-
return this.#childrenOf(treeNode);
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
/** Remove all event listeners and release internal state. */
|
|
1647
|
-
disconnect() {
|
|
1648
|
-
this.#connectSignal?.abort();
|
|
1649
|
-
this.#connectSignal = void 0;
|
|
1650
|
-
this.#element.loadChildren = void 0;
|
|
1651
|
-
}
|
|
1652
|
-
/**
|
|
1653
|
-
* Navigate the component to the first node (depth-first) in the ixfx tree
|
|
1654
|
-
* whose value satisfies `predicate`.
|
|
1655
|
-
*
|
|
1656
|
-
* Any ancestor `MillerNode`s with unloaded children are populated
|
|
1657
|
-
* synchronously before calling `element.navigateTo()`.
|
|
1658
|
-
*
|
|
1659
|
-
* Key paths are relative to the element's current view root: when the view
|
|
1660
|
-
* is rebased, only matches at or below the view root can be reached.
|
|
1661
|
-
*
|
|
1662
|
-
* Returns `false` if no matching node is found.
|
|
1663
|
-
*/
|
|
1664
|
-
navigateToValue(predicate, options) {
|
|
1665
|
-
const treePath = this.#findTreePath(this.#root, predicate);
|
|
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) {
|
|
1729
|
-
const base = this.#element.viewRoot ?? this.#element.root;
|
|
1730
|
-
if (!base) return false;
|
|
1731
|
-
let startIndex = 1;
|
|
1732
|
-
if (this.#element.viewRoot) {
|
|
1733
|
-
const mappedBase = this.#nodeToTree.get(base);
|
|
1734
|
-
if (!mappedBase) return false;
|
|
1735
|
-
const baseId = mappedBase.getIdentity();
|
|
1736
|
-
const baseIndex = treePath.findIndex((n) => n.getIdentity() === baseId);
|
|
1737
|
-
if (baseIndex < 0) return false;
|
|
1738
|
-
startIndex = baseIndex + 1;
|
|
1739
|
-
}
|
|
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);
|
|
1757
|
-
}
|
|
1758
|
-
return item;
|
|
1759
|
-
}
|
|
1760
|
-
/** Convert a TraversableTree node's children to MillerNode[]. */
|
|
1761
|
-
#childrenOf(treeNode) {
|
|
1762
|
-
return Array.from(treeNode.children(), (child) => this.#toMillerNode(child));
|
|
1763
|
-
}
|
|
1764
|
-
#toMillerNode(treeNode) {
|
|
1765
|
-
const childList = [...treeNode.children()];
|
|
1766
|
-
for (const child of childList) this.#identityToTree.set(child.getIdentity(), child);
|
|
1767
|
-
const userItem = this.#itemOf(treeNode);
|
|
1768
|
-
const isLeaf = userItem.isLeaf ?? childList.length === 0;
|
|
1769
|
-
const millerNode = {
|
|
1770
|
-
item: {
|
|
1771
|
-
...userItem,
|
|
1772
|
-
isLeaf
|
|
1773
|
-
},
|
|
1774
|
-
children: isLeaf ? [] : void 0
|
|
1775
|
-
};
|
|
1776
|
-
this.#register(millerNode, treeNode);
|
|
1777
|
-
return millerNode;
|
|
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
|
-
}
|
|
1805
|
-
/** DFS through the ixfx tree returning the path of nodes to the first match. */
|
|
1806
|
-
#findTreePath(node, predicate, ancestors = []) {
|
|
1807
|
-
const path = [...ancestors, node];
|
|
1808
|
-
if (predicate(node.getValue())) return path;
|
|
1809
|
-
for (const child of node.children()) {
|
|
1810
|
-
const result = this.#findTreePath(child, predicate, path);
|
|
1811
|
-
if (result) return result;
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
};
|
|
1815
|
-
//#endregion
|
|
1
|
+
import { n as MillerListElement, r as MillerBaseElement, t as MillerTreeController } from "./miller-BFRfn0ve.js";
|
|
1816
2
|
export { MillerBaseElement, MillerListElement, MillerTreeController };
|