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