@ixfx/components 0.11.7 → 0.11.8
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 +153 -2
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +1129 -164
- package/bundle/index.js.map +1 -1
- package/dist/{detail-list-CgkOHYOa.d.ts → detail-list-lE_5UWVl.d.ts} +34 -3
- package/dist/detail-list-lE_5UWVl.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -2
- package/dist/detail-list.js +271 -6
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-D6AfWfwn.d.ts → grid-list-Bc-RkDoO.d.ts} +42 -1
- package/dist/grid-list-Bc-RkDoO.d.ts.map +1 -0
- package/dist/{grid-list-3fGhdaFA.js → grid-list-Bwaf61is.js} +659 -158
- package/dist/grid-list-Bwaf61is.js.map +1 -0
- package/dist/grid-list.d.ts +3 -2
- package/dist/grid-list.js +2 -1
- package/dist/grouping-CUKgmkMm.d.ts +63 -0
- package/dist/grouping-CUKgmkMm.d.ts.map +1 -0
- package/dist/grouping-RYPnHkrS.js +53 -0
- package/dist/grouping-RYPnHkrS.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/list-group-header.d.ts +26 -0
- package/dist/list-group-header.d.ts.map +1 -0
- package/dist/list-group-header.js +122 -0
- package/dist/list-group-header.js.map +1 -0
- package/dist/{list-selection-controller-SA4piSBi.js → list-selection-controller-BeE70lbB.js} +42 -4
- package/dist/list-selection-controller-BeE70lbB.js.map +1 -0
- package/dist/vertical-list.js +1 -1
- package/docs-user/README.md +1 -0
- package/docs-user/detail-list.md +28 -0
- package/docs-user/grid-list.md +35 -3
- package/docs-user/index.json +7 -2
- package/docs-user/list-group-header.md +14 -0
- package/docs-user/llms.txt +1 -0
- package/llms.txt +1 -0
- package/package.json +5 -1
- package/dist/detail-list-CgkOHYOa.d.ts.map +0 -1
- package/dist/grid-list-3fGhdaFA.js.map +0 -1
- package/dist/grid-list-D6AfWfwn.d.ts.map +0 -1
- package/dist/list-selection-controller-SA4piSBi.js.map +0 -1
|
@@ -3,8 +3,10 @@ import { r as IDataProvider } from "./data-provider-BAn_-oR1.js";
|
|
|
3
3
|
import { F as ListDragPreviewSourceResolver, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent } from "./tree-component-DukLqesg.js";
|
|
4
4
|
import { n as ListViewportChangeEvent } from "./list-viewport-types-BRZsLcvp.js";
|
|
5
5
|
import { S as ListSearchController } from "./index-CMsmZ6Or.js";
|
|
6
|
+
import { a as ListGroupEventMap, o as ListGroupInfo, s as ListGroupLabel } from "./grouping-CUKgmkMm.js";
|
|
6
7
|
import { i as ListInteractionMode, l as ListSelectionMode, r as ListElementEventMap } from "./list-selection-types-PjKdhF-a.js";
|
|
7
8
|
import { n as CustomCheckboxOptions, t as AddItemOptions } from "./vertical-list-DTp-UIUE.js";
|
|
9
|
+
import "./list-group-header.js";
|
|
8
10
|
import { LitElement, PropertyValues } from "lit";
|
|
9
11
|
import { CommandId, CommandRegistry } from "@clinth/ui-commands";
|
|
10
12
|
//#region src/detail-list/types.d.ts
|
|
@@ -20,6 +22,12 @@ type DetailSortItem = {
|
|
|
20
22
|
type DetailSortComparator = (a: DetailSortItem, b: DetailSortItem) => number;
|
|
21
23
|
/** Turns one object item into row content — a plain string or a DOM node. */
|
|
22
24
|
type DetailFormatter = (item: unknown, index: number) => string | Node;
|
|
25
|
+
/**
|
|
26
|
+
* How a detail list derives an item's group key: a property name (object
|
|
27
|
+
* items) or `data-<name>` attribute, or a function of the item's data (`undefined`
|
|
28
|
+
* for declarative rows) and its `<li>` element.
|
|
29
|
+
*/
|
|
30
|
+
type DetailGroupBy = string | ((data: unknown, el: Element) => string);
|
|
23
31
|
//#endregion
|
|
24
32
|
//#region src/detail-list/detail-list.d.ts
|
|
25
33
|
/**
|
|
@@ -95,6 +103,21 @@ declare class DetailListElement extends LitElement {
|
|
|
95
103
|
*/
|
|
96
104
|
dragPreviewRenderer: ListDragPreviewRenderer<Element> | undefined;
|
|
97
105
|
dragPreviewSource: ListDragPreviewSourceResolver<Element> | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Group rows under collapsible headers that flow inline in the columns. A
|
|
108
|
+
* string names an item property (object items) or a `data-<name>` attribute;
|
|
109
|
+
* a function derives the key from `(data, element)`. `null` = ungrouped.
|
|
110
|
+
* Groups appear in first-encounter order (after sorting and filtering).
|
|
111
|
+
*/
|
|
112
|
+
groupBy: DetailGroupBy | null;
|
|
113
|
+
/** Header label for a group. Defaults to the key (`(none)` for the empty key). */
|
|
114
|
+
groupLabel: ListGroupLabel | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Let selection span groups. When `false` (default) selecting in one group
|
|
117
|
+
* clears the others and Shift-range / select-all stay inside one group.
|
|
118
|
+
*/
|
|
119
|
+
crossGroupSelection: boolean;
|
|
120
|
+
private _collapsedGroups;
|
|
98
121
|
private _listEl;
|
|
99
122
|
private readonly selection;
|
|
100
123
|
private tickled;
|
|
@@ -106,6 +129,7 @@ declare class DetailListElement extends LitElement {
|
|
|
106
129
|
connectedCallback(): void;
|
|
107
130
|
disconnectedCallback(): void;
|
|
108
131
|
firstUpdated(): void;
|
|
132
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
109
133
|
updated(changed: PropertyValues): void;
|
|
110
134
|
/** The most recently selected item, or `undefined` if nothing is selected. */
|
|
111
135
|
get selectedItem(): Element | undefined;
|
|
@@ -170,6 +194,13 @@ declare class DetailListElement extends LitElement {
|
|
|
170
194
|
/** Move the cursor to the item with this key and scroll it into view. */
|
|
171
195
|
focusKey(key: string): void;
|
|
172
196
|
selectedFilter(kind: `selected` | `checked` | `either`): Generator<Element>;
|
|
197
|
+
/** Groups in display order (empty when ungrouped). */
|
|
198
|
+
get groups(): readonly ListGroupInfo[];
|
|
199
|
+
/** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
|
|
200
|
+
setGroupCollapsed(groupKey: string, collapsed: boolean): void;
|
|
201
|
+
toggleGroup(groupKey: string): void;
|
|
202
|
+
collapseAllGroups(): void;
|
|
203
|
+
expandAllGroups(): void;
|
|
173
204
|
private _getVisibleItems;
|
|
174
205
|
/**
|
|
175
206
|
* Set the column width to fit the widest visible item.
|
|
@@ -215,10 +246,10 @@ declare global {
|
|
|
215
246
|
}
|
|
216
247
|
interface HTMLElementEventMap extends DetailListElementEventMap {}
|
|
217
248
|
}
|
|
218
|
-
interface DetailListElementEventMap extends ListElementEventMap {
|
|
249
|
+
interface DetailListElementEventMap extends ListElementEventMap, ListGroupEventMap {
|
|
219
250
|
'detail-list-viewport-change': ListViewportChangeEvent;
|
|
220
251
|
'list-native-drag-start': ListNativeDragStartEvent<Element>;
|
|
221
252
|
}
|
|
222
253
|
//#endregion
|
|
223
|
-
export {
|
|
224
|
-
//# sourceMappingURL=detail-list-
|
|
254
|
+
export { DetailSortComparator as a, DetailGroupBy as i, DetailListElementEventMap as n, DetailSortItem as o, DetailFormatter as r, DetailSortMode as s, DetailListElement as t };
|
|
255
|
+
//# sourceMappingURL=detail-list-lE_5UWVl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detail-list-lE_5UWVl.d.ts","names":[],"sources":["../src/detail-list/types.ts","../src/detail-list/detail-list.ts"],"mappings":";;;;;;;;;;;;;KACY;;KAGA;WACD,SAAS;WACT;WACA;;;KAIC,wBAAwB,GAAG,gBAAgB,GAAG;;KAG9C,mBAAmB,eAAe,2BAA2B;;;;;;KAc7D,2BAA2B,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;cCkI7C,0BAA0B;;EAErC,eAAe;EAGf,iBAAiB;EAGjB,MAAM;EAGN;EAGA,gBAAgB;EAGhB;EAGA,UAAU;EAGV;EAGA,WAAW;EAGX;;;;;EAOA;;;;;EAOA;;;;;;EAQA;EAGA;EAGA,mBAAmB,IAAI;;;;;;EAQvB;;EAIA,oBAAoB,gCAAgC;;;;;;EAQpD;;;;;;EAQA;;;;;EAOA,qBAAqB,wBAAwB;EAG7C,mBAAmB,8BAA8B;;;;;;;EASjD,SAAS;;EAIT,YAAY;;;;;EAOZ;UAEiB;UAGT;mBAiCS;UAqBT;UAyFA;;WAoGC,QAAM,qBAAA;;MAYX,YAAY;EAoBP;EAkEA;EA+BA;YASU,WAAW,SAAS;EAS9B,QAAQ,SAAS;;MA+DtB,gBAAgB;;MAKhB,iBAAiB,YAAY;;;;;;EASjC;EAIA,QAAQ,MAAM,kBAAkB,UAAU;;;;;EAwB1C,WAAW,MAAM,UAAU;;EAY3B,YAAY,OAAO,SAAS,WAAW;EAIvC;;;;;EAcA,SAAS,gBAAgB;;;;;EAiBzB,YAAY,MAAM;;;;;;;;;;EAalB;;;;;;EAsBA,SAAS,WAAW,WAAW,OAAO;EAItC,OAAO,MAAM;EAIb,WAAW,OAAO,SAAS;EAI3B,SAAS,MAAM;EAIf;EAIA;;MAQI,gBAAgB;;MAKhB;;EAMJ,WAAW,MAAM;;EAKjB,gBAAgB,MAAM;;EAKtB,mBAAmB,MAAM;;EAKzB,SAAS;EAgBP,eAAe,0CAA0C,UAAU;;MA8VjE,mBAAmB;;EAKvB,kBAAkB,kBAAkB;EAYpC,YAAY;EAIZ;EAKA;UAwRQ;;;;;;;;;EAoBF,kBAAkB;UAwBhB;;UAOA;UA2DA;UAiBA;;;;;;;UA2DA;;UAoBA;UAcA;UAYA;UAQA;UA6DA;UAMA;;UAKA;UAIA;UA+EA;UAiBA;UAMA;UAUA;SAUQ,sBAAM;EAyKb,wBAAM;;QAsBT;YACI;IACR,oBAAoB;;YAGZ,4BAA4B;;UAGvB,kCAAkC,qBAAqB;EACtE,+BAA+B;EAC/B,0BAA0B,yBAAyB"}
|
package/dist/detail-list.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { I as ListDragSource, L as ListNativeDragStartDetail, M as ListDragPreview, N as ListDragPreviewContext, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent, j as ListDragEntry } from "./tree-component-DukLqesg.js";
|
|
2
2
|
import { n as ListViewportChangeEvent, t as ListViewportChangeDetail } from "./list-viewport-types-BRZsLcvp.js";
|
|
3
|
+
import { i as ListGroupChangeEvent, l as ListGroupToggleDetail, n as ListGroupBy, o as ListGroupInfo, r as ListGroupChangeDetail, s as ListGroupLabel, u as ListGroupToggleEvent } from "./grouping-CUKgmkMm.js";
|
|
3
4
|
import { n as CustomCheckboxOptions, t as AddItemOptions } from "./vertical-list-DTp-UIUE.js";
|
|
4
|
-
import { a as
|
|
5
|
-
export { type AddItemOptions, type CustomCheckboxOptions, type DetailFormatter, DetailListElement, DetailListElementEventMap, type DetailSortComparator, type DetailSortItem, type DetailSortMode, type ListDragEntry, type ListDragPreview, type ListDragPreviewContext, type ListDragPreviewRenderer, type ListDragSource, type ListNativeDragStartDetail, type ListNativeDragStartEvent, type ListViewportChangeDetail, type ListViewportChangeEvent };
|
|
5
|
+
import { a as DetailSortComparator, i as DetailGroupBy, n as DetailListElementEventMap, o as DetailSortItem, r as DetailFormatter, s as DetailSortMode, t as DetailListElement } from "./detail-list-lE_5UWVl.js";
|
|
6
|
+
export { type AddItemOptions, type CustomCheckboxOptions, type DetailFormatter, type DetailGroupBy, DetailListElement, DetailListElementEventMap, type DetailSortComparator, type DetailSortItem, type DetailSortMode, type ListDragEntry, type ListDragPreview, type ListDragPreviewContext, type ListDragPreviewRenderer, type ListDragSource, type ListGroupBy, type ListGroupChangeDetail, type ListGroupChangeEvent, type ListGroupInfo, type ListGroupLabel, type ListGroupToggleDetail, type ListGroupToggleEvent, type ListNativeDragStartDetail, type ListNativeDragStartEvent, type ListViewportChangeDetail, type ListViewportChangeEvent };
|
package/dist/detail-list.js
CHANGED
|
@@ -4,16 +4,20 @@ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js"
|
|
|
4
4
|
import "./icon-1gpwtPx8.js";
|
|
5
5
|
import { t as DataController } from "./data-controller-DOlmqNKb.js";
|
|
6
6
|
import { c as createElementListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-D91M4Ozq.js";
|
|
7
|
+
import { a as toGroupKey, r as partitionByGroup } from "./grouping-RYPnHkrS.js";
|
|
7
8
|
import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
|
|
8
|
-
import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-
|
|
9
|
+
import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-BeE70lbB.js";
|
|
9
10
|
import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
|
|
10
11
|
import { t as scrollFadeStyles } from "./scroll-fade-BrNUIYnm.js";
|
|
11
12
|
import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
|
|
13
|
+
import "./list-group-header.js";
|
|
12
14
|
import { LitElement, css, html, nothing, render } from "lit";
|
|
13
|
-
import { property, query } from "lit/decorators.js";
|
|
15
|
+
import { property, query, state } from "lit/decorators.js";
|
|
14
16
|
import { createCommandRegistry } from "@clinth/ui-commands";
|
|
15
17
|
//#region src/detail-list/detail-list.ts
|
|
16
18
|
const ITEM_SELECTOR = `:scope > li`;
|
|
19
|
+
const GROUP_HEADER_ATTR = `data-ixfx-group-header`;
|
|
20
|
+
const GROUP_COLLAPSED_ATTR = `data-group-collapsed`;
|
|
17
21
|
const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
|
|
18
22
|
const NAV_KEYS = /* @__PURE__ */ new Set([
|
|
19
23
|
`ArrowDown`,
|
|
@@ -71,14 +75,26 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
71
75
|
this.keyProperty = ``;
|
|
72
76
|
this.itemWrap = `stop`;
|
|
73
77
|
this.itemsDraggable = false;
|
|
78
|
+
this.groupBy = null;
|
|
79
|
+
this.crossGroupSelection = false;
|
|
80
|
+
this._collapsedGroups = /* @__PURE__ */ new Set();
|
|
74
81
|
this.#itemData = /* @__PURE__ */ new WeakMap();
|
|
75
82
|
this.#grid = [];
|
|
76
83
|
this.#positions = /* @__PURE__ */ new Map();
|
|
84
|
+
this.#tops = /* @__PURE__ */ new Map();
|
|
77
85
|
this.#geometryScheduled = false;
|
|
78
86
|
this.#sortRaf = 0;
|
|
79
87
|
this.#viewportScrollRaf = 0;
|
|
80
88
|
this.#animatedItems = /* @__PURE__ */ new WeakSet();
|
|
81
89
|
this.#builtFromProvider = [];
|
|
90
|
+
this.#groupIndex = /* @__PURE__ */ new Map();
|
|
91
|
+
this.#groupList = [];
|
|
92
|
+
this.#headerEls = /* @__PURE__ */ new Map();
|
|
93
|
+
this.#seq = /* @__PURE__ */ new WeakMap();
|
|
94
|
+
this.#seqNext = 0;
|
|
95
|
+
this.#wasGrouped = false;
|
|
96
|
+
this.#groupsScheduled = false;
|
|
97
|
+
this.#groupByChanged = false;
|
|
82
98
|
this.#providerBuilt = 0;
|
|
83
99
|
this.#data = new DataController(this);
|
|
84
100
|
this.selection = new ListSelectionController({
|
|
@@ -89,6 +105,12 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
89
105
|
interactionMode: this.interactionMode
|
|
90
106
|
}),
|
|
91
107
|
getKey: keyForElement,
|
|
108
|
+
getGroupKey: (el) => this.#groupIndex.get(el) ?? null,
|
|
109
|
+
crossGroupSelection: () => this.crossGroupSelection,
|
|
110
|
+
getFallbackGroupKey: () => {
|
|
111
|
+
const t = this.tickled.tickledId;
|
|
112
|
+
return t ? this.#groupIndex.get(t) ?? null : null;
|
|
113
|
+
},
|
|
92
114
|
onChange: (selected, previous) => {
|
|
93
115
|
this._syncItemStates();
|
|
94
116
|
this._dispatch(`list-select`, {
|
|
@@ -170,6 +192,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
170
192
|
if (this._listEl) this._listEl.scrollLeft += dx;
|
|
171
193
|
},
|
|
172
194
|
shouldStart: (event) => {
|
|
195
|
+
if (this.#findGroupHeader(event.composedPath())) return false;
|
|
173
196
|
this.#blankItemPressPending = this.itemsDraggable && this.#isBlankItemPress(event);
|
|
174
197
|
return !this.itemsDraggable || this.#blankItemPressPending;
|
|
175
198
|
}
|
|
@@ -261,7 +284,13 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
261
284
|
};
|
|
262
285
|
this._onClick = (e) => {
|
|
263
286
|
if (this.#dragSelect.isDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
|
|
264
|
-
const
|
|
287
|
+
const path = e.composedPath();
|
|
288
|
+
const header = this.#findGroupHeader(path);
|
|
289
|
+
if (header) {
|
|
290
|
+
this.toggleGroup(header.dataset.groupKey ?? ``);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const item = this._findListItem(path);
|
|
265
294
|
if (!item) return;
|
|
266
295
|
this.focus();
|
|
267
296
|
this._dispatch(`list-item-click`, { item });
|
|
@@ -280,6 +309,8 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
280
309
|
#itemData;
|
|
281
310
|
#grid;
|
|
282
311
|
#positions;
|
|
312
|
+
/** Viewport-relative `top` per item at the last geometry pass — lines rows up across columns. */
|
|
313
|
+
#tops;
|
|
283
314
|
#geometryScheduled;
|
|
284
315
|
#sortRaf;
|
|
285
316
|
#viewportScrollRaf;
|
|
@@ -291,6 +322,17 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
291
322
|
/** Items that have already played (or been skipped for) their enter animation. */
|
|
292
323
|
#animatedItems;
|
|
293
324
|
#builtFromProvider;
|
|
325
|
+
/** Element → group key, for the selection controller. Empty while ungrouped. */
|
|
326
|
+
#groupIndex;
|
|
327
|
+
#groupList;
|
|
328
|
+
#headerEls;
|
|
329
|
+
/** Insertion sequence per item, so regrouping starts from the original order. */
|
|
330
|
+
#seq;
|
|
331
|
+
#seqNext;
|
|
332
|
+
#wasGrouped;
|
|
333
|
+
#groupsScheduled;
|
|
334
|
+
#groupByChanged;
|
|
335
|
+
#groupObserver;
|
|
294
336
|
#providerBuilt;
|
|
295
337
|
#data;
|
|
296
338
|
#nativeDragging;
|
|
@@ -407,6 +449,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
407
449
|
this.#processChildren();
|
|
408
450
|
this.#animateChildMutations(records);
|
|
409
451
|
this.#scheduleGeometry();
|
|
452
|
+
this.#scheduleGroups();
|
|
410
453
|
});
|
|
411
454
|
this.#mutationObserver.observe(this, {
|
|
412
455
|
childList: true,
|
|
@@ -416,6 +459,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
416
459
|
for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
|
|
417
460
|
this.#searchRegistry ??= createCommandRegistry();
|
|
418
461
|
this.#syncSearchCommands();
|
|
462
|
+
this.#scheduleGroups();
|
|
419
463
|
}
|
|
420
464
|
/**
|
|
421
465
|
* Collapse-in items appended to the light DOM (however they got there). A bulk
|
|
@@ -457,6 +501,8 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
457
501
|
this.#resizeObserver = void 0;
|
|
458
502
|
this.#mutationObserver?.disconnect();
|
|
459
503
|
this.#mutationObserver = void 0;
|
|
504
|
+
this.#groupObserver?.disconnect();
|
|
505
|
+
this.#groupObserver = void 0;
|
|
460
506
|
this.#unregisterSearchCommands?.();
|
|
461
507
|
this.#unregisterSearchCommands = void 0;
|
|
462
508
|
this.#searchRegistry = void 0;
|
|
@@ -471,8 +517,22 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
471
517
|
this.#recomputeGeometry();
|
|
472
518
|
this._syncNativeDraggable();
|
|
473
519
|
}
|
|
520
|
+
willUpdate(changed) {
|
|
521
|
+
super.willUpdate(changed);
|
|
522
|
+
if (changed.has(`groupBy`) && this.hasUpdated) {
|
|
523
|
+
if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
|
|
524
|
+
this.#groupByChanged = true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
474
527
|
updated(changed) {
|
|
475
528
|
super.updated(changed);
|
|
529
|
+
if (changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`)) {
|
|
530
|
+
this.#scheduleGroups();
|
|
531
|
+
if (this.#groupByChanged) {
|
|
532
|
+
this.#groupByChanged = false;
|
|
533
|
+
this._dispatch(`list-group-change`, { groupBy: this.groupBy });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
476
536
|
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
477
537
|
if (changed.has(`provider`)) {
|
|
478
538
|
this.#builtFromProvider = [];
|
|
@@ -489,6 +549,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
489
549
|
if (changed.has(`filterPredicate`)) {
|
|
490
550
|
this.#applyFilterPredicate();
|
|
491
551
|
this.#scheduleGeometry();
|
|
552
|
+
this.#scheduleGroups();
|
|
492
553
|
}
|
|
493
554
|
if (changed.has(`interactionMode`)) {
|
|
494
555
|
this.selection.clearAnchor();
|
|
@@ -528,6 +589,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
528
589
|
this.#ensureIcon(el);
|
|
529
590
|
if (this.sort !== `none`) this.#applySort();
|
|
530
591
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
592
|
+
this.#scheduleGroups();
|
|
531
593
|
}
|
|
532
594
|
/**
|
|
533
595
|
* Collapse + fade `item` out, then detach it; the rows after it slide up to
|
|
@@ -537,6 +599,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
537
599
|
this.selection.deselect(item);
|
|
538
600
|
return animateListItemOut(item).then(() => {
|
|
539
601
|
item.remove();
|
|
602
|
+
this.#scheduleGroups();
|
|
540
603
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
541
604
|
this.#recomputeGeometry();
|
|
542
605
|
});
|
|
@@ -547,6 +610,8 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
547
610
|
}
|
|
548
611
|
clearItems() {
|
|
549
612
|
this.replaceChildren();
|
|
613
|
+
this.#headerEls.clear();
|
|
614
|
+
this.#scheduleGroups();
|
|
550
615
|
this.selection.reset();
|
|
551
616
|
this.#builtFromProvider = [];
|
|
552
617
|
this.#providerBuilt = 0;
|
|
@@ -564,6 +629,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
564
629
|
}
|
|
565
630
|
if (this.sort !== `none`) this.#applySort();
|
|
566
631
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
632
|
+
this.#scheduleGroups();
|
|
567
633
|
}
|
|
568
634
|
/**
|
|
569
635
|
* Get the source object behind an `<li>` built from an `items` array or
|
|
@@ -683,6 +749,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
683
749
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
684
750
|
this._syncItemStates();
|
|
685
751
|
this.#scheduleGeometry();
|
|
752
|
+
this.#scheduleGroups();
|
|
686
753
|
}
|
|
687
754
|
#createItemEl(item, index) {
|
|
688
755
|
const li = document.createElement(`li`);
|
|
@@ -734,6 +801,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
734
801
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
735
802
|
this._syncItemStates();
|
|
736
803
|
this.#scheduleGeometry();
|
|
804
|
+
this.#scheduleGroups();
|
|
737
805
|
} else {
|
|
738
806
|
this.#providerBuilt = 0;
|
|
739
807
|
this.#rebuildFromObjects(items);
|
|
@@ -752,6 +820,16 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
752
820
|
if (this.sort === `none`) return;
|
|
753
821
|
const rows = [...this.querySelectorAll(ITEM_SELECTOR)];
|
|
754
822
|
if (rows.length < 2) return;
|
|
823
|
+
if (this.#groupingActive()) {
|
|
824
|
+
this.#scheduleGroups();
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
for (const el of this.#sortRows(rows)) this.appendChild(el);
|
|
828
|
+
this.#scheduleGeometry();
|
|
829
|
+
this.#scheduleGroups();
|
|
830
|
+
}
|
|
831
|
+
/** Stable sort of `rows` by the current `sort` / `sortBy` / `sortComparator`. */
|
|
832
|
+
#sortRows(rows) {
|
|
755
833
|
const dir = this.sort === `descending` ? -1 : 1;
|
|
756
834
|
const meta = (el) => ({
|
|
757
835
|
element: el,
|
|
@@ -781,9 +859,147 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
781
859
|
i
|
|
782
860
|
}));
|
|
783
861
|
indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
|
|
784
|
-
|
|
862
|
+
return indexed.map(({ el }) => el);
|
|
863
|
+
}
|
|
864
|
+
#groupingActive() {
|
|
865
|
+
return this.groupBy !== null && this.groupBy !== void 0 && this.groupBy !== ``;
|
|
866
|
+
}
|
|
867
|
+
/** Coalesce regrouping to one pass per microtask (streaming batches, search keystrokes). */
|
|
868
|
+
#scheduleGroups() {
|
|
869
|
+
if (this.#groupsScheduled || !this.#groupingActive() && !this.#wasGrouped) return;
|
|
870
|
+
this.#groupsScheduled = true;
|
|
871
|
+
queueMicrotask(() => {
|
|
872
|
+
this.#groupsScheduled = false;
|
|
873
|
+
this.#syncGroups();
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
#groupValue(el, name) {
|
|
877
|
+
return this.#propertyValue(this.getItemData(el), name) ?? el.getAttribute(`data-${name}`);
|
|
878
|
+
}
|
|
879
|
+
#groupKeyOf(el) {
|
|
880
|
+
const by = this.groupBy;
|
|
881
|
+
if (typeof by === `string`) return toGroupKey(this.#groupValue(el, by));
|
|
882
|
+
return toGroupKey(by(this.getItemData(el), el));
|
|
883
|
+
}
|
|
884
|
+
#isItemVisible(el) {
|
|
885
|
+
return !el.hidden && (!this.filterPredicate || this.filterPredicate(el));
|
|
886
|
+
}
|
|
887
|
+
#headerFor(key) {
|
|
888
|
+
let header = this.#headerEls.get(key);
|
|
889
|
+
if (!header) {
|
|
890
|
+
header = document.createElement(`ixfx-list-group-header`);
|
|
891
|
+
header.setAttribute(GROUP_HEADER_ATTR, ``);
|
|
892
|
+
header.dataset.groupKey = key;
|
|
893
|
+
this.#headerEls.set(key, header);
|
|
894
|
+
}
|
|
895
|
+
return header;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Bring the light DOM into grouped order: original order → sort → partition
|
|
899
|
+
* into contiguous groups, each preceded by a header row. Idempotent — it only
|
|
900
|
+
* touches the DOM when the order actually changes, so it never loops through
|
|
901
|
+
* the mutation observer. Ungrouping restores the original (sorted) order.
|
|
902
|
+
*/
|
|
903
|
+
#syncGroups() {
|
|
904
|
+
const active = this.#groupingActive();
|
|
905
|
+
if (!active && !this.#wasGrouped) return;
|
|
906
|
+
const items = [...this.querySelectorAll(ITEM_SELECTOR)];
|
|
907
|
+
for (const el of items) if (!this.#seq.has(el)) this.#seq.set(el, this.#seqNext++);
|
|
908
|
+
let ordered = items.toSorted((a, b) => this.#seq.get(a) - this.#seq.get(b));
|
|
909
|
+
if (this.sort !== `none` && ordered.length > 1) ordered = this.#sortRows(ordered);
|
|
910
|
+
const nodes = [];
|
|
911
|
+
const info = [];
|
|
912
|
+
const index = /* @__PURE__ */ new Map();
|
|
913
|
+
const liveHeaders = /* @__PURE__ */ new Set();
|
|
914
|
+
if (active) {
|
|
915
|
+
const label = this.groupLabel;
|
|
916
|
+
const parts = partitionByGroup(ordered, (el) => this.#groupKeyOf(el), label ? (key, els) => label(key, els.map((el) => this.getItemData(el) ?? el)) : void 0);
|
|
917
|
+
for (const part of parts) {
|
|
918
|
+
const count = part.items.filter((el) => this.#isItemVisible(el)).length;
|
|
919
|
+
const collapsed = count > 0 && this._collapsedGroups.has(part.key);
|
|
920
|
+
if (count > 0) {
|
|
921
|
+
const header = this.#headerFor(part.key);
|
|
922
|
+
header.label = part.label;
|
|
923
|
+
header.count = count;
|
|
924
|
+
header.collapsed = collapsed;
|
|
925
|
+
liveHeaders.add(part.key);
|
|
926
|
+
nodes.push(header);
|
|
927
|
+
info.push({
|
|
928
|
+
key: part.key,
|
|
929
|
+
label: part.label,
|
|
930
|
+
count,
|
|
931
|
+
collapsed: this._collapsedGroups.has(part.key)
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
for (const el of part.items) {
|
|
935
|
+
index.set(el, part.key);
|
|
936
|
+
el.toggleAttribute(GROUP_COLLAPSED_ATTR, collapsed);
|
|
937
|
+
nodes.push(el);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
for (const el of ordered) el.removeAttribute(GROUP_COLLAPSED_ATTR);
|
|
942
|
+
nodes.push(...ordered);
|
|
943
|
+
}
|
|
944
|
+
for (const [key, header] of this.#headerEls) if (!liveHeaders.has(key)) {
|
|
945
|
+
header.remove();
|
|
946
|
+
this.#headerEls.delete(key);
|
|
947
|
+
}
|
|
948
|
+
this.#groupIndex = index;
|
|
949
|
+
this.#groupList = info;
|
|
950
|
+
this.#wasGrouped = active;
|
|
951
|
+
const current = [...this.children].filter((c) => c.matches(`li`) || c.hasAttribute(GROUP_HEADER_ATTR));
|
|
952
|
+
if (current.length !== nodes.length || current.some((c, i) => c !== nodes[i])) this.append(...nodes);
|
|
953
|
+
if (this.tickled.tickledId?.hasAttribute(GROUP_COLLAPSED_ATTR)) this.tickled.focus(void 0);
|
|
954
|
+
this.#syncGroupObserver(active);
|
|
785
955
|
this.#scheduleGeometry();
|
|
786
956
|
}
|
|
957
|
+
/** Filter-search hides rows via `hidden` on the `<li>`s — regroup when that changes. */
|
|
958
|
+
#syncGroupObserver(active) {
|
|
959
|
+
if (!active) {
|
|
960
|
+
this.#groupObserver?.disconnect();
|
|
961
|
+
this.#groupObserver = void 0;
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (this.#groupObserver || !this.isConnected) return;
|
|
965
|
+
this.#groupObserver = new MutationObserver(() => this.#scheduleGroups());
|
|
966
|
+
this.#groupObserver.observe(this, {
|
|
967
|
+
subtree: true,
|
|
968
|
+
attributes: true,
|
|
969
|
+
attributeFilter: [`hidden`]
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/** Groups in display order (empty when ungrouped). */
|
|
973
|
+
get groups() {
|
|
974
|
+
return this.#groupList;
|
|
975
|
+
}
|
|
976
|
+
/** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
|
|
977
|
+
setGroupCollapsed(groupKey, collapsed) {
|
|
978
|
+
if (this._collapsedGroups.has(groupKey) === collapsed) return;
|
|
979
|
+
const next = new Set(this._collapsedGroups);
|
|
980
|
+
if (collapsed) next.add(groupKey);
|
|
981
|
+
else next.delete(groupKey);
|
|
982
|
+
this._collapsedGroups = next;
|
|
983
|
+
this._dispatch(`list-group-toggle`, {
|
|
984
|
+
groupKey,
|
|
985
|
+
collapsed
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
toggleGroup(groupKey) {
|
|
989
|
+
this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
|
|
990
|
+
}
|
|
991
|
+
collapseAllGroups() {
|
|
992
|
+
for (const g of this.#groupList) this.setGroupCollapsed(g.key, true);
|
|
993
|
+
}
|
|
994
|
+
expandAllGroups() {
|
|
995
|
+
for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
|
|
996
|
+
}
|
|
997
|
+
#findGroupHeader(path) {
|
|
998
|
+
for (const target of path) {
|
|
999
|
+
if (target === this) break;
|
|
1000
|
+
if (target instanceof HTMLElement && target.parentElement === this && target.hasAttribute(GROUP_HEADER_ATTR)) return target;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
787
1003
|
#scheduleGeometry() {
|
|
788
1004
|
if (this.#geometryScheduled) return;
|
|
789
1005
|
this.#geometryScheduled = true;
|
|
@@ -795,11 +1011,13 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
795
1011
|
#recomputeGeometry() {
|
|
796
1012
|
this.#grid = [];
|
|
797
1013
|
this.#positions.clear();
|
|
1014
|
+
this.#tops.clear();
|
|
798
1015
|
if (!this.isConnected) return;
|
|
799
1016
|
const columns = [];
|
|
800
1017
|
for (const el of this._getVisibleItems()) {
|
|
801
1018
|
const rect = el.getBoundingClientRect();
|
|
802
1019
|
if (rect.width === 0 && rect.height === 0) continue;
|
|
1020
|
+
this.#tops.set(el, rect.top);
|
|
803
1021
|
let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
|
|
804
1022
|
if (ci === -1) {
|
|
805
1023
|
columns.push({
|
|
@@ -920,6 +1138,30 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
920
1138
|
const next = this.#findNextCheckable(cur.col, cur.row, 0, -1);
|
|
921
1139
|
if (next) this.#navigateTo(next);
|
|
922
1140
|
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Left/Right target. Ungrouped lists keep the row-index rule. Grouped lists
|
|
1143
|
+
* have headers at arbitrary heights, so an item's row index no longer equals
|
|
1144
|
+
* its visual row — pick the item in the neighbouring column whose top is
|
|
1145
|
+
* nearest the cursor's (ties go to the upper one).
|
|
1146
|
+
*/
|
|
1147
|
+
#horizontalTarget(from, dir) {
|
|
1148
|
+
if (this.#groupList.length === 0) return this.#findNextCheckable(from.col, from.row, dir, 0);
|
|
1149
|
+
const column = this.#grid[from.col + dir];
|
|
1150
|
+
const current = this.tickled.tickledId;
|
|
1151
|
+
const top = current ? this.#tops.get(current) : void 0;
|
|
1152
|
+
if (!column || top === void 0) return void 0;
|
|
1153
|
+
let best;
|
|
1154
|
+
let bestDelta = Number.POSITIVE_INFINITY;
|
|
1155
|
+
for (const el of column) {
|
|
1156
|
+
if (!this._isCheckable(el)) continue;
|
|
1157
|
+
const delta = Math.abs((this.#tops.get(el) ?? Number.POSITIVE_INFINITY) - top);
|
|
1158
|
+
if (delta < bestDelta) {
|
|
1159
|
+
best = el;
|
|
1160
|
+
bestDelta = delta;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return best;
|
|
1164
|
+
}
|
|
923
1165
|
#navigateRight() {
|
|
924
1166
|
this.#recomputeGeometry();
|
|
925
1167
|
const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
|
|
@@ -927,7 +1169,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
927
1169
|
this.#navigateTo(this.#findFirstCheckable());
|
|
928
1170
|
return;
|
|
929
1171
|
}
|
|
930
|
-
const next = this.#
|
|
1172
|
+
const next = this.#horizontalTarget(cur, 1);
|
|
931
1173
|
if (next) this.#navigateTo(next);
|
|
932
1174
|
else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
|
|
933
1175
|
}
|
|
@@ -938,13 +1180,14 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
938
1180
|
this.#navigateTo(this.#findFirstCheckable());
|
|
939
1181
|
return;
|
|
940
1182
|
}
|
|
941
|
-
const next = this.#
|
|
1183
|
+
const next = this.#horizontalTarget(cur, -1);
|
|
942
1184
|
if (next) this.#navigateTo(next);
|
|
943
1185
|
else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
|
|
944
1186
|
}
|
|
945
1187
|
_getVisibleItems() {
|
|
946
1188
|
return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
|
|
947
1189
|
if (el.hidden) return false;
|
|
1190
|
+
if (el.hasAttribute(GROUP_COLLAPSED_ATTR)) return false;
|
|
948
1191
|
if (this.filterPredicate && !this.filterPredicate(el)) return false;
|
|
949
1192
|
return true;
|
|
950
1193
|
});
|
|
@@ -1233,6 +1476,20 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
1233
1476
|
display: none !important;
|
|
1234
1477
|
}
|
|
1235
1478
|
|
|
1479
|
+
/* Rows of a collapsed group leave the flow; their header stays. */
|
|
1480
|
+
::slotted([data-group-collapsed]) {
|
|
1481
|
+
display: none !important;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/* Group headers are rows in the column flow, the same size as an item. */
|
|
1485
|
+
::slotted(ixfx-list-group-header) {
|
|
1486
|
+
box-sizing: border-box;
|
|
1487
|
+
flex: none;
|
|
1488
|
+
width: var(--detail-list-column-width, 220px);
|
|
1489
|
+
height: var(--detail-list-item-height, 28px);
|
|
1490
|
+
--list-group-header-height: var(--detail-list-group-header-height, var(--detail-list-item-height, 28px));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1236
1493
|
::slotted(li) {
|
|
1237
1494
|
box-sizing: border-box;
|
|
1238
1495
|
display: flex;
|
|
@@ -1409,6 +1666,14 @@ __decorate([property({
|
|
|
1409
1666
|
})], DetailListElement.prototype, "itemsDraggable", void 0);
|
|
1410
1667
|
__decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewRenderer", void 0);
|
|
1411
1668
|
__decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewSource", void 0);
|
|
1669
|
+
__decorate([property({ attribute: `group-by` })], DetailListElement.prototype, "groupBy", void 0);
|
|
1670
|
+
__decorate([property({ attribute: false })], DetailListElement.prototype, "groupLabel", void 0);
|
|
1671
|
+
__decorate([property({
|
|
1672
|
+
type: Boolean,
|
|
1673
|
+
attribute: `cross-group-selection`,
|
|
1674
|
+
reflect: true
|
|
1675
|
+
})], DetailListElement.prototype, "crossGroupSelection", void 0);
|
|
1676
|
+
__decorate([state()], DetailListElement.prototype, "_collapsedGroups", void 0);
|
|
1412
1677
|
__decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
|
|
1413
1678
|
DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
|
|
1414
1679
|
//#endregion
|