@ixfx/components 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.d.ts +747 -4
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +12240 -9065
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.d.ts +1 -1
- package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
- package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/button.d.ts.map +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/crumbs.d.ts +2 -2
- package/dist/{data-grid-BUeZjnvj.d.ts → data-grid-C5hk_pzK.d.ts} +5 -4
- package/dist/{data-grid-BUeZjnvj.d.ts.map → data-grid-C5hk_pzK.d.ts.map} +1 -1
- package/dist/data-grid.d.ts +2 -2
- package/dist/data-grid.js +3 -1
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-CQguZg9h.d.ts → detail-list-CxPpv89T.d.ts} +6 -5
- package/dist/detail-list-CxPpv89T.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -3
- package/dist/detail-list.js +3 -1
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-lTwg4lp7.d.ts → grid-list-BGvoWbZz.d.ts} +28 -4
- package/dist/{grid-list-lTwg4lp7.d.ts.map → grid-list-BGvoWbZz.d.ts.map} +1 -1
- package/dist/{grid-list-40blkeza.js → grid-list-DSnYOGh7.js} +60 -2
- package/dist/grid-list-DSnYOGh7.js.map +1 -0
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-CEZ72VOs.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +2 -2
- package/dist/{grouped-item-lister-CEZ72VOs.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
- package/dist/icon-CDGprSdE.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -2
- package/dist/incr-search.d.ts +1 -1
- package/dist/{index--qnhBWZJ.d.ts → index--5BSdbnL.d.ts} +2 -2
- package/dist/{index--qnhBWZJ.d.ts.map → index--5BSdbnL.d.ts.map} +1 -1
- package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
- package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
- package/dist/{index-DsjpDksO.d.ts → index-CS9yM8L-.d.ts} +2 -2
- package/dist/{index-DsjpDksO.d.ts.map → index-CS9yM8L-.d.ts.map} +1 -1
- package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
- package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
- package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
- package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
- package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
- package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
- package/dist/index.d.ts +647 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4804 -2313
- package/dist/index.js.map +1 -1
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
- package/dist/labelled-radial-input.d.ts +2 -2
- package/dist/labelled-radial-input.d.ts.map +1 -1
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-slider-input.d.ts +1 -1
- package/dist/{list-native-drag-controller-B0hWdbtM.js → list-native-drag-controller-D97TYjn1.js} +36 -5
- package/dist/list-native-drag-controller-D97TYjn1.js.map +1 -0
- package/dist/{list-native-drag-controller-Cmpx7jnx.d.ts → list-native-drag-controller-DWiXQZou.d.ts} +3 -2
- package/dist/list-native-drag-controller-DWiXQZou.d.ts.map +1 -0
- package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
- package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
- package/dist/menu.d.ts +2 -2
- package/dist/miller.d.ts +2 -2
- package/dist/panel.d.ts +1 -1
- package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
- package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
- package/dist/split-layout.d.ts +1 -1
- package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
- package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
- package/dist/tabs.d.ts +1 -1
- package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +2 -2
- package/dist/{titlebar-B5zSTZnF.d.ts.map → titlebar-Tqu-VvMU.d.ts.map} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/{tree-CVj7TIk2.js → tree-C25xYUa6.js} +4 -2
- package/dist/tree-C25xYUa6.js.map +1 -0
- package/dist/{tree-component-BW2iCFq9.d.ts → tree-component-mw8x-n2Y.d.ts} +4 -2
- package/dist/tree-component-mw8x-n2Y.d.ts.map +1 -0
- package/dist/tree.d.ts +4 -3
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-CHrXmmQ3.d.ts → vertical-list--tFanQwM.d.ts} +92 -4
- package/dist/vertical-list--tFanQwM.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +2 -2
- package/dist/vertical-list.js +617 -28
- package/dist/vertical-list.js.map +1 -1
- package/docs-user/grid-list.md +14 -0
- package/docs-user/index.json +1 -1
- package/package.json +1 -1
- package/dist/detail-list-CQguZg9h.d.ts.map +0 -1
- package/dist/grid-list-40blkeza.js.map +0 -1
- package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
- package/dist/list-native-drag-controller-B0hWdbtM.js.map +0 -1
- package/dist/list-native-drag-controller-Cmpx7jnx.d.ts.map +0 -1
- package/dist/tree-CVj7TIk2.js.map +0 -1
- package/dist/tree-component-BW2iCFq9.d.ts.map +0 -1
- package/dist/vertical-list-CHrXmmQ3.d.ts.map +0 -1
package/dist/vertical-list.js
CHANGED
|
@@ -2,14 +2,19 @@ import { t as TickledController } from "./tickled-controller-Dh1Uh_vG.js";
|
|
|
2
2
|
import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
|
|
3
3
|
import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
|
|
4
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-C05WjbZh.js";
|
|
5
|
-
import { t as ListNativeDragController } from "./list-native-drag-controller-
|
|
5
|
+
import { t as ListNativeDragController } from "./list-native-drag-controller-D97TYjn1.js";
|
|
6
6
|
import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-SA4piSBi.js";
|
|
7
7
|
import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
|
|
8
8
|
import { LitElement, css, html, render } from "lit";
|
|
9
9
|
import { property } from "lit/decorators.js";
|
|
10
10
|
import { createCommandRegistry } from "@clinth/ui-commands";
|
|
11
11
|
//#region src/vertical-list/vertical-list.ts
|
|
12
|
-
const ITEM_SELECTOR = `li, [data-list-item]`;
|
|
12
|
+
const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data-virtual-spacer])`;
|
|
13
|
+
/**
|
|
14
|
+
* Rows rendered above/below the viewport as scroll margin. Grid-list uses 2;
|
|
15
|
+
* vertical rows are shorter relative to the viewport, so 3.
|
|
16
|
+
*/
|
|
17
|
+
const VIRTUAL_OVERSCAN = 3;
|
|
13
18
|
let VerticalListElement = class VerticalListElement extends LitElement {
|
|
14
19
|
constructor(..._args) {
|
|
15
20
|
super(..._args);
|
|
@@ -17,11 +22,24 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
17
22
|
this.interactionMode = `standard`;
|
|
18
23
|
this.itemWrap = `stop`;
|
|
19
24
|
this.itemsDraggable = false;
|
|
25
|
+
this.keyProperty = ``;
|
|
26
|
+
this.virtualize = false;
|
|
27
|
+
this.itemHeight = 0;
|
|
20
28
|
this.#customWrappers = /* @__PURE__ */ new Map();
|
|
21
29
|
this.#wrappedElements = /* @__PURE__ */ new Set();
|
|
30
|
+
this.#itemData = /* @__PURE__ */ new WeakMap();
|
|
22
31
|
this.#animatedItems = /* @__PURE__ */ new WeakSet();
|
|
32
|
+
this.#allItems = [];
|
|
33
|
+
this.#pool = document.createDocumentFragment();
|
|
34
|
+
this.#windowStart = 0;
|
|
35
|
+
this.#windowEnd = 0;
|
|
36
|
+
this.#pendingAnimate = [];
|
|
37
|
+
this.#suspendAdapterRebuild = false;
|
|
38
|
+
this.#scrollRaf = 0;
|
|
39
|
+
this.#lastViewportH = 0;
|
|
40
|
+
this.#virtualWarned = false;
|
|
23
41
|
this.selection = new ListSelectionController({
|
|
24
|
-
getOrderedItems: () => this
|
|
42
|
+
getOrderedItems: () => this.#selectionOrder(),
|
|
25
43
|
isSelectable: (el) => this._isCheckable(el),
|
|
26
44
|
getModes: () => ({
|
|
27
45
|
selectionMode: this.selectionMode,
|
|
@@ -63,6 +81,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
63
81
|
prepareSelection: (originKey) => this.#prepareNativeDrag(originKey),
|
|
64
82
|
getMetrics: () => ({}),
|
|
65
83
|
getRenderer: () => this.dragPreviewRenderer,
|
|
84
|
+
getDragPreviewSourceResolver: () => this.dragPreviewSource,
|
|
66
85
|
dispatch: (detail) => this._dispatchCancelable(`list-native-drag-start`, detail),
|
|
67
86
|
onActiveChange: (active, originKey) => {
|
|
68
87
|
this.#nativeDragging = active;
|
|
@@ -85,7 +104,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
85
104
|
});
|
|
86
105
|
this.#dragSelect = new ListDragSelectController(this, {
|
|
87
106
|
getSurface: () => this.renderRoot.querySelector(`.container`),
|
|
88
|
-
isEnabled: () => this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
|
|
107
|
+
isEnabled: () => !this.#virtualActive() && this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
|
|
89
108
|
hitTest: (rect) => this.#marqueeHitTest(rect),
|
|
90
109
|
getBaseSelection: () => this.selection.selected,
|
|
91
110
|
onPreview: (preview, rect) => {
|
|
@@ -109,6 +128,14 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
109
128
|
getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
110
129
|
highlightKey: `vertical-list-search`
|
|
111
130
|
}) });
|
|
131
|
+
this.#onScroll = () => {
|
|
132
|
+
if (!this.#virtualActive()) return;
|
|
133
|
+
if (this.#scrollRaf !== 0) return;
|
|
134
|
+
this.#scrollRaf = requestAnimationFrame(() => {
|
|
135
|
+
this.#scrollRaf = 0;
|
|
136
|
+
this.#syncVirtualWindow();
|
|
137
|
+
});
|
|
138
|
+
};
|
|
112
139
|
this._onPointerOver = (e) => {
|
|
113
140
|
const item = this._findListItem(e.composedPath());
|
|
114
141
|
if (item) this.tickled.handlePointerEnter(item);
|
|
@@ -176,10 +203,34 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
176
203
|
}
|
|
177
204
|
#customWrappers;
|
|
178
205
|
#wrappedElements;
|
|
206
|
+
/** Source object behind each adapter-built row (`items` mode). */
|
|
207
|
+
#itemData;
|
|
179
208
|
/** Watches for items added to the light DOM so they can be animated in. */
|
|
180
209
|
#itemObserver;
|
|
181
210
|
/** Items that have already played (or been skipped for) their enter animation. */
|
|
182
211
|
#animatedItems;
|
|
212
|
+
/**
|
|
213
|
+
* Global row order while virtualized — the source of truth both modes
|
|
214
|
+
* write to. Holds canonical rows (checked-mode wrappers, not inners).
|
|
215
|
+
* Non-virtualized mode ignores it and reads the DOM directly.
|
|
216
|
+
*/
|
|
217
|
+
#allItems;
|
|
218
|
+
/** Off-DOM pool for recycled offscreen rows. `querySelectorAll` works on it. */
|
|
219
|
+
#pool;
|
|
220
|
+
/** Current rendered window as registry indices `[start, end)`. */
|
|
221
|
+
#windowStart;
|
|
222
|
+
#windowEnd;
|
|
223
|
+
/** Window rows queued for their enter animation after placement. */
|
|
224
|
+
#pendingAnimate;
|
|
225
|
+
/** Skip one adapter rebuild (updateItem's write-through to `items`). */
|
|
226
|
+
#suspendAdapterRebuild;
|
|
227
|
+
#scrollRaf;
|
|
228
|
+
#resizeObserver;
|
|
229
|
+
#topSpacer;
|
|
230
|
+
#bottomSpacer;
|
|
231
|
+
/** Last measured viewport height — kept so a 0 read can't collapse the window. */
|
|
232
|
+
#lastViewportH;
|
|
233
|
+
#virtualWarned;
|
|
183
234
|
#nativeDragging;
|
|
184
235
|
#postDragSuppressTimer;
|
|
185
236
|
#nativeDrag;
|
|
@@ -250,9 +301,13 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
250
301
|
}
|
|
251
302
|
#effectiveSearchModes() {
|
|
252
303
|
const raw = this.searchModes?.trim();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
304
|
+
let modes;
|
|
305
|
+
if (raw === void 0) modes = [...LIST_SEARCH_MODES];
|
|
306
|
+
else {
|
|
307
|
+
const requested = new Set(raw.split(/\s+/));
|
|
308
|
+
modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
309
|
+
}
|
|
310
|
+
return this.#virtualActive() ? modes.filter((m) => m !== `filter`) : modes;
|
|
256
311
|
}
|
|
257
312
|
#syncSearchCommands() {
|
|
258
313
|
if (!this.#searchRegistry) return;
|
|
@@ -273,6 +328,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
273
328
|
this.#itemObserver.observe(this, { childList: true });
|
|
274
329
|
this.#searchRegistry ??= createCommandRegistry();
|
|
275
330
|
this.#syncSearchCommands();
|
|
331
|
+
this.#ensureResizeObserved();
|
|
276
332
|
}
|
|
277
333
|
disconnectedCallback() {
|
|
278
334
|
super.disconnectedCallback();
|
|
@@ -282,6 +338,12 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
282
338
|
this.removeEventListener(`click`, this._onClick);
|
|
283
339
|
this.removeEventListener(`dblclick`, this._onDblClick);
|
|
284
340
|
this.#itemObserver?.disconnect();
|
|
341
|
+
this.#resizeObserver?.disconnect();
|
|
342
|
+
this.#resizeObserver = void 0;
|
|
343
|
+
if (this.#scrollRaf !== 0) {
|
|
344
|
+
cancelAnimationFrame(this.#scrollRaf);
|
|
345
|
+
this.#scrollRaf = 0;
|
|
346
|
+
}
|
|
285
347
|
this.#unregisterSearchCommands?.();
|
|
286
348
|
this.#unregisterSearchCommands = void 0;
|
|
287
349
|
this.#searchRegistry = void 0;
|
|
@@ -298,6 +360,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
298
360
|
*/
|
|
299
361
|
#onItemsMutated(records) {
|
|
300
362
|
if (!this.isConnected) return;
|
|
363
|
+
if (this.#virtualActive()) {
|
|
364
|
+
this.#syncVirtualWindow(this.#reconcileRecords(records));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
301
367
|
const fresh = [];
|
|
302
368
|
for (const record of records) for (const node of record.addedNodes) {
|
|
303
369
|
if (!(node instanceof HTMLElement) || node.parentElement !== this) continue;
|
|
@@ -307,13 +373,43 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
307
373
|
fresh.push(node);
|
|
308
374
|
}
|
|
309
375
|
if (fresh.length === 0) return;
|
|
310
|
-
|
|
311
|
-
if (fresh.length >= total) return;
|
|
376
|
+
if (this.#isBulkPopulate(fresh.length)) return;
|
|
312
377
|
for (const el of fresh) animateListItemIn(el);
|
|
313
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Whether `freshCount` newly-added rows amount to a bulk populate (first
|
|
381
|
+
* render / wholesale replace) rather than an incremental add — the single
|
|
382
|
+
* threshold both the virtual and non-virtual mutation-observer paths use
|
|
383
|
+
* to decide whether to skip the enter animation.
|
|
384
|
+
*/
|
|
385
|
+
#isBulkPopulate(freshCount) {
|
|
386
|
+
return freshCount >= this.querySelectorAll(ITEM_SELECTOR).length;
|
|
387
|
+
}
|
|
314
388
|
updated(changed) {
|
|
315
389
|
super.updated(changed);
|
|
316
390
|
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
391
|
+
if (changed.has(`virtualize`) || changed.has(`itemHeight`)) {
|
|
392
|
+
const wasActive = (changed.has(`virtualize`) ? changed.get(`virtualize`) : this.virtualize) === true && ((changed.has(`itemHeight`) ? changed.get(`itemHeight`) : this.itemHeight) ?? 0) > 0;
|
|
393
|
+
if (this.#virtualActive()) {
|
|
394
|
+
if (!wasActive) {
|
|
395
|
+
this.#pool = document.createDocumentFragment();
|
|
396
|
+
this.#allItems = [...this.querySelectorAll(ITEM_SELECTOR)];
|
|
397
|
+
for (const el of this.#allItems) this.#animatedItems.add(el);
|
|
398
|
+
this.#pendingAnimate = [];
|
|
399
|
+
}
|
|
400
|
+
this.#syncVirtualWindow(true);
|
|
401
|
+
} else if (wasActive) this.#teardownVirtualWindow();
|
|
402
|
+
else if (this.virtualize && !this.#virtualWarned) {
|
|
403
|
+
this.#virtualWarned = true;
|
|
404
|
+
console.warn(`ixfx-vertical-list: virtualize needs item-height > 0 — running non-virtualized.`);
|
|
405
|
+
}
|
|
406
|
+
this.#syncSearchCommands();
|
|
407
|
+
}
|
|
408
|
+
if ((changed.has(`items`) || changed.has(`formatter`) || changed.has(`keyProperty`)) && this.items !== void 0) {
|
|
409
|
+
if (this.#suspendAdapterRebuild) this.#suspendAdapterRebuild = false;
|
|
410
|
+
else if (this.#virtualActive()) this.#rebuildAdapterVirtual();
|
|
411
|
+
else this.#rebuildFromAdapter();
|
|
412
|
+
}
|
|
317
413
|
if (changed.has(`interactionMode`)) {
|
|
318
414
|
this.selection.clearAnchor();
|
|
319
415
|
const previousSelected = this.selection.selected;
|
|
@@ -350,15 +446,28 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
350
446
|
} else el = item;
|
|
351
447
|
if (options?.notChecked) el.setAttribute(`not-checked`, ``);
|
|
352
448
|
this.appendChild(el);
|
|
449
|
+
if (this.#virtualActive()) {
|
|
450
|
+
this.#syncVirtualWindow(true);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
353
453
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
354
454
|
}
|
|
355
455
|
/**
|
|
356
456
|
* Collapse + fade `item` out, then detach it. The rows below slide up to
|
|
357
457
|
* close the gap. Resolves once the element has actually been removed.
|
|
358
458
|
* Under `prefers-reduced-motion: reduce` it's removed immediately.
|
|
459
|
+
* A pooled (offscreen) row under virtualization is removed immediately.
|
|
359
460
|
*/
|
|
360
461
|
removeItem(item) {
|
|
361
462
|
this.selection.deselect(item);
|
|
463
|
+
if (this.#virtualActive()) return (item.parentNode !== this ? Object.assign(Promise.resolve(), { animation: void 0 }) : animateListItemOut(item)).then(() => {
|
|
464
|
+
const idx = this.#registryIndexOf(item);
|
|
465
|
+
if (idx >= 0) this.#allItems.splice(idx, 1);
|
|
466
|
+
this.#cleanupWrapperRefs(item);
|
|
467
|
+
item.remove();
|
|
468
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
469
|
+
this.#syncVirtualWindow(true);
|
|
470
|
+
});
|
|
362
471
|
return animateListItemOut(item).then(() => {
|
|
363
472
|
item.remove();
|
|
364
473
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
@@ -370,9 +479,15 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
370
479
|
}
|
|
371
480
|
clearItems() {
|
|
372
481
|
this.#customWrappers.clear();
|
|
482
|
+
if (this.#virtualActive()) {
|
|
483
|
+
this.#allItems = [];
|
|
484
|
+
this.#pool = document.createDocumentFragment();
|
|
485
|
+
this.#pendingAnimate = [];
|
|
486
|
+
}
|
|
373
487
|
this.replaceChildren();
|
|
374
488
|
this.selection.reset();
|
|
375
489
|
this._syncItemStates();
|
|
490
|
+
if (this.#virtualActive()) this.#syncVirtualWindow(true);
|
|
376
491
|
}
|
|
377
492
|
/**
|
|
378
493
|
* Replace the entire item list in one call.
|
|
@@ -380,7 +495,16 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
380
495
|
* Preserves `interactionMode` checkbox wrapping.
|
|
381
496
|
*/
|
|
382
497
|
setItems(items) {
|
|
383
|
-
this
|
|
498
|
+
if (this.#virtualActive()) {
|
|
499
|
+
this.#customWrappers.clear();
|
|
500
|
+
this.#pool = document.createDocumentFragment();
|
|
501
|
+
this.#pendingAnimate = [];
|
|
502
|
+
this.#allItems = [...items];
|
|
503
|
+
for (const el of this.#allItems) this.#animatedItems.add(el);
|
|
504
|
+
this.selection.reset();
|
|
505
|
+
this.#syncVirtualWindow(true);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
384
508
|
for (const item of items) this.appendChild(item);
|
|
385
509
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
386
510
|
}
|
|
@@ -428,10 +552,23 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
428
552
|
removeSelectedKeys(keys) {
|
|
429
553
|
this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
|
|
430
554
|
}
|
|
431
|
-
/**
|
|
555
|
+
/**
|
|
556
|
+
* Move the cursor to the item with this key and scroll it into view.
|
|
557
|
+
* Under virtualization the key's index is scrolled into the rendered
|
|
558
|
+
* window first (a no-op for unknown keys).
|
|
559
|
+
*/
|
|
432
560
|
focusKey(key) {
|
|
561
|
+
if (this.#virtualActive()) {
|
|
562
|
+
const index = this.#allItems.findIndex((el) => keyForElement(el) === key);
|
|
563
|
+
if (index < 0) return;
|
|
564
|
+
const ul = this.renderRoot.querySelector(`ul`);
|
|
565
|
+
if (ul) {
|
|
566
|
+
ul.scrollTop = index * this.itemHeight;
|
|
567
|
+
this.#syncVirtualWindow(true);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
433
570
|
const el = this.#itemForKey(key);
|
|
434
|
-
if (!el) return;
|
|
571
|
+
if (!el || el.parentNode !== this) return;
|
|
435
572
|
this.tickled.focus(el);
|
|
436
573
|
el.scrollIntoView({
|
|
437
574
|
block: `nearest`,
|
|
@@ -439,25 +576,435 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
439
576
|
});
|
|
440
577
|
}
|
|
441
578
|
#itemForKey(key) {
|
|
579
|
+
if (this.#virtualActive()) return this.#allItems.find((el) => keyForElement(el) === key);
|
|
442
580
|
for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
|
|
443
581
|
if (this.#wrappedElements.has(el)) continue;
|
|
444
582
|
if (keyForElement(el) === key) return el;
|
|
445
583
|
}
|
|
446
584
|
}
|
|
585
|
+
/** The item carrying this `data-key`, or `undefined`. */
|
|
586
|
+
getItemByKey(key) {
|
|
587
|
+
return this.#itemForKey(key);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Keys of items currently intersecting the viewport — for prioritising
|
|
591
|
+
* viewport-first work (e.g. metadata/thumbnail fetches). A rect-intersection
|
|
592
|
+
* test against the scroll container over the already-created rows (the same
|
|
593
|
+
* cheap trick as `ixfx-detail-list.visibleKeys()`); hidden (filtered-out)
|
|
594
|
+
* and keyless items are omitted.
|
|
595
|
+
*/
|
|
596
|
+
visibleKeys() {
|
|
597
|
+
const container = this.renderRoot.querySelector(`ul`);
|
|
598
|
+
if (!container) return [];
|
|
599
|
+
const box = container.getBoundingClientRect();
|
|
600
|
+
const out = [];
|
|
601
|
+
for (const el of this._getVisibleItems()) {
|
|
602
|
+
const key = keyForElement(el);
|
|
603
|
+
if (key === void 0) continue;
|
|
604
|
+
const r = el.getBoundingClientRect();
|
|
605
|
+
if (r.bottom > box.top && r.top < box.bottom) out.push(key);
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Keys of the currently rendered window (viewport + overscan) — the
|
|
611
|
+
* priority set for prefetching, mirroring `ixfx-grid-list.renderedKeys()`.
|
|
612
|
+
* Use {@link visibleKeys} for the strict viewport intersection. Keyless
|
|
613
|
+
* rows are omitted. Without virtualization, every visible row's key.
|
|
614
|
+
*/
|
|
615
|
+
renderedKeys() {
|
|
616
|
+
if (!this.#virtualActive()) {
|
|
617
|
+
const out = [];
|
|
618
|
+
for (const el of this._getVisibleItems()) {
|
|
619
|
+
const key = keyForElement(el);
|
|
620
|
+
if (key !== void 0) out.push(key);
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
const out = [];
|
|
625
|
+
for (let i = this.#windowStart; i < this.#windowEnd; i++) {
|
|
626
|
+
const el = this.#allItems[i];
|
|
627
|
+
if (!el) continue;
|
|
628
|
+
const key = keyForElement(el);
|
|
629
|
+
if (key !== void 0) out.push(key);
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
447
633
|
*selectedFilter(kind) {
|
|
448
|
-
const
|
|
634
|
+
const source = this.#virtualActive() ? this.#allItems : this.querySelectorAll(ITEM_SELECTOR);
|
|
635
|
+
const isChecked = (el) => {
|
|
636
|
+
if (this.interactionMode !== `checked`) return false;
|
|
637
|
+
return !(this.#customWrappers.get(el) ?? el).hasAttribute(`not-checked`) && this.selection.selected.has(el);
|
|
638
|
+
};
|
|
639
|
+
for (const el of source) {
|
|
640
|
+
if (!this.#virtualActive() && this.#wrappedElements.has(el)) continue;
|
|
641
|
+
if (el.hidden) continue;
|
|
449
642
|
switch (kind) {
|
|
450
|
-
case `selected`:
|
|
451
|
-
|
|
452
|
-
|
|
643
|
+
case `selected`:
|
|
644
|
+
if (this.selection.selected.has(el)) yield el;
|
|
645
|
+
break;
|
|
646
|
+
case `checked`:
|
|
647
|
+
if (isChecked(el)) yield el;
|
|
648
|
+
break;
|
|
649
|
+
case `either`: if (this.selection.selected.has(el) || isChecked(el)) yield el;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Get the source object behind a row built from `items`. Returns
|
|
655
|
+
* `undefined` for declarative / programmatic elements.
|
|
656
|
+
*/
|
|
657
|
+
getItemData(item) {
|
|
658
|
+
return this.#itemData.get(item);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Adapter mode: re-render the row carrying `key` in place — scroll and
|
|
662
|
+
* selection are untouched. Unknown keys, and keys on rows not built from
|
|
663
|
+
* `items`, are ignored. A no-op unless `items` is set.
|
|
664
|
+
*
|
|
665
|
+
* Rows whose formatter returns a full row element are swapped for the fresh
|
|
666
|
+
* element; `<li>` rows are refilled. (A custom-element row with a
|
|
667
|
+
* non-element formatter result cannot be re-rendered in place and is left
|
|
668
|
+
* alone — have the formatter return a replacement element instead.)
|
|
669
|
+
*/
|
|
670
|
+
updateItem(key, data) {
|
|
671
|
+
if (this.items === void 0) return;
|
|
672
|
+
const el = this.getItemByKey(key);
|
|
673
|
+
if (!el || !this.#itemData.has(el)) return;
|
|
674
|
+
const index = this.#adapterIndexOf(el);
|
|
675
|
+
const fresh = this.formatter ? this.formatter(data, index) : data;
|
|
676
|
+
if (el instanceof HTMLLIElement) {
|
|
677
|
+
for (const child of [...el.childNodes]) child.remove();
|
|
678
|
+
this.#fillAdapterItem(el, fresh, data);
|
|
679
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
680
|
+
this._syncItemStates();
|
|
681
|
+
} else if (fresh instanceof Element && this.#isRowElement(fresh)) {
|
|
682
|
+
if (fresh.tagName !== `LI` && !fresh.hasAttribute(`data-list-item`)) fresh.setAttribute(`data-list-item`, ``);
|
|
683
|
+
this.#stampAdapterKey(fresh, data);
|
|
684
|
+
this.#itemData.set(fresh, data);
|
|
685
|
+
this.#itemData.delete(el);
|
|
686
|
+
this.selection.remap((item) => item === el ? fresh : item);
|
|
687
|
+
if (this.#virtualActive()) this.#swapRegistry(el, fresh);
|
|
688
|
+
el.replaceWith(fresh);
|
|
689
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
690
|
+
this._syncItemStates();
|
|
691
|
+
}
|
|
692
|
+
if (this.keyProperty) {
|
|
693
|
+
const sourceIndex = this.items.findIndex((entry) => this.#adapterProperty(entry, this.keyProperty) !== void 0 && String(this.#adapterProperty(entry, this.keyProperty)) === key);
|
|
694
|
+
if (sourceIndex >= 0) {
|
|
695
|
+
const next = [...this.items];
|
|
696
|
+
next[sourceIndex] = data;
|
|
697
|
+
this.#suspendAdapterRebuild = true;
|
|
698
|
+
this.items = next;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
#rebuildFromAdapter() {
|
|
703
|
+
const source = this.items ?? [];
|
|
704
|
+
const keyOf = (el) => el.getAttribute(`data-key`);
|
|
705
|
+
const previousKeys = new Set([...this.selection.selected].map(keyOf).filter((k) => k !== null));
|
|
706
|
+
const anchorKey = this.selection.anchor ? keyOf(this.selection.anchor) : null;
|
|
707
|
+
this.#customWrappers.clear();
|
|
708
|
+
this.replaceChildren(...source.map((item, index) => this.#createAdapterItem(item, index)));
|
|
709
|
+
if (previousKeys.size > 0 || anchorKey !== null) {
|
|
710
|
+
const next = [];
|
|
711
|
+
let anchor;
|
|
712
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
|
|
713
|
+
const key = keyOf(el);
|
|
714
|
+
if (key !== null && previousKeys.has(key)) next.push(el);
|
|
715
|
+
if (key !== null && key === anchorKey) anchor = el;
|
|
716
|
+
}
|
|
717
|
+
this.selection.seed(next, anchor);
|
|
718
|
+
}
|
|
719
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
720
|
+
this._syncItemStates();
|
|
721
|
+
this._syncNativeDraggable();
|
|
722
|
+
this.#nativeDrag.refreshDraggable();
|
|
723
|
+
}
|
|
724
|
+
#createAdapterItem(item, index) {
|
|
725
|
+
const content = this.formatter ? this.formatter(item, index) : item;
|
|
726
|
+
if (content instanceof Element && this.#isRowElement(content)) {
|
|
727
|
+
if (content.tagName !== `LI` && !content.hasAttribute(`data-list-item`)) content.setAttribute(`data-list-item`, ``);
|
|
728
|
+
this.#stampAdapterKey(content, item);
|
|
729
|
+
this.#itemData.set(content, item);
|
|
730
|
+
return content;
|
|
731
|
+
}
|
|
732
|
+
const li = document.createElement(`li`);
|
|
733
|
+
this.#fillAdapterItem(li, content, item);
|
|
734
|
+
return li;
|
|
735
|
+
}
|
|
736
|
+
#fillAdapterItem(li, content, item) {
|
|
737
|
+
if (typeof content === `string`) li.textContent = content;
|
|
738
|
+
else if (content instanceof Node) li.appendChild(content);
|
|
739
|
+
else if (this.formatter && content !== void 0 && content !== null) render(content, li);
|
|
740
|
+
this.#stampAdapterKey(li, item);
|
|
741
|
+
this.#itemData.set(li, item);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Whether a formatter-produced element is a whole row (used directly)
|
|
745
|
+
* rather than row content (rendered into a `<li>`): an `<li>`, already
|
|
746
|
+
* marked `[data-list-item]`, or a custom element.
|
|
747
|
+
*/
|
|
748
|
+
#isRowElement(content) {
|
|
749
|
+
return content.tagName === `LI` || content.hasAttribute(`data-list-item`) || content.tagName.includes(`-`);
|
|
750
|
+
}
|
|
751
|
+
#stampAdapterKey(el, item) {
|
|
752
|
+
if (!this.keyProperty) return;
|
|
753
|
+
const key = this.#adapterProperty(item, this.keyProperty);
|
|
754
|
+
if (key !== void 0 && key !== null) el.setAttribute(`data-key`, String(key));
|
|
755
|
+
}
|
|
756
|
+
#adapterProperty(item, prop) {
|
|
757
|
+
if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
|
|
758
|
+
return item[prop];
|
|
759
|
+
}
|
|
760
|
+
/** Ordinal of an adapter-built row among adapter-built rows in DOM order. */
|
|
761
|
+
#adapterIndexOf(el) {
|
|
762
|
+
if (this.#virtualActive()) return this.#registryIndexOf(el);
|
|
763
|
+
let index = 0;
|
|
764
|
+
for (const item of this.querySelectorAll(ITEM_SELECTOR)) {
|
|
765
|
+
if (item === el) return index;
|
|
766
|
+
if (this.#itemData.has(item)) index++;
|
|
767
|
+
}
|
|
768
|
+
return index;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Adapter rebuild for the virtualized window: same content as
|
|
772
|
+
* {@link #rebuildFromAdapter} but rows become the recycle registry and
|
|
773
|
+
* only the window attaches.
|
|
774
|
+
*/
|
|
775
|
+
#rebuildAdapterVirtual() {
|
|
776
|
+
const source = this.items ?? [];
|
|
777
|
+
const keyOf = (el) => el.getAttribute(`data-key`);
|
|
778
|
+
const previousKeys = new Set([...this.selection.selected].map(keyOf).filter((k) => k !== null));
|
|
779
|
+
const anchorKey = this.selection.anchor ? keyOf(this.selection.anchor) : null;
|
|
780
|
+
this.#customWrappers.clear();
|
|
781
|
+
this.#pool = document.createDocumentFragment();
|
|
782
|
+
this.#pendingAnimate = [];
|
|
783
|
+
this.#allItems = source.map((item, index) => this.#createAdapterItem(item, index));
|
|
784
|
+
for (const el of this.#allItems) this.#animatedItems.add(el);
|
|
785
|
+
if (previousKeys.size > 0 || anchorKey !== null) {
|
|
786
|
+
const next = [];
|
|
787
|
+
let anchor;
|
|
788
|
+
for (const el of this.#allItems) {
|
|
789
|
+
const key = keyOf(el);
|
|
790
|
+
if (key !== null && previousKeys.has(key)) next.push(el);
|
|
791
|
+
if (key !== null && key === anchorKey) anchor = el;
|
|
792
|
+
}
|
|
793
|
+
this.selection.seed(next, anchor);
|
|
794
|
+
}
|
|
795
|
+
this.#syncVirtualWindow(true);
|
|
796
|
+
}
|
|
797
|
+
#virtualActive() {
|
|
798
|
+
return this.virtualize && this.itemHeight > 0;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Selection order: the full registry while virtualized (so `selectAll` and
|
|
802
|
+
* range operations span pooled rows too — they hold live element refs), the
|
|
803
|
+
* attached rows otherwise.
|
|
804
|
+
*/
|
|
805
|
+
#selectionOrder() {
|
|
806
|
+
if (!this.#virtualActive()) return this._getVisibleItems();
|
|
807
|
+
return this.#allItems.filter((el) => {
|
|
808
|
+
if (el.hidden) return false;
|
|
809
|
+
if (this.filterPredicate && !this.filterPredicate(el)) return false;
|
|
810
|
+
return true;
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/** Registry index of a row, resolving wrapped inners to their wrapper. */
|
|
814
|
+
#registryIndexOf(el) {
|
|
815
|
+
const direct = this.#allItems.indexOf(el);
|
|
816
|
+
if (direct >= 0) return direct;
|
|
817
|
+
if (this.#wrappedElements.has(el)) {
|
|
818
|
+
for (const [wrapper, inner] of this.#customWrappers) if (inner === el) return this.#allItems.indexOf(wrapper);
|
|
819
|
+
}
|
|
820
|
+
return -1;
|
|
821
|
+
}
|
|
822
|
+
#swapRegistry(oldEl, newEl) {
|
|
823
|
+
const i = this.#allItems.indexOf(oldEl);
|
|
824
|
+
if (i >= 0) this.#allItems[i] = newEl;
|
|
825
|
+
}
|
|
826
|
+
#cleanupWrapperRefs(el) {
|
|
827
|
+
if (this.#customWrappers.has(el)) {
|
|
828
|
+
const inner = this.#customWrappers.get(el);
|
|
829
|
+
this.#customWrappers.delete(el);
|
|
830
|
+
if (inner) this.#wrappedElements.delete(inner);
|
|
831
|
+
}
|
|
832
|
+
if (this.#wrappedElements.has(el)) {
|
|
833
|
+
for (const [wrapper, inner] of this.#customWrappers) if (inner === el) {
|
|
834
|
+
this.#customWrappers.delete(wrapper);
|
|
835
|
+
break;
|
|
453
836
|
}
|
|
837
|
+
this.#wrappedElements.delete(el);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Global registry index for an attached row, derived from its DOM position:
|
|
842
|
+
* nodes after the bottom spacer are end-of-list appends, nodes before the
|
|
843
|
+
* top spacer are prepends, otherwise window-start plus preceding rows.
|
|
844
|
+
*/
|
|
845
|
+
#globalIndexForAttached(el) {
|
|
846
|
+
let count = 0;
|
|
847
|
+
let sib = el.previousElementSibling;
|
|
848
|
+
while (sib) {
|
|
849
|
+
if (sib === this.#topSpacer) return this.#windowStart + count;
|
|
850
|
+
if (sib === this.#bottomSpacer) return this.#allItems.length;
|
|
851
|
+
if (sib.matches(ITEM_SELECTOR)) count++;
|
|
852
|
+
sib = sib.previousElementSibling;
|
|
853
|
+
}
|
|
854
|
+
return count;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Fold consumer-side DOM edits into the registry. Our own window moves
|
|
858
|
+
* reconcile to no-ops (known nodes already pooled or correctly placed);
|
|
859
|
+
* genuinely new nodes are spliced by DOM position; removed-then-gone nodes
|
|
860
|
+
* are dropped. Returns whether the registry changed.
|
|
861
|
+
*/
|
|
862
|
+
#reconcileRecords(records) {
|
|
863
|
+
let changed = false;
|
|
864
|
+
const fresh = [];
|
|
865
|
+
for (const record of records) {
|
|
866
|
+
for (const node of record.addedNodes) {
|
|
867
|
+
if (!(node instanceof Element) || node.parentNode !== this) continue;
|
|
868
|
+
if (node.hasAttribute(`data-virtual-spacer`) || !node.matches(ITEM_SELECTOR)) continue;
|
|
869
|
+
const known = this.#registryIndexOf(node);
|
|
870
|
+
if (known < 0) {
|
|
871
|
+
const at = Math.min(this.#globalIndexForAttached(node), this.#allItems.length);
|
|
872
|
+
this.#allItems.splice(at, 0, node);
|
|
873
|
+
fresh.push(node);
|
|
874
|
+
changed = true;
|
|
875
|
+
} else if (this.#globalIndexForAttached(node) !== known) {
|
|
876
|
+
const at = Math.min(this.#globalIndexForAttached(node), this.#allItems.length - 1);
|
|
877
|
+
this.#allItems.splice(known, 1);
|
|
878
|
+
this.#allItems.splice(at, 0, node);
|
|
879
|
+
changed = true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const node of record.removedNodes) {
|
|
883
|
+
if (!(node instanceof Element) || node.hasAttribute(`data-virtual-spacer`)) continue;
|
|
884
|
+
const known = this.#registryIndexOf(node);
|
|
885
|
+
if (known < 0) continue;
|
|
886
|
+
if (node.parentNode === this.#pool || node.parentNode === this) continue;
|
|
887
|
+
this.#allItems.splice(known, 1);
|
|
888
|
+
changed = true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (fresh.length > 0) {
|
|
892
|
+
if (this.#isBulkPopulate(fresh.length)) for (const el of fresh) this.#animatedItems.add(el);
|
|
893
|
+
else this.#pendingAnimate.push(...fresh);
|
|
894
|
+
}
|
|
895
|
+
if (changed && this.interactionMode === `checked`) this._syncCheckboxes();
|
|
896
|
+
return changed;
|
|
897
|
+
}
|
|
898
|
+
/** Consume queued observer records first so a sync never drops a row. */
|
|
899
|
+
#drainMutations() {
|
|
900
|
+
const pending = this.#itemObserver?.takeRecords() ?? [];
|
|
901
|
+
return pending.length > 0 ? this.#reconcileRecords(pending) : false;
|
|
902
|
+
}
|
|
903
|
+
#scrollList() {
|
|
904
|
+
return this.renderRoot.querySelector(`ul`);
|
|
905
|
+
}
|
|
906
|
+
#measureViewport(ul) {
|
|
907
|
+
if (!ul) return this.#lastViewportH;
|
|
908
|
+
const h = ul.clientHeight || ul.getBoundingClientRect().height || 0;
|
|
909
|
+
if (h > 0) this.#lastViewportH = h;
|
|
910
|
+
return this.#lastViewportH;
|
|
911
|
+
}
|
|
912
|
+
#ensureSpacers() {
|
|
913
|
+
if (this.#topSpacer && this.#bottomSpacer) return;
|
|
914
|
+
const make = () => {
|
|
915
|
+
const li = document.createElement(`li`);
|
|
916
|
+
li.setAttribute(`data-virtual-spacer`, ``);
|
|
917
|
+
li.setAttribute(`aria-hidden`, `true`);
|
|
918
|
+
return li;
|
|
454
919
|
};
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
920
|
+
this.#topSpacer = make();
|
|
921
|
+
this.#bottomSpacer = make();
|
|
922
|
+
}
|
|
923
|
+
#ensureResizeObserved() {
|
|
924
|
+
const ul = this.#scrollList();
|
|
925
|
+
if (!ul) return;
|
|
926
|
+
this.#resizeObserver ??= new ResizeObserver(() => {
|
|
927
|
+
this.#syncVirtualWindow();
|
|
928
|
+
});
|
|
929
|
+
this.#resizeObserver.observe(ul);
|
|
930
|
+
}
|
|
931
|
+
#onScroll;
|
|
932
|
+
/**
|
|
933
|
+
* Reconcile the attached light DOM with the overscanned window. Rows are
|
|
934
|
+
* moved by identity (never recreated); selection, cursor and component
|
|
935
|
+
* state travel with them.
|
|
936
|
+
*/
|
|
937
|
+
#syncVirtualWindow(force = false) {
|
|
938
|
+
if (!this.#virtualActive() || !this.isConnected) return;
|
|
939
|
+
const changed = this.#drainMutations() || force;
|
|
940
|
+
this.#ensureResizeObserved();
|
|
941
|
+
const count = this.#allItems.length;
|
|
942
|
+
const ul = this.#scrollList();
|
|
943
|
+
const stride = this.itemHeight;
|
|
944
|
+
const viewportH = this.#measureViewport(ul);
|
|
945
|
+
const maxScroll = Math.max(0, count * stride - viewportH);
|
|
946
|
+
const offset = Math.min(Math.max(0, ul ? ul.scrollTop : 0), maxScroll);
|
|
947
|
+
if (ul && Math.abs(ul.scrollTop - offset) > .5) ul.scrollTop = offset;
|
|
948
|
+
const firstVisible = Math.floor(offset / stride);
|
|
949
|
+
const lastVisible = Math.ceil((offset + viewportH) / stride);
|
|
950
|
+
const start = count === 0 ? 0 : Math.max(0, firstVisible - VIRTUAL_OVERSCAN);
|
|
951
|
+
const end = Math.min(count, lastVisible + VIRTUAL_OVERSCAN);
|
|
952
|
+
if (!changed && start === this.#windowStart && end === this.#windowEnd) return;
|
|
953
|
+
if (count === 0) {
|
|
954
|
+
if (this.#allItems.length > 0) this.#pool.append(...this.#allItems);
|
|
955
|
+
this.replaceChildren();
|
|
956
|
+
} else {
|
|
957
|
+
this.#ensureSpacers();
|
|
958
|
+
const top = this.#topSpacer;
|
|
959
|
+
const bottom = this.#bottomSpacer;
|
|
960
|
+
if (!top || !bottom) return;
|
|
961
|
+
top.style.height = `${start * stride}px`;
|
|
962
|
+
bottom.style.height = `${Math.max(0, (count - end) * stride)}px`;
|
|
963
|
+
const rows = this.#allItems.slice(start, end);
|
|
964
|
+
const parked = [...this.#allItems.slice(0, start), ...this.#allItems.slice(end)];
|
|
965
|
+
if (parked.length > 0) this.#pool.append(...parked);
|
|
966
|
+
this.replaceChildren(top, ...rows, bottom);
|
|
967
|
+
}
|
|
968
|
+
this.#windowStart = start;
|
|
969
|
+
this.#windowEnd = end;
|
|
970
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
971
|
+
this._syncItemStates();
|
|
972
|
+
for (let i = start; i < end; i++) {
|
|
973
|
+
const el = this.#allItems[i];
|
|
974
|
+
if (el) this.#applyRowState(el);
|
|
975
|
+
}
|
|
976
|
+
const pending = this.#pendingAnimate;
|
|
977
|
+
this.#pendingAnimate = [];
|
|
978
|
+
for (const el of pending) {
|
|
979
|
+
this.#animatedItems.add(el);
|
|
980
|
+
if (el.parentNode === this) animateListItemIn(el);
|
|
459
981
|
}
|
|
460
982
|
}
|
|
983
|
+
/** Per-attach state the attached sweep can't cover (it only sees the window). */
|
|
984
|
+
#applyRowState(el) {
|
|
985
|
+
if (el instanceof HTMLElement) el.draggable = this.itemsDraggable && this._isCheckable(el);
|
|
986
|
+
el.removeAttribute(`data-marquee`);
|
|
987
|
+
el.removeAttribute(`data-drag-origin`);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Leave virtualization: reattach every registry row in order and drop the
|
|
991
|
+
* spacers and pool. Selection (live refs) and cursor survive untouched.
|
|
992
|
+
*/
|
|
993
|
+
#teardownVirtualWindow() {
|
|
994
|
+
for (const el of this.#allItems) this.#animatedItems.add(el);
|
|
995
|
+
if (this.#allItems.length > 0) this.replaceChildren(...this.#allItems);
|
|
996
|
+
else this.replaceChildren();
|
|
997
|
+
this.#pool = document.createDocumentFragment();
|
|
998
|
+
this.#topSpacer = void 0;
|
|
999
|
+
this.#bottomSpacer = void 0;
|
|
1000
|
+
this.#windowStart = 0;
|
|
1001
|
+
this.#windowEnd = 0;
|
|
1002
|
+
this.#pendingAnimate = [];
|
|
1003
|
+
this._syncCheckboxes();
|
|
1004
|
+
this._syncItemStates();
|
|
1005
|
+
this._syncNativeDraggable();
|
|
1006
|
+
this.#nativeDrag.refreshDraggable();
|
|
1007
|
+
}
|
|
461
1008
|
_getVisibleItems() {
|
|
462
1009
|
return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
|
|
463
1010
|
if (this.#wrappedElements.has(el)) return false;
|
|
@@ -485,9 +1032,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
485
1032
|
_syncCheckboxes() {
|
|
486
1033
|
const inCheckedMode = this.interactionMode === `checked`;
|
|
487
1034
|
const wantsCustom = typeof this.checkboxRenderer === `function`;
|
|
488
|
-
|
|
1035
|
+
const roots = this.#virtualActive() ? [this, this.#pool] : [this];
|
|
1036
|
+
if (inCheckedMode) for (const root of roots) for (const inner of Array.from(root.children)) {
|
|
489
1037
|
if (!inner.hasAttribute(`data-list-item`)) continue;
|
|
490
|
-
if (inner.
|
|
1038
|
+
if (inner.parentNode !== root) continue;
|
|
491
1039
|
const wrapper = document.createElement(`li`);
|
|
492
1040
|
wrapper.className = `ixfx-custom-row`;
|
|
493
1041
|
const key = inner.getAttribute(`data-key`);
|
|
@@ -495,17 +1043,22 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
495
1043
|
if (key !== null) wrapper.setAttribute(`data-key`, key);
|
|
496
1044
|
if (label !== null) wrapper.setAttribute(`data-search-label`, label);
|
|
497
1045
|
inner.removeAttribute(`data-list-item`);
|
|
498
|
-
|
|
1046
|
+
root.insertBefore(wrapper, inner);
|
|
499
1047
|
wrapper.appendChild(inner);
|
|
500
1048
|
this.#customWrappers.set(wrapper, inner);
|
|
501
1049
|
this.#wrappedElements.add(inner);
|
|
1050
|
+
if (this.#itemData.has(inner)) this.#itemData.set(wrapper, this.#itemData.get(inner));
|
|
1051
|
+
if (this.#virtualActive()) this.#swapRegistry(inner, wrapper);
|
|
502
1052
|
}
|
|
503
1053
|
else {
|
|
504
1054
|
for (const [wrapper, inner] of this.#customWrappers) {
|
|
505
|
-
if (wrapper.
|
|
1055
|
+
if (wrapper.parentNode !== this && wrapper.parentNode !== this.#pool) continue;
|
|
506
1056
|
inner.setAttribute(`data-list-item`, ``);
|
|
507
|
-
|
|
1057
|
+
const parent = wrapper.parentNode;
|
|
1058
|
+
if (parent) parent.insertBefore(inner, wrapper);
|
|
508
1059
|
wrapper.remove();
|
|
1060
|
+
this.#itemData.delete(wrapper);
|
|
1061
|
+
if (this.#virtualActive()) this.#swapRegistry(wrapper, inner);
|
|
509
1062
|
}
|
|
510
1063
|
this.#customWrappers.clear();
|
|
511
1064
|
this.#wrappedElements.clear();
|
|
@@ -681,8 +1234,11 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
681
1234
|
outline: 1px solid transparent;
|
|
682
1235
|
}
|
|
683
1236
|
|
|
684
|
-
/* Custom shadow-DOM components handle their own internal layout/padding
|
|
685
|
-
|
|
1237
|
+
/* Custom shadow-DOM components handle their own internal layout/padding.
|
|
1238
|
+
Wrapped in :where() so the selector stays at zero specificity and a
|
|
1239
|
+
consumer's own :host rule (e.g. display: flex) wins without
|
|
1240
|
+
needing !important. */
|
|
1241
|
+
:where(::slotted([data-list-item])) {
|
|
686
1242
|
display: block;
|
|
687
1243
|
margin: 0 var(--list-gutter, 4px);
|
|
688
1244
|
cursor: pointer;
|
|
@@ -692,6 +1248,23 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
692
1248
|
outline: 1px solid transparent;
|
|
693
1249
|
}
|
|
694
1250
|
|
|
1251
|
+
/* Virtualization spacers: plain height holders bracketing the rendered
|
|
1252
|
+
window. Attribute specificity beats ::slotted(li) so none of the row
|
|
1253
|
+
layout (flex, padding, min-height, gutters) applies; pointer events are
|
|
1254
|
+
off so clicks land on the scroll container, not a phantom row. */
|
|
1255
|
+
::slotted([data-virtual-spacer]) {
|
|
1256
|
+
display: block;
|
|
1257
|
+
padding: 0;
|
|
1258
|
+
margin: 0;
|
|
1259
|
+
border: none;
|
|
1260
|
+
min-height: 0;
|
|
1261
|
+
overflow: hidden;
|
|
1262
|
+
cursor: default;
|
|
1263
|
+
pointer-events: none;
|
|
1264
|
+
outline: none;
|
|
1265
|
+
user-select: none;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
695
1268
|
/* In checked mode, only the checkbox column is clickable; the row body
|
|
696
1269
|
gets a default cursor so it doesn't suggest otherwise. */
|
|
697
1270
|
:host([interaction-mode="checked"]) ::slotted(li),
|
|
@@ -766,7 +1339,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
766
1339
|
render() {
|
|
767
1340
|
return html`
|
|
768
1341
|
<div class="container">
|
|
769
|
-
<ul part="list">
|
|
1342
|
+
<ul part="list" @scroll=${this.#onScroll}>
|
|
770
1343
|
<slot></slot>
|
|
771
1344
|
</ul>
|
|
772
1345
|
${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
|
|
@@ -800,6 +1373,22 @@ __decorate([property({
|
|
|
800
1373
|
reflect: true
|
|
801
1374
|
})], VerticalListElement.prototype, "itemsDraggable", void 0);
|
|
802
1375
|
__decorate([property({ attribute: false })], VerticalListElement.prototype, "dragPreviewRenderer", void 0);
|
|
1376
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "dragPreviewSource", void 0);
|
|
1377
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "items", void 0);
|
|
1378
|
+
__decorate([property({
|
|
1379
|
+
type: String,
|
|
1380
|
+
attribute: `key-property`
|
|
1381
|
+
})], VerticalListElement.prototype, "keyProperty", void 0);
|
|
1382
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "formatter", void 0);
|
|
1383
|
+
__decorate([property({
|
|
1384
|
+
type: Boolean,
|
|
1385
|
+
attribute: `virtualize`,
|
|
1386
|
+
reflect: true
|
|
1387
|
+
})], VerticalListElement.prototype, "virtualize", void 0);
|
|
1388
|
+
__decorate([property({
|
|
1389
|
+
type: Number,
|
|
1390
|
+
attribute: `item-height`
|
|
1391
|
+
})], VerticalListElement.prototype, "itemHeight", void 0);
|
|
803
1392
|
VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
|
|
804
1393
|
//#endregion
|
|
805
1394
|
export { VerticalListElement };
|