@ixfx/components 0.15.4 → 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.
Files changed (101) hide show
  1. package/bundle/index.d.ts +1223 -48
  2. package/bundle/index.js +875 -700
  3. package/dist/ac-text.d.ts +1 -1
  4. package/dist/{ac-token-Cq7lsYf-.d.ts → ac-token-qEs3FsQv.d.ts} +1 -1
  5. package/dist/ac-token.d.ts +1 -1
  6. package/dist/colour-picker.d.ts +1 -1
  7. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  8. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  9. package/dist/crumbs.d.ts +2 -115
  10. package/dist/crumbs.js +1 -1179
  11. package/dist/{data-grid-CbiubdG2.d.ts → data-grid-CIG2DVip.d.ts} +58 -9
  12. package/dist/{data-grid-Dx3l3dxw.js → data-grid-rMkLX2qo.js} +630 -215
  13. package/dist/data-grid.d.ts +3 -3
  14. package/dist/data-grid.js +1 -1
  15. package/dist/{detail-list-ClkMV0SN.d.ts → detail-list-6iMTNP4N.d.ts} +35 -5
  16. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  17. package/dist/detail-list.d.ts +4 -4
  18. package/dist/detail-list.js +1 -1
  19. package/dist/{flat-list-0xTZdXzh.d.ts → flat-list-JPgQSmLW.d.ts} +28 -5
  20. package/dist/flat-list.d.ts +2 -2
  21. package/dist/flat-list.js +123 -4
  22. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  23. package/dist/{grid-list-BnWJUIBC.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  24. package/dist/grid-list.d.ts +3 -3
  25. package/dist/grid-list.js +1 -1
  26. package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +1 -1
  27. package/dist/grouped-item-lister.d.ts +1 -1
  28. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +1 -1
  29. package/dist/icons.d.ts +2 -2
  30. package/dist/incr-search.d.ts +1 -1
  31. package/dist/index-9ttcdwlL.d.ts +71 -0
  32. package/dist/{index-BAuqR9El.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  33. package/dist/index-CYRc6Dah.d.ts +475 -0
  34. package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +1 -1
  35. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +1 -1
  36. package/dist/{index-Bz3loIAf.d.ts → index-DN2BWctP.d.ts} +1 -1
  37. package/dist/{index-BPTuCRGb.d.ts → index-Dr80Zdl6.d.ts} +2 -2
  38. package/dist/{index-cnaRPQPp.d.ts → index-DxyyVqzA.d.ts} +2 -2
  39. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +1 -1
  40. package/dist/index.d.ts +380 -33
  41. package/dist/index.js +2247 -866
  42. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +1 -1
  43. package/dist/labelled-radial-input.d.ts +2 -2
  44. package/dist/labelled-range-input.d.ts +1 -1
  45. package/dist/labelled-slider-input.d.ts +1 -1
  46. package/dist/list-group-header.d.ts +1 -1
  47. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  48. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  49. package/dist/{menu-item-B0zqw2xp.d.ts → menu-item-bF6narIz.d.ts} +1 -1
  50. package/dist/menu.d.ts +2 -2
  51. package/dist/miller-BFRfn0ve.js +2237 -0
  52. package/dist/miller.d.ts +4 -437
  53. package/dist/miller.js +1 -1815
  54. package/dist/panel.d.ts +1 -1
  55. package/dist/pending-BGidHppL.js +91 -0
  56. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  57. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-CophkNeU.d.ts} +42 -4
  58. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  59. package/dist/split-layout.d.ts +1 -1
  60. package/dist/split-layout.js +1 -1
  61. package/dist/sunburst-tree.d.ts +2 -0
  62. package/dist/sunburst-tree.js +2 -0
  63. package/dist/sunbursttree-list-B_VxOHlo.js +1556 -0
  64. package/dist/sunbursttree-list-D6j5BxVQ.d.ts +303 -0
  65. package/dist/{tab-list-wC0xNz1b.d.ts → tab-list-Df371rqw.d.ts} +1 -1
  66. package/dist/tabs.d.ts +1 -1
  67. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +1 -1
  68. package/dist/titlebar.d.ts +1 -1
  69. package/dist/tree-CftHwtEb.js +146 -0
  70. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  71. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  72. package/dist/tree.d.ts +3 -3
  73. package/dist/tree.js +1 -1
  74. package/dist/types-LU1u0CB8.d.ts +87 -0
  75. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  76. package/dist/{vertical-list-B8FoU-pU.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  77. package/dist/vertical-list.d.ts +3 -3
  78. package/dist/vertical-list.js +1 -1789
  79. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  80. package/docs-user/README.md +3 -0
  81. package/docs-user/crumbs.md +27 -0
  82. package/docs-user/data-grid.md +77 -5
  83. package/docs-user/detail-list.md +54 -0
  84. package/docs-user/dock.md +54 -12
  85. package/docs-user/flat-list.md +27 -0
  86. package/docs-user/grid-list.md +64 -11
  87. package/docs-user/hyperbolic-tree.md +31 -4
  88. package/docs-user/icicle-tree.md +36 -0
  89. package/docs-user/index.json +17 -2
  90. package/docs-user/llms.txt +3 -0
  91. package/docs-user/miller.md +52 -21
  92. package/docs-user/multicol-list.md +50 -8
  93. package/docs-user/split-layout.md +45 -17
  94. package/docs-user/sunburst-tree.md +223 -0
  95. package/docs-user/tree.md +46 -7
  96. package/docs-user/treemap.md +32 -0
  97. package/docs-user/twosplit-layout.md +22 -0
  98. package/docs-user/user-catalog.md +1 -1
  99. package/docs-user/vertical-list.md +55 -7
  100. package/llms.txt +3 -0
  101. package/package.json +5 -1
package/dist/miller.js CHANGED
@@ -1,1816 +1,2 @@
1
- import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
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 };