@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.
Files changed (76) hide show
  1. package/bundle/index.d.ts +917 -44
  2. package/bundle/index.js +702 -696
  3. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  4. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  5. package/dist/crumbs.d.ts +2 -115
  6. package/dist/crumbs.js +1 -1179
  7. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
  9. package/dist/data-grid.d.ts +3 -3
  10. package/dist/data-grid.js +1 -1
  11. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  13. package/dist/detail-list.d.ts +4 -4
  14. package/dist/detail-list.js +1 -1
  15. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-JPgQSmLW.d.ts} +27 -4
  16. package/dist/flat-list.d.ts +2 -2
  17. package/dist/flat-list.js +123 -4
  18. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  19. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  20. package/dist/grid-list.d.ts +3 -3
  21. package/dist/grid-list.js +1 -1
  22. package/dist/incr-search.d.ts +1 -1
  23. package/dist/index-9ttcdwlL.d.ts +71 -0
  24. package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  25. package/dist/index-CYRc6Dah.d.ts +475 -0
  26. package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
  27. package/dist/index.d.ts +334 -23
  28. package/dist/index.js +2247 -867
  29. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  30. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  31. package/dist/miller-BFRfn0ve.js +2237 -0
  32. package/dist/miller.d.ts +4 -437
  33. package/dist/miller.js +1 -1815
  34. package/dist/pending-BGidHppL.js +91 -0
  35. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  36. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  37. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  38. package/dist/split-layout.d.ts +1 -1
  39. package/dist/split-layout.js +1 -1
  40. package/dist/sunburst-tree.d.ts +2 -2
  41. package/dist/sunburst-tree.js +1 -1
  42. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_VxOHlo.js} +298 -79
  43. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
  44. package/dist/tree-CftHwtEb.js +146 -0
  45. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  46. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  47. package/dist/tree.d.ts +3 -3
  48. package/dist/tree.js +1 -1
  49. package/dist/types-LU1u0CB8.d.ts +87 -0
  50. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  51. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  52. package/dist/vertical-list.d.ts +3 -3
  53. package/dist/vertical-list.js +1 -1789
  54. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  55. package/docs-user/README.md +2 -0
  56. package/docs-user/crumbs.md +27 -0
  57. package/docs-user/data-grid.md +66 -1
  58. package/docs-user/detail-list.md +54 -0
  59. package/docs-user/dock.md +54 -12
  60. package/docs-user/flat-list.md +27 -0
  61. package/docs-user/grid-list.md +64 -11
  62. package/docs-user/hyperbolic-tree.md +31 -4
  63. package/docs-user/icicle-tree.md +36 -0
  64. package/docs-user/index.json +12 -2
  65. package/docs-user/llms.txt +2 -0
  66. package/docs-user/miller.md +52 -21
  67. package/docs-user/multicol-list.md +50 -8
  68. package/docs-user/split-layout.md +45 -17
  69. package/docs-user/sunburst-tree.md +32 -2
  70. package/docs-user/tree.md +45 -6
  71. package/docs-user/treemap.md +32 -0
  72. package/docs-user/twosplit-layout.md +22 -0
  73. package/docs-user/user-catalog.md +1 -1
  74. package/docs-user/vertical-list.md +55 -7
  75. package/llms.txt +2 -0
  76. package/package.json +1 -1
@@ -0,0 +1,1993 @@
1
+ import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
2
+ import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
3
+ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
4
+ import { c as createElementListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-JIhwB49y.js";
5
+ import { i as animateListItemOut, r as animateListItemIn, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
6
+ import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-5L0Nj9tR.js";
7
+ import { l as keysToArray } from "./pending-BGidHppL.js";
8
+ import { a as restoreElementScroll, n as anchorFromRow, o as scrollToOffset, r as captureElementScroll, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
9
+ import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
10
+ import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
11
+ import "./editable-label.js";
12
+ import { LitElement, css, html, render } from "lit";
13
+ import { property } from "lit/decorators.js";
14
+ import { createCommandRegistry } from "@clinth/ui-commands";
15
+ import { ifDefined } from "lit/directives/if-defined.js";
16
+ //#region src/vertical-list/vertical-list-view-state.ts
17
+ /** Key → row over `rows`. Keyless rows are skipped; the first row with a key wins. */
18
+ function indexByKey(rows, getKey) {
19
+ const out = /* @__PURE__ */ new Map();
20
+ for (const row of rows) {
21
+ const key = getKey(row);
22
+ if (key !== void 0 && !out.has(key)) out.set(key, row);
23
+ }
24
+ return out;
25
+ }
26
+ /** Items of `selected` that `isPresent` reports have left the list. */
27
+ function departed(selected, isPresent) {
28
+ const out = [];
29
+ for (const item of selected) if (!isPresent(item)) out.push(item);
30
+ return out;
31
+ }
32
+ /**
33
+ * Carry a selection across a rebuild that replaced every row element: each
34
+ * previously-selected row maps to the new row with the same key, or is listed
35
+ * in `gone` so its key can be kept pending.
36
+ */
37
+ function rekeySelection(previous, anchorKey, rows, getKey) {
38
+ const previousByKey = /* @__PURE__ */ new Map();
39
+ const gone = [];
40
+ for (const el of previous) {
41
+ const key = getKey(el);
42
+ if (key === void 0) gone.push(el);
43
+ else previousByKey.set(key, el);
44
+ }
45
+ const live = [];
46
+ const matched = /* @__PURE__ */ new Set();
47
+ let anchor;
48
+ for (const row of rows) {
49
+ const key = getKey(row);
50
+ if (key === void 0) continue;
51
+ if (previousByKey.has(key) && !matched.has(key)) {
52
+ live.push(row);
53
+ matched.add(key);
54
+ }
55
+ if (key === anchorKey) anchor ??= row;
56
+ }
57
+ for (const [key, el] of previousByKey) if (!matched.has(key)) gone.push(el);
58
+ return {
59
+ live,
60
+ gone,
61
+ anchor
62
+ };
63
+ }
64
+ /**
65
+ * - a keyboard cursor whose row left the list (`departedKey`) goes pending,
66
+ * unless something is already pending
67
+ * - a pending key that now resolves is applied
68
+ */
69
+ function reconcileFocus(pending, departedKey, has) {
70
+ const next = pending ?? departedKey;
71
+ if (next !== void 0 && has(next)) return {
72
+ pending: void 0,
73
+ apply: next
74
+ };
75
+ return {
76
+ pending: next,
77
+ apply: void 0
78
+ };
79
+ }
80
+ /** Anchor for a virtualised list with fixed `stride` rows. */
81
+ function captureVirtualScroll(top, stride, count, keyAt) {
82
+ if (stride <= 0 || count === 0) return anchorFromRow(top, void 0, 0);
83
+ const index = Math.min(count - 1, Math.max(0, Math.floor(top / stride)));
84
+ return anchorFromRow(top, keyAt(index), index * stride);
85
+ }
86
+ /**
87
+ * Scroll offset a saved state asks for in a virtualised list, or `undefined`
88
+ * while its anchor key isn't in the list yet.
89
+ */
90
+ function virtualScrollTarget(scroll, stride, indexOf) {
91
+ if (!scroll.anchor) return scroll.top;
92
+ const index = indexOf(scroll.anchor.key);
93
+ return index < 0 ? void 0 : index * stride + scroll.anchor.offset;
94
+ }
95
+ //#endregion
96
+ //#region src/vertical-list/vertical-list.ts
97
+ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data-virtual-spacer])`;
98
+ /**
99
+ * Rows rendered above/below the viewport as scroll margin. Grid-list uses 2;
100
+ * vertical rows are shorter relative to the viewport, so 3.
101
+ */
102
+ const VIRTUAL_OVERSCAN = 3;
103
+ /**
104
+ * Layout for the per-item icon / badge wrappers `#decorateAdapterItem` adds.
105
+ *
106
+ * Applied as inline `cssText` rather than shadow CSS because `::slotted()` only
107
+ * matches the host's *direct* children — these spans live inside a slotted
108
+ * `<li>`, so a `::slotted()` rule could never reach them. The values still read
109
+ * the CSS custom properties, so they stay themeable from the outside.
110
+ */
111
+ const ITEM_ICON_EL_CSS = `flex:none;display:inline-flex;align-items:center;justify-content:center;overflow:hidden;margin-inline-end:var(--list-item-icon-gap,6px);width:var(--list-item-icon-size,1em);height:var(--list-item-icon-size,1em)`;
112
+ const ITEM_BADGE_EL_CSS = `flex:none;display:inline-flex;align-items:center;margin-inline-start:auto;padding-inline-start:var(--list-item-badge-gap,8px)`;
113
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
114
+ function sameKeys(a, b) {
115
+ if (a === void 0 || a.length !== b.length) return false;
116
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
117
+ return true;
118
+ }
119
+ let VerticalListElement = class VerticalListElement extends LitElement {
120
+ constructor(..._args) {
121
+ super(..._args);
122
+ this.selectionMode = `single`;
123
+ this.interactionMode = `standard`;
124
+ this.itemWrap = `stop`;
125
+ this.itemsDraggable = false;
126
+ this.keyProperty = ``;
127
+ this.virtualize = false;
128
+ this.itemHeight = 0;
129
+ this.itemsDroppable = false;
130
+ this.#customWrappers = /* @__PURE__ */ new Map();
131
+ this.#wrappedElements = /* @__PURE__ */ new Set();
132
+ this.#itemData = /* @__PURE__ */ new WeakMap();
133
+ this.#animatedItems = /* @__PURE__ */ new WeakSet();
134
+ this.#allItems = [];
135
+ this.#pool = document.createDocumentFragment();
136
+ this.#windowStart = 0;
137
+ this.#windowEnd = 0;
138
+ this.#pendingAnimate = [];
139
+ this.#suspendAdapterRebuild = false;
140
+ this.#scrollRaf = 0;
141
+ this.#lastViewportH = 0;
142
+ this.#virtualWarned = false;
143
+ this.#viewState = new ViewStateController(this, {
144
+ kind: `vertical-list`,
145
+ capture: () => this.getViewState()
146
+ });
147
+ this.#reconcilingViewState = false;
148
+ this.selection = new ListSelectionController({
149
+ getOrderedItems: () => this.#selectionOrder(),
150
+ isSelectable: (el) => this._isCheckable(el),
151
+ getModes: () => ({
152
+ selectionMode: this.selectionMode,
153
+ interactionMode: this.interactionMode
154
+ }),
155
+ getKey: keyForElement,
156
+ onChange: (selected, previous) => {
157
+ this._syncItemStates();
158
+ this.#viewState.notify();
159
+ this._dispatch(`list-select`, {
160
+ selected,
161
+ previous,
162
+ selectedKeys: this.selection.selectedKeys
163
+ });
164
+ }
165
+ });
166
+ this.tickled = ((self) => new TickledController(self, {
167
+ getItems: () => self.#selectionOrder().map((el) => ({
168
+ id: el,
169
+ enabled: !el.hasAttribute(`not-checked`)
170
+ })),
171
+ onTickle: (id) => {
172
+ if (self.tickled.inputMode === `keyboard`) {
173
+ if (!self.#viewState.isRestoring) self.#pendingFocus = void 0;
174
+ self.#viewState.notify();
175
+ }
176
+ if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
177
+ },
178
+ onStateChange: () => this._syncItemStates(),
179
+ get itemWrap() {
180
+ return self.itemWrap;
181
+ },
182
+ hoverDisabled: () => this.#dragSelect.isDragging || this.#nativeDrag.isDragging
183
+ }))(this);
184
+ this.#nativeDragging = false;
185
+ this.#postDragSuppressTimer = 0;
186
+ this.#nativeDrag = new ListNativeDragController(this, {
187
+ source: `vertical-list`,
188
+ isEnabled: () => this.itemsDraggable && this.selectionMode !== `none` && this._getVisibleItems().length > 0,
189
+ resolveEventToKey: (target) => {
190
+ if (!(target instanceof Element)) return void 0;
191
+ const item = target.closest?.(ITEM_SELECTOR);
192
+ if (!item || !this.contains(item) || this.#wrappedElements.has(item)) return void 0;
193
+ return keyForElement(item);
194
+ },
195
+ prepareSelection: (originKey) => this.#prepareNativeDrag(originKey),
196
+ getMetrics: () => ({}),
197
+ getRenderer: () => this.dragPreviewRenderer,
198
+ getDragPreviewSourceResolver: () => this.dragPreviewSource,
199
+ dispatch: (detail) => this._dispatchCancelable(`list-native-drag-start`, detail),
200
+ onActiveChange: (active, originKey) => {
201
+ this.#nativeDragging = active;
202
+ this.toggleAttribute(`data-native-dragging`, active);
203
+ for (const el of this._getVisibleItems()) el.toggleAttribute(`data-drag-origin`, active && originKey !== void 0 && keyForElement(el) === originKey);
204
+ if (active) {
205
+ const count = this.selection.selected.size;
206
+ this.setAttribute(`aria-label`, count <= 1 ? `Dragging 1 item` : `Dragging ${count} items`);
207
+ } else this.removeAttribute(`aria-label`);
208
+ this.requestUpdate();
209
+ },
210
+ onEnd: () => {
211
+ this.#nativeDragging = false;
212
+ this.toggleAttribute(`data-native-dragging`, false);
213
+ this.removeAttribute(`aria-label`);
214
+ for (const el of this._getVisibleItems()) el.removeAttribute(`data-drag-origin`);
215
+ if (this.#virtualActive()) for (const el of this.#allItems) el.removeAttribute(`data-drag-origin`);
216
+ this.#startPostDragSuppressTimer();
217
+ },
218
+ syncLightDomDraggable: () => this._syncNativeDraggable()
219
+ });
220
+ this.#dragSelect = new ListDragSelectController(this, {
221
+ getSurface: () => this.renderRoot.querySelector(`.container`),
222
+ isEnabled: () => !this.#virtualActive() && this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
223
+ hitTest: (rect) => this.#marqueeHitTest(rect),
224
+ getBaseSelection: () => this.selection.selected,
225
+ onPreview: (preview, rect) => {
226
+ this.#marqueeRect = rect;
227
+ for (const el of this._getVisibleItems()) el.toggleAttribute(`data-marquee`, preview.has(el));
228
+ this.requestUpdate();
229
+ },
230
+ onCommit: (finalSet) => {
231
+ this.selection.applyDragSelection(finalSet);
232
+ for (const el of this._getVisibleItems()) el.removeAttribute(`data-marquee`);
233
+ this.#marqueeRect = void 0;
234
+ this.requestUpdate();
235
+ },
236
+ autoScroll: (_dx, dy) => {
237
+ const list = this.renderRoot.querySelector(`ul`);
238
+ if (list) list.scrollTop += dy;
239
+ }
240
+ });
241
+ this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
242
+ itemSelector: ITEM_SELECTOR,
243
+ getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
244
+ highlightKey: `vertical-list-search`,
245
+ getAllItems: () => this.#selectionOrder(),
246
+ observeContainer: false
247
+ }) });
248
+ this.#onScroll = () => {
249
+ if (!this.#viewState.pendingScroll) this.#viewState.notify();
250
+ if (this.#scrollRaf !== 0) return;
251
+ this.#scrollRaf = requestAnimationFrame(() => {
252
+ this.#scrollRaf = 0;
253
+ this.#syncVirtualWindow();
254
+ this.#emitViewportChangeIfChanged();
255
+ });
256
+ };
257
+ this._onPointerOver = (e) => {
258
+ const item = this._findListItem(e.composedPath());
259
+ if (item) this.tickled.handlePointerEnter(item);
260
+ };
261
+ this._onPointerLeave = (e) => {
262
+ this.tickled.handlePointerLeave(e);
263
+ };
264
+ this._onKeyDown = (e) => {
265
+ if (this.search.isOpen || this.#isEditEvent(e)) return;
266
+ if (e.key === `ContextMenu` || e.shiftKey && e.key === `F10`) {
267
+ const id = this.tickled.tickledId;
268
+ if (id) {
269
+ e.preventDefault();
270
+ const rect = id.getBoundingClientRect();
271
+ this.#requestContextMenu(id, rect.left, rect.bottom);
272
+ }
273
+ return;
274
+ }
275
+ if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
276
+ if (this.selection.canSelectAll()) {
277
+ e.preventDefault();
278
+ this.selection.selectAll();
279
+ }
280
+ return;
281
+ }
282
+ switch (e.key) {
283
+ case `ArrowDown`:
284
+ e.preventDefault();
285
+ this.tickled.handleKeyboardNavigation(`next`);
286
+ this.#revealCursor();
287
+ if (e.shiftKey && this.interactionMode === `vscode`) {
288
+ const cur = this.tickled.tickledId;
289
+ if (cur) this.selection.rangeTo(cur);
290
+ }
291
+ break;
292
+ case `ArrowUp`:
293
+ e.preventDefault();
294
+ this.tickled.handleKeyboardNavigation(`previous`);
295
+ this.#revealCursor();
296
+ if (e.shiftKey && this.interactionMode === `vscode`) {
297
+ const cur = this.tickled.tickledId;
298
+ if (cur) this.selection.rangeTo(cur);
299
+ }
300
+ break;
301
+ case `Home`:
302
+ e.preventDefault();
303
+ this.tickled.handleKeyboardNavigation(`first`);
304
+ this.#revealCursor();
305
+ break;
306
+ case `End`:
307
+ e.preventDefault();
308
+ this.tickled.handleKeyboardNavigation(`last`);
309
+ this.#revealCursor();
310
+ break;
311
+ case `PageDown`:
312
+ e.preventDefault();
313
+ this.#handlePage(1);
314
+ break;
315
+ case `PageUp`:
316
+ e.preventDefault();
317
+ this.#handlePage(-1);
318
+ break;
319
+ case `Enter`: {
320
+ const id = this.tickled.tickledId;
321
+ if (id) {
322
+ e.preventDefault();
323
+ this._dispatch(`list-item-click`, { item: id });
324
+ this._activateItem(id);
325
+ }
326
+ break;
327
+ }
328
+ case ` `: {
329
+ const id = this.tickled.tickledId;
330
+ if (id) {
331
+ e.preventDefault();
332
+ this.selection.handleSpace(id);
333
+ }
334
+ break;
335
+ }
336
+ case `Escape`: this.selection.clear();
337
+ }
338
+ };
339
+ this._onClick = (e) => {
340
+ if (this.#isEditEvent(e)) return;
341
+ if (this.#dragSelect.isDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
342
+ const item = this._findListItem(e.composedPath());
343
+ if (!item) return;
344
+ this._dispatch(`list-item-click`, { item });
345
+ this.selection.handleClick(item, e);
346
+ };
347
+ this._onDblClick = (e) => {
348
+ if (this.#isEditEvent(e)) return;
349
+ const item = this._findListItem(e.composedPath());
350
+ if (item) this._activateItem(item);
351
+ };
352
+ this._onDragOver = (e) => {
353
+ const dt = e.dataTransfer;
354
+ if (!this.itemsDroppable || !dt || this.#isEditEvent(e)) return;
355
+ const item = this._findListItem(e.composedPath());
356
+ if (this.canDrop && !this.canDrop(item, dt)) {
357
+ this.#clearDropTarget();
358
+ return;
359
+ }
360
+ if (this.#dragOver && this.#dragOver.item !== item) this.#clearDropTarget();
361
+ this.#dragOver = { item };
362
+ if (!!this._dispatchCancelable(`list-drag-over`, {
363
+ key: item ? keyForElement(item) : void 0,
364
+ item,
365
+ dataTransfer: dt
366
+ })) {
367
+ this.#highlightDropTarget(void 0, false);
368
+ return;
369
+ }
370
+ e.preventDefault();
371
+ if (dt.dropEffect === `none`) dt.dropEffect = `copy`;
372
+ this.#highlightDropTarget(item, true);
373
+ };
374
+ this._onDragLeave = (e) => {
375
+ const next = e.relatedTarget;
376
+ if (next instanceof Node && this.contains(next)) return;
377
+ this.#clearDropTarget();
378
+ };
379
+ this._onDrop = (e) => {
380
+ const dt = e.dataTransfer;
381
+ if (!this.itemsDroppable || !dt || this.#isEditEvent(e)) return;
382
+ const item = this._findListItem(e.composedPath());
383
+ if (this.canDrop && !this.canDrop(item, dt)) {
384
+ this.#clearDropTarget();
385
+ return;
386
+ }
387
+ e.preventDefault();
388
+ this.#dragOver = void 0;
389
+ this.#highlightDropTarget(void 0, false);
390
+ this._dispatch(`list-drop`, {
391
+ key: item ? keyForElement(item) : void 0,
392
+ item,
393
+ dataTransfer: dt
394
+ });
395
+ };
396
+ this._onContextMenu = (e) => {
397
+ if (this.#isEditEvent(e)) return;
398
+ const item = this._findListItem(e.composedPath());
399
+ if (!item) return;
400
+ if (!this.#requestContextMenu(item, e.clientX, e.clientY)) e.preventDefault();
401
+ };
402
+ }
403
+ #customWrappers;
404
+ #wrappedElements;
405
+ /** Source object behind each adapter-built row (`items` mode). */
406
+ #itemData;
407
+ /** Watches for items added to the light DOM so they can be animated in. */
408
+ #itemObserver;
409
+ /** Items that have already played (or been skipped for) their enter animation. */
410
+ #animatedItems;
411
+ /**
412
+ * Global row order while virtualized — the source of truth both modes
413
+ * write to. Holds canonical rows (checked-mode wrappers, not inners).
414
+ * Non-virtualized mode ignores it and reads the DOM directly.
415
+ */
416
+ #allItems;
417
+ /** Off-DOM pool for recycled offscreen rows. `querySelectorAll` works on it. */
418
+ #pool;
419
+ /** Current rendered window as registry indices `[start, end)`. */
420
+ #windowStart;
421
+ #windowEnd;
422
+ /** Window rows queued for their enter animation after placement. */
423
+ #pendingAnimate;
424
+ /** Skip one adapter rebuild (updateItem's write-through to `items`). */
425
+ #suspendAdapterRebuild;
426
+ #scrollRaf;
427
+ #resizeObserver;
428
+ /** Key sets as of the last viewport-change dispatch, for dedupe. */
429
+ #lastViewportRenderedKeys;
430
+ #lastViewportVisibleKeys;
431
+ #topSpacer;
432
+ #bottomSpacer;
433
+ /** Last measured viewport height — kept so a 0 read can't collapse the window. */
434
+ #lastViewportH;
435
+ #virtualWarned;
436
+ #viewState;
437
+ /** Saved keyboard-cursor key whose row isn't in the list (yet). */
438
+ #pendingFocus;
439
+ #reconcilingViewState;
440
+ #nativeDragging;
441
+ #postDragSuppressTimer;
442
+ #nativeDrag;
443
+ #prepareNativeDrag(originKey) {
444
+ const originEl = this.#itemForKey(originKey);
445
+ if (!originEl || !this._isCheckable(originEl)) return void 0;
446
+ if (!this.selection.selected.has(originEl)) this.selection.replace(originEl);
447
+ const ordered = this.#selectionOrder();
448
+ const selected = [];
449
+ ordered.forEach((el) => {
450
+ if (!this.selection.selected.has(el)) return;
451
+ const idx = this.#registryIndexOf(el);
452
+ selected.push({
453
+ key: keyForElement(el) ?? originKey,
454
+ index: idx,
455
+ item: el,
456
+ element: el
457
+ });
458
+ });
459
+ const origin = selected.find((entry) => entry.key === originKey);
460
+ if (!origin) return void 0;
461
+ return {
462
+ origin,
463
+ selected
464
+ };
465
+ }
466
+ _syncNativeDraggable() {
467
+ if (!this.isConnected) return;
468
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
469
+ if (this.#wrappedElements.has(el)) continue;
470
+ if (el instanceof HTMLElement) el.draggable = this.itemsDraggable && this._isCheckable(el);
471
+ }
472
+ }
473
+ #startPostDragSuppressTimer() {
474
+ if (this.#postDragSuppressTimer) clearTimeout(this.#postDragSuppressTimer);
475
+ this.#postDragSuppressTimer = window.setTimeout(() => {
476
+ this.#postDragSuppressTimer = 0;
477
+ }, 0);
478
+ }
479
+ /** Live drag-select preview rect (surface-relative); the covered items carry `data-marquee`. */
480
+ #marqueeRect;
481
+ #dragSelect;
482
+ /** Collect visible items whose surface-relative box intersects `rect`. */
483
+ #marqueeHitTest(rect) {
484
+ const surface = this.renderRoot.querySelector(`.container`);
485
+ if (!surface) return [];
486
+ const box = surface.getBoundingClientRect();
487
+ const rx0 = rect.x;
488
+ const ry0 = rect.y;
489
+ const rx1 = rect.x + rect.width;
490
+ const ry1 = rect.y + rect.height;
491
+ const out = [];
492
+ for (const el of this._getVisibleItems()) {
493
+ const r = el.getBoundingClientRect();
494
+ const left = r.left - box.left;
495
+ const top = r.top - box.top;
496
+ if (left <= rx1 && left + r.width >= rx0 && top <= ry1 && top + r.height >= ry0) out.push(el);
497
+ }
498
+ return out;
499
+ }
500
+ #marqueeStyle(rect) {
501
+ return `left:${rect.x}px;top:${rect.y}px;width:${Math.max(0, rect.width)}px;height:${Math.max(0, rect.height)}px`;
502
+ }
503
+ #searchRegistry;
504
+ #unregisterSearchCommands;
505
+ /** Command registry carrying this list's `list.search.*` commands. */
506
+ get registry() {
507
+ return this.#searchRegistry;
508
+ }
509
+ #effectiveSearchModes() {
510
+ const raw = this.searchModes?.trim();
511
+ let modes;
512
+ if (raw === void 0) modes = [...LIST_SEARCH_MODES];
513
+ else {
514
+ const requested = new Set(raw.split(/\s+/));
515
+ modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
516
+ }
517
+ return this.#virtualActive() ? modes.filter((m) => m !== `filter`) : modes;
518
+ }
519
+ #syncSearchCommands() {
520
+ if (!this.#searchRegistry) return;
521
+ this.search.setModes(this.#effectiveSearchModes());
522
+ this.#unregisterSearchCommands?.();
523
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
524
+ }
525
+ connectedCallback() {
526
+ super.connectedCallback();
527
+ if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
528
+ this.addEventListener(`pointerover`, this._onPointerOver);
529
+ this.addEventListener(`pointerleave`, this._onPointerLeave);
530
+ this.addEventListener(`keydown`, this._onKeyDown);
531
+ this.addEventListener(`click`, this._onClick);
532
+ this.addEventListener(`dblclick`, this._onDblClick);
533
+ this.addEventListener(`dragover`, this._onDragOver);
534
+ this.addEventListener(`dragleave`, this._onDragLeave);
535
+ this.addEventListener(`drop`, this._onDrop);
536
+ this.addEventListener(`contextmenu`, this._onContextMenu);
537
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
538
+ this.#itemObserver ??= new MutationObserver((records) => this.#onItemsMutated(records));
539
+ this.#itemObserver.observe(this, { childList: true });
540
+ this.#searchRegistry ??= createCommandRegistry();
541
+ this.#syncSearchCommands();
542
+ this.#ensureResizeObserved();
543
+ }
544
+ disconnectedCallback() {
545
+ super.disconnectedCallback();
546
+ this.removeEventListener(`pointerover`, this._onPointerOver);
547
+ this.removeEventListener(`pointerleave`, this._onPointerLeave);
548
+ this.removeEventListener(`keydown`, this._onKeyDown);
549
+ this.removeEventListener(`click`, this._onClick);
550
+ this.removeEventListener(`dblclick`, this._onDblClick);
551
+ this.removeEventListener(`dragover`, this._onDragOver);
552
+ this.removeEventListener(`dragleave`, this._onDragLeave);
553
+ this.removeEventListener(`drop`, this._onDrop);
554
+ this.removeEventListener(`contextmenu`, this._onContextMenu);
555
+ this.#clearDropTarget();
556
+ this.#edit = void 0;
557
+ this.#itemObserver?.disconnect();
558
+ this.#resizeObserver?.disconnect();
559
+ this.#resizeObserver = void 0;
560
+ if (this.#scrollRaf !== 0) {
561
+ cancelAnimationFrame(this.#scrollRaf);
562
+ this.#scrollRaf = 0;
563
+ }
564
+ this.#unregisterSearchCommands?.();
565
+ this.#unregisterSearchCommands = void 0;
566
+ this.#searchRegistry = void 0;
567
+ if (this.#postDragSuppressTimer) {
568
+ clearTimeout(this.#postDragSuppressTimer);
569
+ this.#postDragSuppressTimer = 0;
570
+ }
571
+ }
572
+ /**
573
+ * The shadow scroll list only exists after the first render; start
574
+ * watching it so resizes move the window (virtualized) and the visible
575
+ * set (plain) too.
576
+ */
577
+ firstUpdated() {
578
+ this.#ensureResizeObserved();
579
+ this.#viewState.observeScrollTarget(this);
580
+ this.#reconcileViewState();
581
+ }
582
+ /**
583
+ * Animate in items appended to the light DOM (however they got there —
584
+ * {@link addItem}, direct `appendChild`, a framework binding). A bulk populate
585
+ * (roughly everything at once) is left alone, as are the wrapper `<li>`s the
586
+ * checked-mode plumbing shuffles around.
587
+ */
588
+ #onItemsMutated(records) {
589
+ if (!this.isConnected) return;
590
+ if (this.#virtualActive()) {
591
+ this.#syncVirtualWindow(this.#reconcileRecords(records));
592
+ this.#scheduleViewportChangeEmit();
593
+ return;
594
+ }
595
+ const fresh = [];
596
+ for (const record of records) for (const node of record.addedNodes) {
597
+ if (!(node instanceof HTMLElement) || node.parentElement !== this) continue;
598
+ if (!node.matches(ITEM_SELECTOR)) continue;
599
+ if (this.#animatedItems.has(node) || this.#customWrappers.has(node) || this.#wrappedElements.has(node)) continue;
600
+ this.#animatedItems.add(node);
601
+ fresh.push(node);
602
+ }
603
+ this.#scheduleViewportChangeEmit();
604
+ if (records.some((r) => r.removedNodes.length > 0)) this.#demoteDeparted(departed(this.selection.selected, (el) => el.parentNode === this || this.#wrappedElements.has(el)));
605
+ this.#reconcileViewState();
606
+ if (fresh.length === 0) return;
607
+ if (this.#isBulkPopulate(fresh.length)) return;
608
+ for (const el of fresh) animateListItemIn(el);
609
+ }
610
+ /**
611
+ * Whether `freshCount` newly-added rows amount to a bulk populate (first
612
+ * render / wholesale replace) rather than an incremental add — the single
613
+ * threshold both the virtual and non-virtual mutation-observer paths use
614
+ * to decide whether to skip the enter animation.
615
+ */
616
+ #isBulkPopulate(freshCount) {
617
+ return freshCount >= this.querySelectorAll(ITEM_SELECTOR).length;
618
+ }
619
+ updated(changed) {
620
+ super.updated(changed);
621
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
622
+ if (changed.has(`virtualize`) || changed.has(`itemHeight`)) {
623
+ const wasActive = (changed.has(`virtualize`) ? changed.get(`virtualize`) : this.virtualize) === true && ((changed.has(`itemHeight`) ? changed.get(`itemHeight`) : this.itemHeight) ?? 0) > 0;
624
+ if (this.#virtualActive()) {
625
+ if (!wasActive) {
626
+ this.#pool = document.createDocumentFragment();
627
+ this.#allItems = [...this.querySelectorAll(ITEM_SELECTOR)];
628
+ for (const el of this.#allItems) this.#animatedItems.add(el);
629
+ this.#pendingAnimate = [];
630
+ }
631
+ this.#syncVirtualWindow(true);
632
+ } else if (wasActive) this.#teardownVirtualWindow();
633
+ else if (this.virtualize && !this.#virtualWarned) {
634
+ this.#virtualWarned = true;
635
+ console.warn(`ixfx-vertical-list: virtualize needs item-height > 0 — running non-virtualized.`);
636
+ }
637
+ this.#syncSearchCommands();
638
+ }
639
+ if ((changed.has(`items`) || changed.has(`formatter`) || changed.has(`keyProperty`) || changed.has(`icon`) || changed.has(`iconColor`) || changed.has(`textColor`) || changed.has(`badge`)) && this.items !== void 0) {
640
+ if (this.#suspendAdapterRebuild) this.#suspendAdapterRebuild = false;
641
+ else if (this.#virtualActive()) this.#rebuildAdapterVirtual();
642
+ else this.#rebuildFromAdapter();
643
+ }
644
+ if (changed.has(`interactionMode`)) {
645
+ this.selection.clearAnchor();
646
+ const previousSelected = this.selection.selected;
647
+ const savedWrappers = changed.get(`interactionMode`) === `checked` ? new Map(this.#customWrappers) : null;
648
+ this._syncCheckboxes();
649
+ if (previousSelected.size > 0) {
650
+ if (savedWrappers && savedWrappers.size > 0) this.selection.remap((el) => savedWrappers.get(el) ?? el);
651
+ else if (this.#customWrappers.size > 0) {
652
+ const innerToWrapper = new Map([...this.#customWrappers].map(([w, i]) => [i, w]));
653
+ this.selection.remap((el) => innerToWrapper.get(el) ?? el);
654
+ }
655
+ }
656
+ this._syncItemStates();
657
+ } else if (changed.has(`checkboxRenderer`) && this.interactionMode === `checked`) this._syncCheckboxes();
658
+ if (changed.has(`itemsDraggable`) || changed.has(`interactionMode`)) {
659
+ this._syncNativeDraggable();
660
+ this.#nativeDrag.refreshDraggable();
661
+ }
662
+ }
663
+ /** The most recently selected item, or `undefined` if nothing is selected. */
664
+ get selectedItem() {
665
+ return [...this.selection.selected].at(-1);
666
+ }
667
+ /** The full current selection set (read-only). */
668
+ get selectedItems() {
669
+ return this.selection.selected;
670
+ }
671
+ addItem(item, options) {
672
+ let el;
673
+ if (typeof item === `string`) {
674
+ const li = document.createElement(`li`);
675
+ li.textContent = item;
676
+ el = li;
677
+ } else el = item;
678
+ if (options?.notChecked) el.setAttribute(`not-checked`, ``);
679
+ this.appendChild(el);
680
+ if (this.#virtualActive()) {
681
+ this.#syncVirtualWindow(true);
682
+ return;
683
+ }
684
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
685
+ this.search.refresh();
686
+ this.#reconcileViewState();
687
+ }
688
+ /**
689
+ * Collapse + fade `item` out, then detach it. The rows below slide up to
690
+ * close the gap. Resolves once the element has actually been removed.
691
+ * Under `prefers-reduced-motion: reduce` it's removed immediately.
692
+ * A pooled (offscreen) row under virtualization is removed immediately.
693
+ */
694
+ removeItem(item) {
695
+ this.#demoteDeparted([item]);
696
+ if (this.#virtualActive()) return (item.parentNode !== this ? Object.assign(Promise.resolve(), { animation: void 0 }) : animateListItemOut(item)).then(() => {
697
+ const idx = this.#registryIndexOf(item);
698
+ if (idx >= 0) this.#allItems.splice(idx, 1);
699
+ this.#cleanupWrapperRefs(item);
700
+ item.remove();
701
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
702
+ this.#syncVirtualWindow(true);
703
+ });
704
+ return animateListItemOut(item).then(() => {
705
+ item.remove();
706
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
707
+ });
708
+ }
709
+ /** {@link removeItem} for several items at once. */
710
+ removeItems(items) {
711
+ return Promise.all([...items].map((item) => this.removeItem(item))).then(() => void 0);
712
+ }
713
+ clearItems() {
714
+ this.#customWrappers.clear();
715
+ if (this.#virtualActive()) {
716
+ this.#allItems = [];
717
+ this.#pool = document.createDocumentFragment();
718
+ this.#pendingAnimate = [];
719
+ }
720
+ this.replaceChildren();
721
+ this.selection.reset();
722
+ this._syncItemStates();
723
+ this.search.refresh();
724
+ if (this.#virtualActive()) this.#syncVirtualWindow(true);
725
+ else this.#reconcileViewState();
726
+ }
727
+ /**
728
+ * Replace the entire item list in one call.
729
+ * Equivalent to `clearItems()` followed by `addItem()` for each element.
730
+ * Preserves `interactionMode` checkbox wrapping.
731
+ */
732
+ setItems(items) {
733
+ if (this.#virtualActive()) {
734
+ this.#customWrappers.clear();
735
+ this.#pool = document.createDocumentFragment();
736
+ this.#pendingAnimate = [];
737
+ this.#allItems = [...items];
738
+ for (const el of this.#allItems) this.#animatedItems.add(el);
739
+ this.selection.reset();
740
+ this.search.refresh();
741
+ this.#syncVirtualWindow(true);
742
+ return;
743
+ }
744
+ for (const item of items) this.appendChild(item);
745
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
746
+ this.search.refresh();
747
+ this.#reconcileViewState();
748
+ }
749
+ /**
750
+ * Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
751
+ * forward commands to all sub-components. Routes through the command registry
752
+ * (so `list.search.*` and any other registered commands work).
753
+ */
754
+ dispatch(commandId, args) {
755
+ this.#searchRegistry?.invoke(commandId, args);
756
+ }
757
+ select(item) {
758
+ this.selection.replace(item);
759
+ }
760
+ selectMany(items) {
761
+ this.selection.selectMany(items);
762
+ }
763
+ deselect(item) {
764
+ this.selection.deselect(item);
765
+ }
766
+ clearSelection() {
767
+ this.selection.clear();
768
+ }
769
+ selectAll() {
770
+ if (this.selection.canSelectAll()) this.selection.selectAll();
771
+ }
772
+ /** The selection as stable `data-key` / `data-value` strings. */
773
+ get selectedKeys() {
774
+ return this.selection.selectedKeys;
775
+ }
776
+ /** The key of the item under the cursor, or `undefined`. */
777
+ get tickledKey() {
778
+ const id = this.tickled.tickledId;
779
+ return id ? keyForElement(id) : void 0;
780
+ }
781
+ /** Replace the selection with the items carrying these keys (unknown keys ignored). */
782
+ selectKeys(keys) {
783
+ this.selection.selectKeys(keys, (key) => this.#itemForKey(key));
784
+ }
785
+ /** Add items by key to the selection (multi-select modes only). */
786
+ addSelectedKeys(keys) {
787
+ this.selection.addKeys(keys, (key) => this.#itemForKey(key));
788
+ }
789
+ /** Remove items by key from the selection. */
790
+ removeSelectedKeys(keys) {
791
+ this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
792
+ }
793
+ /**
794
+ * Move the cursor to the item with this key and scroll it into view.
795
+ * Under virtualization the key's index is scrolled into the rendered
796
+ * window first (a no-op for unknown keys).
797
+ */
798
+ focusKey(key) {
799
+ if (this.#virtualActive()) {
800
+ const index = this.#allItems.findIndex((el) => keyForElement(el) === key);
801
+ if (index < 0) return;
802
+ const ul = this.renderRoot.querySelector(`ul`);
803
+ if (ul) {
804
+ const stride = this.itemHeight;
805
+ const viewportH = ul.clientHeight;
806
+ const top = index * stride;
807
+ const bottom = (index + 1) * stride;
808
+ const vt = ul.scrollTop;
809
+ const vh = viewportH;
810
+ let next = vt;
811
+ if (top < vt) next = top;
812
+ else if (bottom > vt + vh) next = bottom - vh;
813
+ if (next !== vt) ul.scrollTop = next;
814
+ this.#syncVirtualWindow(true);
815
+ }
816
+ }
817
+ const el = this.#itemForKey(key);
818
+ if (!el || el.parentNode !== this) return;
819
+ this.#pendingFocus = void 0;
820
+ this.tickled.focus(el);
821
+ el.scrollIntoView({
822
+ block: `nearest`,
823
+ inline: `nearest`
824
+ });
825
+ }
826
+ #revealCursor() {
827
+ const el = this.tickled.tickledId;
828
+ const key = el ? keyForElement(el) : void 0;
829
+ if (key !== void 0) this.focusKey(key);
830
+ }
831
+ #pageSize() {
832
+ const ul = this.#scrollList();
833
+ if (!ul) return 1;
834
+ const viewportH = ul.clientHeight;
835
+ if (this.#virtualActive()) return Math.max(1, Math.floor(viewportH / this.itemHeight));
836
+ const first = this.querySelector(`li[data-key]`);
837
+ if (!first) return 1;
838
+ const firstH = first.offsetHeight || this.itemHeight;
839
+ return Math.max(1, Math.floor(viewportH / firstH));
840
+ }
841
+ /** Move the cursor one page up (-1) or down (1) across the whole registry. */
842
+ #handlePage(direction) {
843
+ const page = this.#pageSize();
844
+ const items = this.#selectionOrder().filter((el) => !el.hasAttribute(`not-checked`));
845
+ if (items.length === 0) return;
846
+ const currentKey = this.tickled.tickledId ? keyForElement(this.tickled.tickledId) : void 0;
847
+ const currentIdx = currentKey !== void 0 ? items.findIndex((i) => keyForElement(i) === currentKey) : -1;
848
+ const fallback = direction === 1 ? page - 1 : 0;
849
+ const nextIdx = currentIdx < 0 ? fallback : currentIdx + direction * page;
850
+ this.tickled.handleKeyboardNavigation(items[Math.min(items.length - 1, Math.max(0, nextIdx))]);
851
+ this.#revealCursor();
852
+ }
853
+ #itemForKey(key) {
854
+ if (this.#virtualActive()) return this.#allItems.find((el) => keyForElement(el) === key);
855
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
856
+ if (this.#wrappedElements.has(el)) continue;
857
+ if (keyForElement(el) === key) return el;
858
+ }
859
+ }
860
+ /** The item carrying this `data-key`, or `undefined`. */
861
+ getItemByKey(key) {
862
+ return this.#itemForKey(key);
863
+ }
864
+ /**
865
+ * Snapshot selection (the checked set in `checked` mode), keyboard cursor
866
+ * and scroll as plain JSON keyed by `data-key` / `data-value`. Includes
867
+ * pending state for keys not currently in the list.
868
+ */
869
+ getViewState(options) {
870
+ const withPending = options?.includePending ?? true;
871
+ const focus = this.tickled.inputMode === `keyboard` ? this.tickledKey : void 0;
872
+ return this.#viewState.shape({
873
+ selected: keysToArray(this.selection.selectedKeys, withPending ? this.selection.pendingKeys : []),
874
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0),
875
+ scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? this.#captureScroll()
876
+ }, options);
877
+ }
878
+ /**
879
+ * Apply saved view state. Fields present replace the current ones; keys
880
+ * not in the list yet are applied as matching rows appear.
881
+ */
882
+ setViewState(state) {
883
+ if (!this.#viewState.accepts(state)) return;
884
+ this.#viewState.restoring(() => {
885
+ if (state.selected) {
886
+ const index = this.#keyIndex();
887
+ this.selection.restoreKeys(state.selected, (key) => index.get(key));
888
+ }
889
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
890
+ if (state.scroll) this.#viewState.pendingScroll = state.scroll;
891
+ this.#viewState.notify();
892
+ });
893
+ this.#reconcileViewState();
894
+ }
895
+ clearPendingViewState() {
896
+ this.selection.clearPending();
897
+ this.#pendingFocus = void 0;
898
+ this.#viewState.pendingScroll = void 0;
899
+ }
900
+ /** Rows by key — the registry while virtualized, attached canonical rows otherwise. */
901
+ #keyIndex() {
902
+ if (this.#virtualActive()) return indexByKey(this.#allItems, keyForElement);
903
+ return indexByKey([...this.querySelectorAll(ITEM_SELECTOR)].filter((el) => !this.#wrappedElements.has(el)), keyForElement);
904
+ }
905
+ /** Whether a canonical row is still in the list (attached, or in the virtual registry). */
906
+ #isPresent(el) {
907
+ if (this.#virtualActive()) return this.#registryIndexOf(el) >= 0;
908
+ return el.parentNode === this || this.#wrappedElements.has(el);
909
+ }
910
+ /** Rows that left the data: deselected silently, keys kept pending. */
911
+ #demoteDeparted(items) {
912
+ if (items.length === 0 || !this.selection.demote(items)) return;
913
+ this._syncItemStates();
914
+ this.#viewState.notify(`data`);
915
+ }
916
+ /**
917
+ * After any item change: apply pending selection / cursor / scroll to rows
918
+ * that are now present, and send a departed keyboard cursor pending.
919
+ */
920
+ #reconcileViewState() {
921
+ if (this.#reconcilingViewState) return;
922
+ this.#reconcilingViewState = true;
923
+ try {
924
+ this.#viewState.restoring(() => {
925
+ let index;
926
+ const lookup = (key) => (index ??= this.#keyIndex()).get(key);
927
+ this.selection.resolvePending(lookup);
928
+ const cursor = this.tickled.tickledId;
929
+ const gone = cursor !== void 0 && !this.#isPresent(cursor);
930
+ if (this.#pendingFocus === void 0 && !gone) return;
931
+ const departedKey = gone && this.tickled.inputMode === `keyboard` ? keyForElement(cursor) : void 0;
932
+ if (gone) this.tickled.clearTickled();
933
+ const focus = reconcileFocus(this.#pendingFocus, departedKey, (key) => lookup(key) !== void 0);
934
+ this.#pendingFocus = focus.pending;
935
+ if (focus.apply !== void 0) {
936
+ this.tickled.setInputMode(`keyboard`);
937
+ this.tickled.setTickled(lookup(focus.apply));
938
+ this.#viewState.notify();
939
+ }
940
+ });
941
+ this.#viewState.applyPendingScroll((scroll) => this.#applyScroll(scroll));
942
+ } finally {
943
+ this.#reconcilingViewState = false;
944
+ }
945
+ }
946
+ #captureScroll() {
947
+ const ul = this.#scrollList();
948
+ if (!ul) return void 0;
949
+ if (this.#virtualActive()) return captureVirtualScroll(ul.scrollTop, this.itemHeight, this.#allItems.length, (i) => keyForElement(this.#allItems[i]));
950
+ return captureElementScroll(ul, this._getVisibleItems(), keyForElement);
951
+ }
952
+ /** Returns `true` once the saved position is fully reached. */
953
+ #applyScroll(scroll) {
954
+ const ul = this.#scrollList();
955
+ if (!ul || ul.clientHeight === 0) return false;
956
+ if (this.#virtualActive()) {
957
+ const target = virtualScrollTarget(scroll, this.itemHeight, (key) => this.#allItems.findIndex((el) => keyForElement(el) === key));
958
+ if (target === void 0) return false;
959
+ const max = Math.max(0, this.#allItems.length * this.itemHeight - this.#measureViewport(ul));
960
+ ul.scrollTop = Math.max(0, Math.min(target, max));
961
+ this.#syncVirtualWindow();
962
+ return target <= max + 1;
963
+ }
964
+ if (!scroll.anchor) return scrollToOffset(ul, scroll.top);
965
+ const el = this.#keyIndex().get(scroll.anchor.key);
966
+ if (!el || el.hidden) return false;
967
+ return restoreElementScroll(ul, el, scroll.anchor.offset);
968
+ }
969
+ /**
970
+ * Keys of items currently intersecting the viewport — for prioritising
971
+ * viewport-first work (e.g. metadata/thumbnail fetches). A rect-intersection
972
+ * test against the scroll container over the already-created rows (the same
973
+ * cheap trick as `ixfx-detail-list.visibleKeys()`); hidden (filtered-out)
974
+ * and keyless items are omitted.
975
+ */
976
+ visibleKeys() {
977
+ const container = this.renderRoot.querySelector(`ul`);
978
+ if (!container) return [];
979
+ const box = container.getBoundingClientRect();
980
+ const out = [];
981
+ for (const el of this._getVisibleItems()) {
982
+ const key = keyForElement(el);
983
+ if (key === void 0) continue;
984
+ const r = el.getBoundingClientRect();
985
+ if (r.bottom > box.top && r.top < box.bottom) out.push(key);
986
+ }
987
+ return out;
988
+ }
989
+ /**
990
+ * Keys of the currently rendered window (viewport + overscan) — the
991
+ * priority set for prefetching, mirroring `ixfx-grid-list.renderedKeys()`.
992
+ * Use {@link visibleKeys} for the strict viewport intersection. Keyless
993
+ * rows are omitted. Without virtualization, every visible row's key.
994
+ */
995
+ renderedKeys() {
996
+ if (!this.#virtualActive()) {
997
+ const out = [];
998
+ for (const el of this._getVisibleItems()) {
999
+ const key = keyForElement(el);
1000
+ if (key !== void 0) out.push(key);
1001
+ }
1002
+ return out;
1003
+ }
1004
+ const out = [];
1005
+ for (let i = this.#windowStart; i < this.#windowEnd; i++) {
1006
+ const el = this.#allItems[i];
1007
+ if (!el) continue;
1008
+ const key = keyForElement(el);
1009
+ if (key !== void 0) out.push(key);
1010
+ }
1011
+ return out;
1012
+ }
1013
+ *selectedFilter(kind) {
1014
+ const source = this.#virtualActive() ? this.#allItems : this.querySelectorAll(ITEM_SELECTOR);
1015
+ const isChecked = (el) => {
1016
+ if (this.interactionMode !== `checked`) return false;
1017
+ return !(this.#customWrappers.get(el) ?? el).hasAttribute(`not-checked`) && this.selection.selected.has(el);
1018
+ };
1019
+ for (const el of source) {
1020
+ if (!this.#virtualActive() && this.#wrappedElements.has(el)) continue;
1021
+ if (el.hidden) continue;
1022
+ switch (kind) {
1023
+ case `selected`:
1024
+ if (this.selection.selected.has(el)) yield el;
1025
+ break;
1026
+ case `checked`:
1027
+ if (isChecked(el)) yield el;
1028
+ break;
1029
+ case `either`: if (this.selection.selected.has(el) || isChecked(el)) yield el;
1030
+ }
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Get the source object behind a row built from `items`. Returns
1035
+ * `undefined` for declarative / programmatic elements.
1036
+ */
1037
+ getItemData(item) {
1038
+ return this.#itemData.get(item);
1039
+ }
1040
+ /**
1041
+ * Adapter mode: re-render the row carrying `key` in place — scroll and
1042
+ * selection are untouched. Unknown keys, and keys on rows not built from
1043
+ * `items`, are ignored. A no-op unless `items` is set.
1044
+ *
1045
+ * Rows whose formatter returns a full row element are swapped for the fresh
1046
+ * element; `<li>` rows are refilled. (A custom-element row with a
1047
+ * non-element formatter result cannot be re-rendered in place and is left
1048
+ * alone — have the formatter return a replacement element instead.)
1049
+ */
1050
+ updateItem(key, data) {
1051
+ if (this.items === void 0) return;
1052
+ const el = this.getItemByKey(key);
1053
+ if (!el || !this.#itemData.has(el)) return;
1054
+ const index = this.#adapterIndexOf(el);
1055
+ const fresh = this.formatter ? this.formatter(data, index) : data;
1056
+ if (el instanceof HTMLLIElement) {
1057
+ for (const child of [...el.childNodes]) child.remove();
1058
+ this.#fillAdapterItem(el, fresh, data, index);
1059
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1060
+ this._syncItemStates();
1061
+ } else if (fresh instanceof Element && this.#isRowElement(fresh)) {
1062
+ if (fresh.tagName !== `LI` && !fresh.hasAttribute(`data-list-item`)) fresh.setAttribute(`data-list-item`, ``);
1063
+ this.#stampAdapterKey(fresh, data);
1064
+ this.#itemData.set(fresh, data);
1065
+ this.#itemData.delete(el);
1066
+ this.selection.remap((item) => item === el ? fresh : item);
1067
+ if (this.#virtualActive()) this.#swapRegistry(el, fresh);
1068
+ el.replaceWith(fresh);
1069
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1070
+ this._syncItemStates();
1071
+ }
1072
+ if (this.keyProperty) {
1073
+ const sourceIndex = this.items.findIndex((entry) => this.#adapterProperty(entry, this.keyProperty) !== void 0 && String(this.#adapterProperty(entry, this.keyProperty)) === key);
1074
+ if (sourceIndex >= 0) {
1075
+ const next = [...this.items];
1076
+ next[sourceIndex] = data;
1077
+ this.#suspendAdapterRebuild = true;
1078
+ this.items = next;
1079
+ }
1080
+ }
1081
+ }
1082
+ #rebuildFromAdapter() {
1083
+ const source = this.items ?? [];
1084
+ const previous = [...this.selection.selected];
1085
+ const anchorKey = this.selection.anchor ? keyForElement(this.selection.anchor) : void 0;
1086
+ this.#customWrappers.clear();
1087
+ this.replaceChildren(...source.map((item, index) => this.#createAdapterItem(item, index)));
1088
+ this.#rekeySelection(previous, anchorKey, this.querySelectorAll(ITEM_SELECTOR));
1089
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1090
+ this._syncItemStates();
1091
+ this._syncNativeDraggable();
1092
+ this.#nativeDrag.refreshDraggable();
1093
+ this.#reconcileViewState();
1094
+ }
1095
+ /** Swap a rebuilt-away selection onto same-key rows; unmatched keys go pending. */
1096
+ #rekeySelection(previous, anchorKey, rows) {
1097
+ if (previous.length === 0 && anchorKey === void 0) return;
1098
+ const { live, gone, anchor } = rekeySelection(previous, anchorKey, rows, keyForElement);
1099
+ this.selection.demote(gone);
1100
+ this.selection.seed(live, anchor);
1101
+ }
1102
+ #createAdapterItem(item, index) {
1103
+ const content = this.formatter ? this.formatter(item, index) : item;
1104
+ if (content instanceof Element && this.#isRowElement(content)) {
1105
+ if (content.tagName !== `LI` && !content.hasAttribute(`data-list-item`)) content.setAttribute(`data-list-item`, ``);
1106
+ this.#stampAdapterKey(content, item);
1107
+ this.#itemData.set(content, item);
1108
+ return content;
1109
+ }
1110
+ const li = document.createElement(`li`);
1111
+ this.#fillAdapterItem(li, content, item, index);
1112
+ return li;
1113
+ }
1114
+ #fillAdapterItem(li, content, item, index) {
1115
+ if (typeof content === `string`) li.textContent = content;
1116
+ else if (content instanceof Node) li.appendChild(content);
1117
+ else if (this.formatter && content !== void 0 && content !== null) render(content, li);
1118
+ this.#decorateAdapterItem(li, item, index);
1119
+ this.#stampAdapterKey(li, item);
1120
+ this.#itemData.set(li, item);
1121
+ }
1122
+ /**
1123
+ * Apply the per-item icon / text colour / badge to a built row. Split out
1124
+ * from {@link #fillAdapterItem} so the parts stay readable.
1125
+ */
1126
+ #decorateAdapterItem(li, item, index) {
1127
+ const textColor = appearanceColor(this.textColor?.(item, index));
1128
+ if (textColor !== void 0) li.style.color = textColor;
1129
+ else li.style.removeProperty(`color`);
1130
+ const icon = this.icon?.(item, index);
1131
+ if (!appearanceAbsent(icon)) {
1132
+ const holder = document.createElement(`span`);
1133
+ holder.className = `item-icon`;
1134
+ holder.style.cssText = ITEM_ICON_EL_CSS;
1135
+ const iconColor = appearanceColor(this.iconColor?.(item, index));
1136
+ if (iconColor !== void 0) holder.style.color = iconColor;
1137
+ if (icon instanceof Node) holder.appendChild(icon);
1138
+ else render(icon, holder);
1139
+ li.prepend(holder);
1140
+ }
1141
+ const badge = this.badge?.(item, index);
1142
+ if (!appearanceAbsent(badge)) {
1143
+ const holder = document.createElement(`span`);
1144
+ holder.className = `item-badge`;
1145
+ holder.style.cssText = ITEM_BADGE_EL_CSS;
1146
+ if (badge instanceof Node) holder.appendChild(badge);
1147
+ else render(badge, holder);
1148
+ li.appendChild(holder);
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Whether a formatter-produced element is a whole row (used directly)
1153
+ * rather than row content (rendered into a `<li>`): an `<li>`, already
1154
+ * marked `[data-list-item]`, or a custom element.
1155
+ */
1156
+ #isRowElement(content) {
1157
+ return content.tagName === `LI` || content.hasAttribute(`data-list-item`) || content.tagName.includes(`-`);
1158
+ }
1159
+ #stampAdapterKey(el, item) {
1160
+ if (!this.keyProperty) return;
1161
+ const key = this.#adapterProperty(item, this.keyProperty);
1162
+ if (key !== void 0 && key !== null) el.setAttribute(`data-key`, String(key));
1163
+ }
1164
+ #adapterProperty(item, prop) {
1165
+ if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
1166
+ return item[prop];
1167
+ }
1168
+ /** Ordinal of an adapter-built row among adapter-built rows in DOM order. */
1169
+ #adapterIndexOf(el) {
1170
+ if (this.#virtualActive()) return this.#registryIndexOf(el);
1171
+ let index = 0;
1172
+ for (const item of this.querySelectorAll(ITEM_SELECTOR)) {
1173
+ if (item === el) return index;
1174
+ if (this.#itemData.has(item)) index++;
1175
+ }
1176
+ return index;
1177
+ }
1178
+ /**
1179
+ * Adapter rebuild for the virtualized window: same content as
1180
+ * {@link #rebuildFromAdapter} but rows become the recycle registry and
1181
+ * only the window attaches.
1182
+ */
1183
+ #rebuildAdapterVirtual() {
1184
+ const source = this.items ?? [];
1185
+ const previous = [...this.selection.selected];
1186
+ const anchorKey = this.selection.anchor ? keyForElement(this.selection.anchor) : void 0;
1187
+ this.#customWrappers.clear();
1188
+ this.#pool = document.createDocumentFragment();
1189
+ this.#pendingAnimate = [];
1190
+ this.#allItems = source.map((item, index) => this.#createAdapterItem(item, index));
1191
+ for (const el of this.#allItems) this.#animatedItems.add(el);
1192
+ this.#rekeySelection(previous, anchorKey, this.#allItems);
1193
+ this.#syncVirtualWindow(true);
1194
+ }
1195
+ #virtualActive() {
1196
+ return this.virtualize && this.itemHeight > 0;
1197
+ }
1198
+ /**
1199
+ * Selection order: the full registry while virtualized (so `selectAll` and
1200
+ * range operations span pooled rows too — they hold live element refs), the
1201
+ * attached rows otherwise.
1202
+ */
1203
+ #selectionOrder() {
1204
+ if (!this.#virtualActive()) return this._getVisibleItems();
1205
+ return this.#allItems.filter((el) => {
1206
+ if (el.hidden) return false;
1207
+ if (this.filterPredicate && !this.filterPredicate(el)) return false;
1208
+ return true;
1209
+ });
1210
+ }
1211
+ /** Registry index of a row, resolving wrapped inners to their wrapper. */
1212
+ #registryIndexOf(el) {
1213
+ const direct = this.#allItems.indexOf(el);
1214
+ if (direct >= 0) return direct;
1215
+ if (this.#wrappedElements.has(el)) {
1216
+ for (const [wrapper, inner] of this.#customWrappers) if (inner === el) return this.#allItems.indexOf(wrapper);
1217
+ }
1218
+ return -1;
1219
+ }
1220
+ #swapRegistry(oldEl, newEl) {
1221
+ const i = this.#allItems.indexOf(oldEl);
1222
+ if (i >= 0) this.#allItems[i] = newEl;
1223
+ }
1224
+ #cleanupWrapperRefs(el) {
1225
+ if (this.#customWrappers.has(el)) {
1226
+ const inner = this.#customWrappers.get(el);
1227
+ this.#customWrappers.delete(el);
1228
+ if (inner) this.#wrappedElements.delete(inner);
1229
+ }
1230
+ if (this.#wrappedElements.has(el)) {
1231
+ for (const [wrapper, inner] of this.#customWrappers) if (inner === el) {
1232
+ this.#customWrappers.delete(wrapper);
1233
+ break;
1234
+ }
1235
+ this.#wrappedElements.delete(el);
1236
+ }
1237
+ }
1238
+ /**
1239
+ * Global registry index for an attached row, derived from its DOM position:
1240
+ * nodes after the bottom spacer are end-of-list appends, nodes before the
1241
+ * top spacer are prepends, otherwise window-start plus preceding rows.
1242
+ */
1243
+ #globalIndexForAttached(el) {
1244
+ let count = 0;
1245
+ let sib = el.previousElementSibling;
1246
+ while (sib) {
1247
+ if (sib === this.#topSpacer) return this.#windowStart + count;
1248
+ if (sib === this.#bottomSpacer) return this.#allItems.length;
1249
+ if (sib.matches(ITEM_SELECTOR)) count++;
1250
+ sib = sib.previousElementSibling;
1251
+ }
1252
+ return count;
1253
+ }
1254
+ /**
1255
+ * Fold consumer-side DOM edits into the registry. Our own window moves
1256
+ * reconcile to no-ops (known nodes already pooled or correctly placed);
1257
+ * genuinely new nodes are spliced by DOM position; removed-then-gone nodes
1258
+ * are dropped. Returns whether the registry changed.
1259
+ */
1260
+ #reconcileRecords(records) {
1261
+ let changed = false;
1262
+ const fresh = [];
1263
+ const removed = [];
1264
+ for (const record of records) {
1265
+ for (const node of record.addedNodes) {
1266
+ if (!(node instanceof Element) || node.parentNode !== this) continue;
1267
+ if (node.hasAttribute(`data-virtual-spacer`) || !node.matches(ITEM_SELECTOR)) continue;
1268
+ const known = this.#registryIndexOf(node);
1269
+ if (known < 0) {
1270
+ const at = Math.min(this.#globalIndexForAttached(node), this.#allItems.length);
1271
+ this.#allItems.splice(at, 0, node);
1272
+ fresh.push(node);
1273
+ changed = true;
1274
+ } else if (this.#globalIndexForAttached(node) !== known) {
1275
+ const at = Math.min(this.#globalIndexForAttached(node), this.#allItems.length - 1);
1276
+ this.#allItems.splice(known, 1);
1277
+ this.#allItems.splice(at, 0, node);
1278
+ changed = true;
1279
+ }
1280
+ }
1281
+ for (const node of record.removedNodes) {
1282
+ if (!(node instanceof Element) || node.hasAttribute(`data-virtual-spacer`)) continue;
1283
+ const known = this.#registryIndexOf(node);
1284
+ if (known < 0) continue;
1285
+ if (node.parentNode === this.#pool || node.parentNode === this) continue;
1286
+ this.#allItems.splice(known, 1);
1287
+ removed.push(node);
1288
+ changed = true;
1289
+ }
1290
+ }
1291
+ this.#demoteDeparted(removed);
1292
+ if (fresh.length > 0) {
1293
+ if (this.#isBulkPopulate(fresh.length)) for (const el of fresh) this.#animatedItems.add(el);
1294
+ else this.#pendingAnimate.push(...fresh);
1295
+ }
1296
+ if (changed && this.interactionMode === `checked`) this._syncCheckboxes();
1297
+ if (changed) this.search.refresh();
1298
+ return changed;
1299
+ }
1300
+ /** Consume queued observer records first so a sync never drops a row. */
1301
+ #drainMutations() {
1302
+ const pending = this.#itemObserver?.takeRecords() ?? [];
1303
+ return pending.length > 0 ? this.#reconcileRecords(pending) : false;
1304
+ }
1305
+ #scrollList() {
1306
+ return this.renderRoot.querySelector(`ul`);
1307
+ }
1308
+ #measureViewport(ul) {
1309
+ if (!ul) return this.#lastViewportH;
1310
+ const h = ul.clientHeight || ul.getBoundingClientRect().height || 0;
1311
+ if (h > 0) this.#lastViewportH = h;
1312
+ return this.#lastViewportH;
1313
+ }
1314
+ #ensureSpacers() {
1315
+ if (this.#topSpacer && this.#bottomSpacer) return;
1316
+ const make = () => {
1317
+ const li = document.createElement(`li`);
1318
+ li.setAttribute(`data-virtual-spacer`, ``);
1319
+ li.setAttribute(`aria-hidden`, `true`);
1320
+ return li;
1321
+ };
1322
+ this.#topSpacer = make();
1323
+ this.#bottomSpacer = make();
1324
+ }
1325
+ #ensureResizeObserved() {
1326
+ const ul = this.#scrollList();
1327
+ if (!ul) return;
1328
+ this.#resizeObserver ??= new ResizeObserver(() => {
1329
+ this.#syncVirtualWindow();
1330
+ this.#scheduleViewportChangeEmit();
1331
+ this.#reconcileViewState();
1332
+ });
1333
+ this.#resizeObserver.observe(ul);
1334
+ }
1335
+ #onScroll;
1336
+ /**
1337
+ * Reconcile the attached light DOM with the overscanned window. Rows are
1338
+ * moved by identity (never recreated); selection, cursor and component
1339
+ * state travel with them.
1340
+ */
1341
+ #syncVirtualWindow(force = false) {
1342
+ if (!this.#virtualActive() || !this.isConnected) return;
1343
+ const changed = this.#drainMutations() || force;
1344
+ this.#ensureResizeObserved();
1345
+ const count = this.#allItems.length;
1346
+ const ul = this.#scrollList();
1347
+ const stride = this.itemHeight;
1348
+ const viewportH = this.#measureViewport(ul);
1349
+ const maxScroll = Math.max(0, count * stride - viewportH);
1350
+ const offset = Math.min(Math.max(0, ul ? ul.scrollTop : 0), maxScroll);
1351
+ if (ul && Math.abs(ul.scrollTop - offset) > .5) ul.scrollTop = offset;
1352
+ const firstVisible = Math.floor(offset / stride);
1353
+ const lastVisible = Math.ceil((offset + viewportH) / stride);
1354
+ const start = count === 0 ? 0 : Math.max(0, firstVisible - VIRTUAL_OVERSCAN);
1355
+ const end = Math.min(count, lastVisible + VIRTUAL_OVERSCAN);
1356
+ if (!changed && start === this.#windowStart && end === this.#windowEnd) return;
1357
+ if (count === 0) {
1358
+ if (this.#allItems.length > 0) this.#pool.append(...this.#allItems);
1359
+ this.replaceChildren();
1360
+ } else {
1361
+ this.#ensureSpacers();
1362
+ const top = this.#topSpacer;
1363
+ const bottom = this.#bottomSpacer;
1364
+ if (!top || !bottom) return;
1365
+ top.style.height = `${start * stride}px`;
1366
+ bottom.style.height = `${Math.max(0, (count - end) * stride)}px`;
1367
+ const rows = this.#allItems.slice(start, end);
1368
+ const parked = [...this.#allItems.slice(0, start), ...this.#allItems.slice(end)];
1369
+ if (parked.length > 0) this.#pool.append(...parked);
1370
+ this.replaceChildren(top, ...rows, bottom);
1371
+ }
1372
+ this.#windowStart = start;
1373
+ this.#windowEnd = end;
1374
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1375
+ this._syncItemStates();
1376
+ const totalCount = this.#selectionOrder().length;
1377
+ for (let i = start; i < end; i++) {
1378
+ const el = this.#allItems[i];
1379
+ if (el) this.#applyRowState(el, i + 1, totalCount);
1380
+ }
1381
+ const pending = this.#pendingAnimate;
1382
+ this.#pendingAnimate = [];
1383
+ for (const el of pending) {
1384
+ this.#animatedItems.add(el);
1385
+ if (el.parentNode === this) animateListItemIn(el);
1386
+ }
1387
+ this.search.repaintHighlights();
1388
+ if (changed) this.#reconcileViewState();
1389
+ }
1390
+ /** Per-attach state the attached sweep can't cover (it only sees the window). */
1391
+ #applyRowState(el, posInSet, setSize) {
1392
+ if (el instanceof HTMLElement) {
1393
+ el.draggable = this.itemsDraggable && this._isCheckable(el) && !el.hasAttribute(`data-editing`);
1394
+ if (posInSet !== void 0 && setSize !== void 0) {
1395
+ el.setAttribute(`aria-setsize`, String(setSize));
1396
+ el.setAttribute(`aria-posinset`, String(posInSet));
1397
+ if (!el.hasAttribute(`role`) || el.hasAttribute(`data-aria-role`)) {
1398
+ el.setAttribute(`role`, `option`);
1399
+ el.setAttribute(`data-aria-role`, ``);
1400
+ el.setAttribute(`aria-selected`, String(this.selection.selected.has(el)));
1401
+ }
1402
+ }
1403
+ }
1404
+ el.removeAttribute(`data-marquee`);
1405
+ el.removeAttribute(`data-drag-origin`);
1406
+ el.removeAttribute(`data-drop-target`);
1407
+ }
1408
+ /**
1409
+ * Leave virtualization: reattach every registry row in order and drop the
1410
+ * spacers and pool. Selection (live refs) and cursor survive untouched.
1411
+ */
1412
+ #teardownVirtualWindow() {
1413
+ for (const el of this.#allItems) this.#animatedItems.add(el);
1414
+ if (this.#allItems.length > 0) this.replaceChildren(...this.#allItems);
1415
+ else this.replaceChildren();
1416
+ this.#pool = document.createDocumentFragment();
1417
+ this.#topSpacer = void 0;
1418
+ this.#bottomSpacer = void 0;
1419
+ this.#windowStart = 0;
1420
+ this.#windowEnd = 0;
1421
+ this.#pendingAnimate = [];
1422
+ this._syncCheckboxes();
1423
+ this._syncItemStates();
1424
+ this._syncNativeDraggable();
1425
+ this.#nativeDrag.refreshDraggable();
1426
+ this.#scheduleViewportChangeEmit();
1427
+ }
1428
+ /** Coalesce content/layout viewport checks to at most one animation frame. */
1429
+ #scheduleViewportChangeEmit() {
1430
+ if (this.#scrollRaf !== 0) return;
1431
+ this.#scrollRaf = requestAnimationFrame(() => {
1432
+ this.#scrollRaf = 0;
1433
+ this.#syncVirtualWindow();
1434
+ this.#emitViewportChangeIfChanged();
1435
+ });
1436
+ }
1437
+ /**
1438
+ * Dispatch `vertical-list-viewport-change` with the current key sets — unless
1439
+ * neither set changed since the last dispatch, so coalesced frames and
1440
+ * no-op scrolls stay silent.
1441
+ */
1442
+ #emitViewportChangeIfChanged() {
1443
+ const renderedKeys = this.renderedKeys();
1444
+ const visibleKeys = this.visibleKeys();
1445
+ if (sameKeys(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys(this.#lastViewportVisibleKeys, visibleKeys)) return;
1446
+ this.#lastViewportRenderedKeys = renderedKeys;
1447
+ this.#lastViewportVisibleKeys = visibleKeys;
1448
+ this._dispatch(`vertical-list-viewport-change`, {
1449
+ renderedKeys,
1450
+ visibleKeys
1451
+ });
1452
+ }
1453
+ _getVisibleItems() {
1454
+ return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
1455
+ if (this.#wrappedElements.has(el)) return false;
1456
+ if (el.hidden) return false;
1457
+ if (this.filterPredicate && !this.filterPredicate(el)) return false;
1458
+ return true;
1459
+ });
1460
+ }
1461
+ _isCheckable(item) {
1462
+ if (this.interactionMode !== `checked`) return true;
1463
+ return !(this.#customWrappers.get(item) ?? item).hasAttribute(`not-checked`);
1464
+ }
1465
+ _syncItemStates() {
1466
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
1467
+ if (this.#wrappedElements.has(el)) continue;
1468
+ const item = this.#customWrappers.get(el) ?? el;
1469
+ el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
1470
+ el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
1471
+ if (el.hasAttribute(`data-aria-role`)) el.setAttribute(`aria-selected`, String(this.selection.selected.has(el)));
1472
+ if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !item.hasAttribute(`not-checked`) && this.selection.selected.has(el));
1473
+ else el.removeAttribute(`data-checked`);
1474
+ }
1475
+ this.dataset.inputMode = this.tickled.inputMode;
1476
+ this._syncCheckboxes();
1477
+ }
1478
+ _syncCheckboxes() {
1479
+ const inCheckedMode = this.interactionMode === `checked`;
1480
+ const wantsCustom = typeof this.checkboxRenderer === `function`;
1481
+ const roots = this.#virtualActive() ? [this, this.#pool] : [this];
1482
+ if (inCheckedMode) for (const root of roots) for (const inner of Array.from(root.children)) {
1483
+ if (!inner.hasAttribute(`data-list-item`)) continue;
1484
+ if (inner.parentNode !== root) continue;
1485
+ const wrapper = document.createElement(`li`);
1486
+ wrapper.className = `ixfx-custom-row`;
1487
+ const key = inner.getAttribute(`data-key`);
1488
+ const label = inner.getAttribute(`data-search-label`);
1489
+ if (key !== null) wrapper.setAttribute(`data-key`, key);
1490
+ if (label !== null) wrapper.setAttribute(`data-search-label`, label);
1491
+ inner.removeAttribute(`data-list-item`);
1492
+ root.insertBefore(wrapper, inner);
1493
+ wrapper.appendChild(inner);
1494
+ this.#customWrappers.set(wrapper, inner);
1495
+ this.#wrappedElements.add(inner);
1496
+ if (this.#itemData.has(inner)) this.#itemData.set(wrapper, this.#itemData.get(inner));
1497
+ if (this.#virtualActive()) this.#swapRegistry(inner, wrapper);
1498
+ }
1499
+ else {
1500
+ for (const [wrapper, inner] of this.#customWrappers) {
1501
+ if (wrapper.parentNode !== this && wrapper.parentNode !== this.#pool) continue;
1502
+ inner.setAttribute(`data-list-item`, ``);
1503
+ const parent = wrapper.parentNode;
1504
+ if (parent) parent.insertBefore(inner, wrapper);
1505
+ wrapper.remove();
1506
+ this.#itemData.delete(wrapper);
1507
+ if (this.#virtualActive()) this.#swapRegistry(wrapper, inner);
1508
+ }
1509
+ this.#customWrappers.clear();
1510
+ this.#wrappedElements.clear();
1511
+ }
1512
+ for (const el of this.querySelectorAll(`li`)) {
1513
+ const existing = el.querySelector(`.ixfx-list-checkbox`);
1514
+ if (!inCheckedMode) {
1515
+ if (existing) existing.remove();
1516
+ continue;
1517
+ }
1518
+ if ((this.#customWrappers.get(el) ?? el).hasAttribute(`not-checked`)) {
1519
+ if (existing) existing.remove();
1520
+ continue;
1521
+ }
1522
+ const existingIsCustom = existing?.classList.contains(`ixfx-list-checkbox-custom`) ?? false;
1523
+ if (existing && existingIsCustom !== wantsCustom) existing.remove();
1524
+ let cb = el.querySelector(`.ixfx-list-checkbox`);
1525
+ if (!cb) {
1526
+ if (wantsCustom) {
1527
+ const opts = this.checkboxRenderer;
1528
+ const margin = opts.margin ?? `0`;
1529
+ const width = opts.width ?? `24px`;
1530
+ cb = this._createCustomCheckbox(el, margin, width);
1531
+ } else cb = this._createNativeCheckboxCell(el);
1532
+ el.prepend(cb);
1533
+ }
1534
+ const checked = this.selection.selected.has(el);
1535
+ if (wantsCustom) {
1536
+ cb.setAttribute(`aria-checked`, String(checked));
1537
+ cb.toggleAttribute(`data-checked`, checked);
1538
+ if (this.checkboxRenderer) {
1539
+ const renderer = typeof this.checkboxRenderer === `function` ? this.checkboxRenderer : this.checkboxRenderer.renderer;
1540
+ render(renderer(checked), cb);
1541
+ }
1542
+ } else {
1543
+ const input = cb.querySelector(`input.ixfx-list-checkbox`);
1544
+ if (input) input.checked = checked;
1545
+ }
1546
+ }
1547
+ }
1548
+ /**
1549
+ * Native checkbox cell: a span that stretches to the row's full cross-axis
1550
+ * (so the entire column height is a click target) with a real `<input>`
1551
+ * centred inside. The span owns the click; the input is purely visual and
1552
+ * has `pointer-events:none` so a single click produces one toggle.
1553
+ */
1554
+ _createNativeCheckboxCell(el) {
1555
+ const cell = document.createElement(`span`);
1556
+ cell.className = `ixfx-list-checkbox ixfx-list-checkbox-native`;
1557
+ cell.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:18px;margin:0 6px 0 0;cursor:pointer;pointer-events:auto`;
1558
+ cell.tabIndex = -1;
1559
+ cell.setAttribute(`role`, `checkbox`);
1560
+ cell.setAttribute(`part`, `checkbox`);
1561
+ cell.addEventListener(`click`, (e) => {
1562
+ e.stopPropagation();
1563
+ this.selection.toggle(el);
1564
+ });
1565
+ const input = document.createElement(`input`);
1566
+ input.type = `checkbox`;
1567
+ input.className = `ixfx-list-checkbox`;
1568
+ input.style.cssText = `pointer-events:none;cursor:pointer;margin:0`;
1569
+ cell.appendChild(input);
1570
+ return cell;
1571
+ }
1572
+ /**
1573
+ * Custom checkbox cell: same shape as the native cell (a stretching span)
1574
+ * so behaviour is identical. The renderer's content goes inside.
1575
+ */
1576
+ _createCustomCheckbox(el, margin, width) {
1577
+ const span = document.createElement(`span`);
1578
+ span.className = `ixfx-list-checkbox ixfx-list-checkbox-custom`;
1579
+ span.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:${width};margin:${margin};cursor:pointer;pointer-events:auto`;
1580
+ span.tabIndex = -1;
1581
+ span.setAttribute(`role`, `checkbox`);
1582
+ span.setAttribute(`part`, `checkbox`);
1583
+ span.addEventListener(`click`, (e) => {
1584
+ e.stopPropagation();
1585
+ this.selection.toggle(el);
1586
+ });
1587
+ return span;
1588
+ }
1589
+ _findListItem(path) {
1590
+ for (const target of path) {
1591
+ if (!(target instanceof Element)) continue;
1592
+ if (target === this) break;
1593
+ if (target.parentElement === this && target.matches(ITEM_SELECTOR)) return target;
1594
+ }
1595
+ }
1596
+ _dispatch(name, detail) {
1597
+ this.dispatchEvent(new CustomEvent(name, {
1598
+ detail,
1599
+ bubbles: true,
1600
+ composed: true
1601
+ }));
1602
+ }
1603
+ _dispatchCancelable(name, detail) {
1604
+ return this.dispatchEvent(new CustomEvent(name, {
1605
+ detail,
1606
+ bubbles: true,
1607
+ composed: true,
1608
+ cancelable: true
1609
+ }));
1610
+ }
1611
+ _activateItem(item) {
1612
+ if (this.interactionMode !== `manual`) this.selection.replace(item);
1613
+ this._dispatch(`list-activate`, { item });
1614
+ }
1615
+ activateKey(key) {
1616
+ const item = this.#itemForKey(key);
1617
+ if (item) this._activateItem(item);
1618
+ }
1619
+ /** The row (or `undefined` for empty space) the last `list-drag-over` was fired for. */
1620
+ #dragOver;
1621
+ /** Tell the consumer the drag left its last target and drop the highlight. */
1622
+ #clearDropTarget() {
1623
+ const last = this.#dragOver;
1624
+ this.#dragOver = void 0;
1625
+ this.#highlightDropTarget(void 0, false);
1626
+ if (last) this._dispatch(`list-drag-leave`, { key: last.item ? keyForElement(last.item) : void 0 });
1627
+ }
1628
+ #dropTargetEl;
1629
+ /** Mark one row (or, for empty space, the host) as the drop target. */
1630
+ #highlightDropTarget(item, active) {
1631
+ this.#dropTargetEl?.removeAttribute(`data-drop-target`);
1632
+ this.#dropTargetEl = active ? item : void 0;
1633
+ this.toggleAttribute(`data-drop-target`, active && item === void 0);
1634
+ if (active && item) item.setAttribute(`data-drop-target`, ``);
1635
+ }
1636
+ /**
1637
+ * Select the row if it is not already, then fire `list-context-menu`.
1638
+ * Returns false when a listener cancelled it (the native menu is then suppressed).
1639
+ */
1640
+ #requestContextMenu(item, x, y) {
1641
+ if (!this.selection.selected.has(item) && this.interactionMode !== `manual` && this._isCheckable(item)) this.selection.replace(item);
1642
+ return this._dispatchCancelable(`list-context-menu`, {
1643
+ key: keyForElement(item),
1644
+ item,
1645
+ x,
1646
+ y,
1647
+ selectedKeys: this.selection.selectedKeys
1648
+ });
1649
+ }
1650
+ #edit;
1651
+ /** The key of the row being edited, if any. */
1652
+ get editingKey() {
1653
+ return this.#edit?.key;
1654
+ }
1655
+ #isEditEvent(e) {
1656
+ return this.#edit !== void 0 && e.composedPath().includes(this.#edit.label);
1657
+ }
1658
+ /**
1659
+ * Edit a row's label in place with an `ixfx-editable-label`. The editor
1660
+ * replaces the row's `[data-edit-label]` element (else the whole row) and
1661
+ * the original content returns when editing ends. Fires `list-edit-commit`
1662
+ * `{ key, value }` when the text changed, otherwise `list-edit-cancel`
1663
+ * `{ key }` (Escape, an unchanged value, or one the label rejects). The list
1664
+ * never rewrites the label itself — update your data in response.
1665
+ *
1666
+ * Returns false when `editValidator` is not set, or the row is unknown or
1667
+ * not currently rendered.
1668
+ */
1669
+ beginEdit(key) {
1670
+ this.#finishEdit();
1671
+ const validate = this.editValidator;
1672
+ if (!validate) return false;
1673
+ const row = this.#itemForKey(key);
1674
+ if (!(row instanceof HTMLElement) || row.parentNode !== this) return false;
1675
+ const container = row.querySelector(`[data-edit-label]`) ?? row;
1676
+ const value = container.getAttribute(`data-edit-value`) ?? container.textContent?.trim() ?? ``;
1677
+ const label = document.createElement(`ixfx-editable-label`);
1678
+ label.mode = `label`;
1679
+ label.value = value;
1680
+ label.validator = (text) => validate(text, key);
1681
+ label.style.cssText = `flex:1;min-width:0;--label-align:left;--label-padding:0 var(--space-xs,3px)`;
1682
+ const edit = {
1683
+ key,
1684
+ row,
1685
+ container,
1686
+ saved: [...container.childNodes],
1687
+ label,
1688
+ committed: void 0,
1689
+ refocus: false
1690
+ };
1691
+ this.#edit = edit;
1692
+ label.addEventListener(`change`, (e) => {
1693
+ e.stopPropagation();
1694
+ edit.committed = e.detail.value;
1695
+ });
1696
+ label.addEventListener(`input`, (e) => e.stopPropagation());
1697
+ label.addEventListener(`keydown`, (e) => {
1698
+ edit.refocus = e.key === `Enter` || e.key === `Escape`;
1699
+ });
1700
+ for (const type of [`pointerdown`, `dragstart`]) label.addEventListener(type, (e) => e.stopPropagation());
1701
+ label.addEventListener(`focusout`, () => this.#finishEdit());
1702
+ container.replaceChildren(label);
1703
+ row.setAttribute(`data-editing`, ``);
1704
+ row.draggable = false;
1705
+ row.scrollIntoView({
1706
+ block: `nearest`,
1707
+ inline: `nearest`
1708
+ });
1709
+ label.updateComplete.then(() => {
1710
+ if (this.#edit === edit) label.shadowRoot?.querySelector(`.content`)?.click();
1711
+ });
1712
+ return true;
1713
+ }
1714
+ /** Restore the row's content and report how the edit ended. */
1715
+ #finishEdit() {
1716
+ const edit = this.#edit;
1717
+ if (!edit) return;
1718
+ this.#edit = void 0;
1719
+ if (edit.container.isConnected) edit.container.replaceChildren(...edit.saved);
1720
+ edit.row.removeAttribute(`data-editing`);
1721
+ this.#applyRowState(edit.row);
1722
+ if (edit.refocus) this.focus();
1723
+ if (edit.committed === void 0) this._dispatch(`list-edit-cancel`, { key: edit.key });
1724
+ else this._dispatch(`list-edit-commit`, {
1725
+ key: edit.key,
1726
+ value: edit.committed
1727
+ });
1728
+ }
1729
+ static {
1730
+ this.styles = [
1731
+ themeFallbacks,
1732
+ scrollFadeStyles(`y`),
1733
+ listSearchStyles,
1734
+ css`
1735
+ :host {
1736
+ display: block;
1737
+ position: relative;
1738
+ background: var(--list-bg, var(--surface-3));
1739
+ color: var(--list-text, var(--surface-3-text));
1740
+ border-radius: var(--list-radius, var(--radius-s));
1741
+ border: 1px solid var(--list-border, var(--border));
1742
+ width: 100%;
1743
+ height: 100%;
1744
+ overflow: hidden;
1745
+ box-sizing: border-box;
1746
+ outline: none;
1747
+ --ixfx-list-item-padding: var(--ixfx-list-item-padding, var(--list-item-padding, 4px var(--space-m, 8px)));
1748
+ --_item-padding:
1749
+ var(--ixfx-list-item-padding, var(--list-item-padding, 0 var(--space-xs, 3px)));
1750
+ }
1751
+
1752
+ :host(:focus-visible) {
1753
+ outline: 2px solid var(--accent);
1754
+ outline-offset: 2px;
1755
+ }
1756
+
1757
+ .container {
1758
+ position: relative;
1759
+ width: 100%;
1760
+ height: 100%;
1761
+ }
1762
+
1763
+ ul {
1764
+ list-style: none;
1765
+ margin: 0;
1766
+ padding: 0;
1767
+ width: 100%;
1768
+ height: 100%;
1769
+ overflow-x: hidden;
1770
+ overflow-y: auto;
1771
+ box-sizing: border-box;
1772
+ /* Scrollbar theming: end users override these CSS variables to control
1773
+ the native scrollbar (scrollbar-width, scrollbar-gutter, scrollbar-color). */
1774
+ scrollbar-width: var(--list-scrollbar-width, auto);
1775
+ scrollbar-gutter: var(--list-scrollbar-gutter, auto);
1776
+ scrollbar-color: var(--list-scrollbar-color, auto);
1777
+ }
1778
+
1779
+ ::slotted([hidden]) {
1780
+ display: none !important;
1781
+ }
1782
+
1783
+ ::slotted(li) {
1784
+ display: flex;
1785
+ align-items: center;
1786
+ padding: var(--_item-padding);
1787
+ /* Inset each row from the scroll container's edges (margin, not padding:
1788
+ padding is inside the border box, so the tickle/selection outlines
1789
+ would still sit flush against the edge and be clipped). */
1790
+ margin: 0 var(--list-gutter, 4px);
1791
+ min-height: var(--list-item-height, 28px);
1792
+ cursor: pointer;
1793
+ user-select: none;
1794
+ transition: background var(--transition, 0.1s ease-out),
1795
+ outline-color var(--transition, 0.1s ease-out);
1796
+ box-sizing: border-box;
1797
+ outline: 1px solid transparent;
1798
+ }
1799
+
1800
+ /* Per-item icon / badge (adapter mode) are styled inline by
1801
+ #decorateAdapterItem — ::slotted() only reaches the host's direct
1802
+ children, and these spans live inside a slotted <li>. */
1803
+
1804
+ /* Custom shadow-DOM components handle their own internal layout/padding.
1805
+ Wrapped in :where() so the selector stays at zero specificity and a
1806
+ consumer's own :host rule (e.g. display: flex) wins without
1807
+ needing !important. */
1808
+ :where(::slotted([data-list-item])) {
1809
+ display: block;
1810
+ margin: 0 var(--list-gutter, 4px);
1811
+ cursor: pointer;
1812
+ user-select: none;
1813
+ transition: background var(--transition, 0.1s ease-out),
1814
+ outline-color var(--transition, 0.1s ease-out);
1815
+ outline: 1px solid transparent;
1816
+ }
1817
+
1818
+ /* Virtualization spacers: plain height holders bracketing the rendered
1819
+ window. Attribute specificity beats ::slotted(li) so none of the row
1820
+ layout (flex, padding, min-height, gutters) applies; pointer events are
1821
+ off so clicks land on the scroll container, not a phantom row. */
1822
+ ::slotted([data-virtual-spacer]) {
1823
+ display: block;
1824
+ padding: 0;
1825
+ margin: 0;
1826
+ border: none;
1827
+ min-height: 0;
1828
+ overflow: hidden;
1829
+ cursor: default;
1830
+ pointer-events: none;
1831
+ outline: none;
1832
+ user-select: none;
1833
+ }
1834
+
1835
+ /* In checked mode, only the checkbox column is clickable; the row body
1836
+ gets a default cursor so it doesn't suggest otherwise. */
1837
+ :host([interaction-mode="checked"]) ::slotted(li),
1838
+ :host([interaction-mode="checked"]) ::slotted([data-list-item]) {
1839
+ cursor: default;
1840
+ }
1841
+
1842
+ /* Wrapper inserted around custom [data-list-item] elements in checked mode.
1843
+ Provides the flex layout so the injected checkbox can stretch to the
1844
+ row's full height via align-self:stretch. */
1845
+ .ixfx-custom-row {
1846
+ display: flex;
1847
+ align-items: center;
1848
+ min-height: var(--list-item-height, 28px);
1849
+ box-sizing: border-box;
1850
+ cursor: default;
1851
+ user-select: none;
1852
+ }
1853
+
1854
+ ::slotted([data-tickled]) {
1855
+ background: var(--item-bg-tickled);
1856
+ outline: var(--item-border-tickled);
1857
+ }
1858
+
1859
+ ::slotted([data-selected]) {
1860
+ background: var(--item-bg-selected);
1861
+ color: var(--item-text-selected);
1862
+ outline: 1px solid transparent;
1863
+ }
1864
+
1865
+ ::slotted([data-selected][data-tickled]) {
1866
+ background: var(--item-bg-selected-tickled);
1867
+ outline: 2px dashed var(--surface-3);
1868
+ outline-offset: -2px;
1869
+ }
1870
+
1871
+ /* Drag-select (marquee) rectangle + covered-row preview.
1872
+ Style hooks: part="marquee", --vertical-list-marquee-border,
1873
+ --vertical-list-marquee-fill. */
1874
+ .marquee {
1875
+ position: absolute;
1876
+ pointer-events: none;
1877
+ z-index: 5;
1878
+ border: 1px solid var(--vertical-list-marquee-border, var(--accent));
1879
+ background: var(--vertical-list-marquee-fill, color-mix(in srgb, var(--accent) 15%, transparent));
1880
+ }
1881
+
1882
+ ::slotted([data-marquee]) {
1883
+ background: var(--item-bg-selected);
1884
+ color: var(--item-text-selected);
1885
+ }
1886
+
1887
+ ::slotted([data-drag-origin]) {
1888
+ opacity: 0.6;
1889
+ }
1890
+
1891
+ /* Native drop target: the row under a drag, or the whole list over empty space. */
1892
+ ::slotted([data-drop-target]) {
1893
+ background: var(--list-drop-target-bg, color-mix(in srgb, var(--accent) 25%, transparent));
1894
+ outline: 2px solid var(--list-drop-target-outline, var(--accent));
1895
+ outline-offset: -2px;
1896
+ }
1897
+
1898
+ :host([data-drop-target]) {
1899
+ outline: 2px solid var(--list-drop-target-outline, var(--accent));
1900
+ outline-offset: -2px;
1901
+ }
1902
+
1903
+ /* checked mode: checkbox (injected into li) communicates selection — suppress bg highlight */
1904
+ :host([interaction-mode="checked"]) ::slotted([data-selected]) {
1905
+ background: transparent;
1906
+ color: inherit;
1907
+ outline: 1px solid transparent;
1908
+ padding: var(--space-s);
1909
+ }
1910
+
1911
+ :host([interaction-mode="checked"]) ::slotted([data-selected][data-tickled]) {
1912
+ background: var(--item-bg-tickled);
1913
+ color: inherit;
1914
+ outline: var(--item-border-tickled);
1915
+ }
1916
+ `
1917
+ ];
1918
+ }
1919
+ render() {
1920
+ const role = this.getAttribute(`role`) ? void 0 : `listbox`;
1921
+ const ariaMulti = this.selectionMode === `multiple`;
1922
+ return html`
1923
+ <div class="container">
1924
+ <ul
1925
+ part="list"
1926
+ class="scroll-fade-y"
1927
+ role=${ifDefined(role)}
1928
+ aria-multiselectable=${ariaMulti ? `true` : `false`}
1929
+ @scroll=${this.#onScroll}
1930
+ >
1931
+ <slot></slot>
1932
+ </ul>
1933
+ ${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
1934
+ ${this.search.renderField()}
1935
+ </div>
1936
+ `;
1937
+ }
1938
+ };
1939
+ __decorate([property({
1940
+ type: String,
1941
+ attribute: `selection-mode`
1942
+ })], VerticalListElement.prototype, "selectionMode", void 0);
1943
+ __decorate([property({
1944
+ type: String,
1945
+ attribute: `interaction-mode`
1946
+ })], VerticalListElement.prototype, "interactionMode", void 0);
1947
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "filterPredicate", void 0);
1948
+ __decorate([property({
1949
+ type: String,
1950
+ attribute: `item-wrap`
1951
+ })], VerticalListElement.prototype, "itemWrap", void 0);
1952
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "checkboxRenderer", void 0);
1953
+ __decorate([property({
1954
+ type: String,
1955
+ attribute: `search-modes`,
1956
+ reflect: true
1957
+ })], VerticalListElement.prototype, "searchModes", void 0);
1958
+ __decorate([property({
1959
+ type: Boolean,
1960
+ attribute: `items-draggable`,
1961
+ reflect: true
1962
+ })], VerticalListElement.prototype, "itemsDraggable", void 0);
1963
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "dragPreviewRenderer", void 0);
1964
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "dragPreviewSource", void 0);
1965
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "items", void 0);
1966
+ __decorate([property({
1967
+ type: String,
1968
+ attribute: `key-property`
1969
+ })], VerticalListElement.prototype, "keyProperty", void 0);
1970
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "formatter", void 0);
1971
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "icon", void 0);
1972
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "iconColor", void 0);
1973
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "textColor", void 0);
1974
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "badge", void 0);
1975
+ __decorate([property({
1976
+ type: Boolean,
1977
+ attribute: `virtualize`,
1978
+ reflect: true
1979
+ })], VerticalListElement.prototype, "virtualize", void 0);
1980
+ __decorate([property({
1981
+ type: Number,
1982
+ attribute: `item-height`
1983
+ })], VerticalListElement.prototype, "itemHeight", void 0);
1984
+ __decorate([property({
1985
+ type: Boolean,
1986
+ attribute: `items-droppable`,
1987
+ reflect: true
1988
+ })], VerticalListElement.prototype, "itemsDroppable", void 0);
1989
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "editValidator", void 0);
1990
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "canDrop", void 0);
1991
+ VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
1992
+ //#endregion
1993
+ export { VerticalListElement as t };