@c2n/table 0.0.7
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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/table-column-Dsv4HKrd.js +92 -0
- package/dist/table-column.js +2 -0
- package/dist/table-types.js +18 -0
- package/dist/table.js +1029 -0
- package/package.json +88 -0
- package/react.d.ts +26 -0
- package/react.js +3 -0
- package/types/src/table-column.d.ts +72 -0
- package/types/src/table-column.d.ts.map +1 -0
- package/types/src/table-types.d.ts +112 -0
- package/types/src/table-types.d.ts.map +1 -0
- package/types/src/table.d.ts +272 -0
- package/types/src/table.d.ts.map +1 -0
- package/vue.d.ts +57 -0
- package/vue.js +3 -0
package/dist/table.js
ADDED
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
import { n as TableColumn, r as __decorate, t as COLUMN_CHANGE_EVENT } from "./table-column-Dsv4HKrd.js";
|
|
2
|
+
import { getFieldValue, sortModelConverter } from "./table-types.js";
|
|
3
|
+
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
|
4
|
+
import { property, query, state } from "lit/decorators.js";
|
|
5
|
+
import { customElement } from "@c2n/core/element-helper.js";
|
|
6
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
7
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
8
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
9
|
+
import { arrayPropertyConverter, jsonPropertyConverter } from "@c2n/core/lit-helper.js";
|
|
10
|
+
import { VirtualScrollController } from "@c2n/core/controllers/virtual-scroll.js";
|
|
11
|
+
import { defaultCompare } from "@c2n/core/data-helper.js";
|
|
12
|
+
import { provide } from "@lit/context";
|
|
13
|
+
import { PAGER_CONNECT_EVENT, pagerContext } from "@c2n/core/contexts/pager.js";
|
|
14
|
+
import "@c2n/checkbox";
|
|
15
|
+
import "@c2n/spinner";
|
|
16
|
+
//#region src/table.scss?inline
|
|
17
|
+
var table_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n min-height: 0;\n max-height: var(--c2-table--max-height,none);\n overflow: hidden;\n background: var(--c2-table--background,#ffffff);\n color: var(--c2-table--color,#18181b);\n font-size: var(--c2-table--font-size,14px);\n border-top: var(--c2-table--border-top,1px solid #e4e4e7);\n border-right: var(--c2-table--border-right,1px solid #e4e4e7);\n border-bottom: var(--c2-table--border-bottom,1px solid #e4e4e7);\n border-left: var(--c2-table--border-left,1px solid #e4e4e7);\n border-top-left-radius: var(--c2-table--border-top-left-radius,8px);\n border-top-right-radius: var(--c2-table--border-top-right-radius,8px);\n border-bottom-left-radius: var(--c2-table--border-bottom-left-radius,8px);\n border-bottom-right-radius: var(--c2-table--border-bottom-right-radius,8px);\n box-shadow: var(--c2-table--box-shadow,none);\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.definitions {\n display: none;\n}\n\n.toolbar,\n.footer {\n flex: 0 0 auto;\n}\n\n.toolbar {\n background: var(--c2-table__toolbar--background,transparent);\n padding: var(--c2-table__toolbar--padding,8px 12px);\n border-bottom: var(--c2-table__toolbar--border-bottom,1px solid #e4e4e7);\n}\n\n.footer {\n background: var(--c2-table__footer--background,transparent);\n padding: var(--c2-table__footer--padding,8px 12px);\n border-top: var(--c2-table__footer--border-top,1px solid #e4e4e7);\n}\n\n.viewport {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n overflow-anchor: none;\n}\n\n.grid {\n display: grid;\n grid-template-columns: var(--_grid-template, 1fr);\n align-content: start;\n}\n\n.spacer {\n grid-column: 1/-1;\n}\n\n.row {\n display: grid;\n grid-column: 1/-1;\n grid-template-columns: subgrid;\n height: var(--c2-table__row--height,36px);\n border-bottom: var(--c2-table__row--border-bottom,1px solid #e4e4e7);\n background: var(--c2-table--background,#ffffff);\n}\n\n.row--header {\n position: sticky;\n top: 0;\n z-index: 3;\n height: var(--c2-table__header--height,36px);\n background: var(--c2-table__header--background,#fafafa);\n border-bottom: var(--c2-table__header--border-bottom,1px solid #e4e4e7);\n}\n\n:host([stripe]) .row--odd {\n background: var(--c2-table__row__odd--background,#fafafa);\n}\n\n.row--clickable {\n cursor: pointer;\n}\n\n.row:not(.row--header):not(.row--state):hover {\n background: var(--c2-table__row__hover--background,#fafafa);\n}\n\n.row--selected,\n.row--selected:hover {\n background: var(--c2-table__row__selected--background,#edf1fe);\n color: var(--c2-table__row__selected--color,#18181b);\n}\n\n.cell {\n display: flex;\n align-items: center;\n gap: var(--c2-table__cell--gap,8px);\n padding: var(--c2-table__cell--padding,0 12px);\n box-sizing: border-box;\n min-width: 0;\n overflow: hidden;\n font-size: var(--c2-table__cell--font-size,14px);\n color: var(--c2-table__cell--color,#18181b);\n border-right: var(--c2-table__cell--border-right,none);\n}\n\n.cell:focus {\n outline: none;\n}\n\n.cell:focus-visible {\n outline: var(--c2-table__cell__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: var(--c2-table__cell__focus--outline-offset,-2px);\n}\n\n.cell-content {\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.cell--align-center {\n justify-content: center;\n}\n\n.cell--align-end {\n justify-content: flex-end;\n}\n\n.cell--pinned {\n position: sticky;\n z-index: 2;\n background: inherit;\n}\n\n.cell--pinned-edge-start {\n box-shadow: var(--c2-table__pinned-start--box-shadow,1px 0 0 0 #e4e4e7);\n}\n\n.cell--pinned-edge-end {\n box-shadow: var(--c2-table__pinned-end--box-shadow,-1px 0 0 0 #e4e4e7);\n}\n\n.cell--selection {\n justify-content: center;\n padding: 0;\n}\n\n.cell--header {\n position: relative;\n gap: var(--c2-table__header-cell--gap,6px);\n padding: var(--c2-table__header-cell--padding,0 12px);\n color: var(--c2-table__header--color,#71717a);\n font-size: var(--c2-table__header--font-size,12px);\n font-weight: var(--c2-table__header--font-weight,600);\n letter-spacing: var(--c2-table__header--letter-spacing,0.02em);\n user-select: none;\n}\n\n.row--header .cell--pinned {\n position: sticky;\n z-index: 4;\n}\n\n.cell--sortable {\n cursor: pointer;\n}\n\n.cell--sortable:hover {\n background: var(--c2-table__header-cell__hover--background,#f4f4f5);\n}\n\n.cell--sorted {\n color: var(--c2-table__header-cell__sorted--color,#18181b);\n}\n\n.sort-icon {\n flex: 0 0 auto;\n width: var(--c2-table__sort-icon--width,14px);\n height: var(--c2-table__sort-icon--height,14px);\n color: var(--c2-table__sort-icon--color,#a1a1aa);\n opacity: 0;\n}\n\n.cell--sortable:hover .sort-icon {\n opacity: 1;\n}\n\n.sort-icon--active {\n color: var(--c2-table__sort-icon__active--color,rgb(2, 101, 220));\n opacity: 1;\n}\n\n.sort-icon--desc {\n transform: rotate(180deg);\n}\n\n.sort-order {\n flex: 0 0 auto;\n font-size: 10px;\n font-weight: 600;\n color: var(--c2-table__sort-icon__active--color,rgb(2, 101, 220));\n}\n\n.resizer {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n height: 100%;\n width: var(--c2-table__resizer--width,9px);\n cursor: col-resize;\n touch-action: none;\n}\n\n.resizer::after {\n content: \"\";\n width: var(--c2-table__resizer--line-width,1px);\n height: var(--c2-table__resizer--height,56%);\n background: var(--c2-table__resizer--color,#e4e4e7);\n border-radius: 999px;\n transition: height 120ms ease, width 120ms ease, background-color 120ms ease;\n}\n\n.resizer:hover::after,\n.resizer--active::after {\n width: var(--c2-table__resizer__hover--line-width,2px);\n height: var(--c2-table__resizer__hover--height,100%);\n background: var(--c2-table__resizer__hover--color,rgb(2, 101, 220));\n}\n\n@media (prefers-reduced-motion: reduce) {\n .resizer::after {\n transition: none;\n }\n}\n.skeleton {\n display: block;\n width: 64px;\n height: 10px;\n background: var(--c2-table__skeleton--background,#f4f4f5);\n border-radius: var(--c2-table__skeleton--border-radius,4px);\n}\n\n.row--state {\n height: auto;\n border-bottom: none;\n background: var(--c2-table--background,#ffffff);\n}\n\n.cell--state {\n grid-column: 1/-1;\n justify-content: center;\n padding: var(--c2-table__state--padding,32px 12px);\n color: var(--c2-table__state--color,#71717a);\n font-size: var(--c2-table__state--font-size,14px);\n white-space: normal;\n overflow: visible;\n}\n\n.cell--state-error {\n color: var(--c2-table__state__error--color,rgb(211, 21, 16));\n}";
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/table.ts
|
|
20
|
+
/** Field of the synthetic checkbox column, so it can live in the same render pipeline as the real columns. */
|
|
21
|
+
var SELECTION_FIELD = "__c2-selection";
|
|
22
|
+
var HEADER_ROW = -1;
|
|
23
|
+
function numberFormatOptions(base, extra) {
|
|
24
|
+
return Object.assign({}, base, extra ?? {});
|
|
25
|
+
}
|
|
26
|
+
function dateFormatOptions(base, extra) {
|
|
27
|
+
return Object.assign({}, base, extra ?? {});
|
|
28
|
+
}
|
|
29
|
+
function humanize(field) {
|
|
30
|
+
const spaced = (field.split(".").pop() ?? field).replace(/[_-]+/g, " ").replace(/([a-z\d])([A-Z])/g, "$1 $2");
|
|
31
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
32
|
+
}
|
|
33
|
+
var Table = class Table extends LitElement {
|
|
34
|
+
constructor(..._args) {
|
|
35
|
+
super(..._args);
|
|
36
|
+
this.rows = [];
|
|
37
|
+
this.rowKey = "";
|
|
38
|
+
this.selection = "none";
|
|
39
|
+
this.checkboxSelection = false;
|
|
40
|
+
this.value = [];
|
|
41
|
+
this.sortModel = [];
|
|
42
|
+
this.multiSort = false;
|
|
43
|
+
this.sortable = false;
|
|
44
|
+
this.resizable = false;
|
|
45
|
+
this.stripe = false;
|
|
46
|
+
this.virtual = "auto";
|
|
47
|
+
this.rowHeight = 36;
|
|
48
|
+
this.overscan = 6;
|
|
49
|
+
this.virtualThreshold = 100;
|
|
50
|
+
this.blockSize = 100;
|
|
51
|
+
this.loading = false;
|
|
52
|
+
this.error = "";
|
|
53
|
+
this.emptyMessage = "No rows";
|
|
54
|
+
this.page = 1;
|
|
55
|
+
this.pageSize = 0;
|
|
56
|
+
this.columnElements = [];
|
|
57
|
+
this.widthOverrides = {};
|
|
58
|
+
this.focusedCell = {
|
|
59
|
+
row: HEADER_ROW,
|
|
60
|
+
column: 0
|
|
61
|
+
};
|
|
62
|
+
this.hasToolbar = false;
|
|
63
|
+
this.hasFooter = false;
|
|
64
|
+
this.remoteTotal = -1;
|
|
65
|
+
this.#sortedRows = [];
|
|
66
|
+
this.#blocks = /* @__PURE__ */ new Map();
|
|
67
|
+
this.#pendingBlocks = /* @__PURE__ */ new Set();
|
|
68
|
+
this.#measuredWidths = /* @__PURE__ */ new Map();
|
|
69
|
+
this.#measuredRowHeight = 0;
|
|
70
|
+
this.#selectionAnchor = -1;
|
|
71
|
+
this.#pendingFocus = false;
|
|
72
|
+
this.#requestToken = 0;
|
|
73
|
+
this.#pagers = /* @__PURE__ */ new WeakSet();
|
|
74
|
+
this.#pendingScrollTop = false;
|
|
75
|
+
this.pager = void 0;
|
|
76
|
+
this.#virtualizer = new VirtualScrollController(this, {
|
|
77
|
+
scrollElement: () => this.viewport,
|
|
78
|
+
itemCount: () => this.rowCount,
|
|
79
|
+
itemHeight: () => this.#rowHeightPx,
|
|
80
|
+
overscan: () => this.overscan,
|
|
81
|
+
enabled: () => this.isVirtualized
|
|
82
|
+
});
|
|
83
|
+
this.#handleDefinitionsChange = (event) => {
|
|
84
|
+
this.#collectColumns(event.target);
|
|
85
|
+
};
|
|
86
|
+
this.#handleColumnChange = (event) => {
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
this.#collectColumns();
|
|
89
|
+
};
|
|
90
|
+
this.#handlePagerConnect = (event) => {
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
if (event.target) this.#pagers.add(event.target);
|
|
93
|
+
if (this.pageSize === 0) {
|
|
94
|
+
const pageSize = Math.floor(event.detail?.pageSize ?? 0);
|
|
95
|
+
if (Number.isFinite(pageSize) && pageSize > 0) this.pageSize = pageSize;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.#handlePagerEvent = (event) => {
|
|
99
|
+
if (!this.paginated || event.target === this) return;
|
|
100
|
+
if (event.target && this.#pagers.has(event.target)) event.stopImmediatePropagation();
|
|
101
|
+
};
|
|
102
|
+
this.#handleSelectAll = (event) => {
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
if (event.target.checked) this.selectAll();
|
|
105
|
+
else this.clearSelection();
|
|
106
|
+
};
|
|
107
|
+
this.#handleKeyDown = (event) => {
|
|
108
|
+
const columns = this.#renderColumns();
|
|
109
|
+
if (columns.length === 0) return;
|
|
110
|
+
let { row, column } = this.focusedCell;
|
|
111
|
+
const first = this.#pageStart;
|
|
112
|
+
const last = first + this.rowCount - 1;
|
|
113
|
+
const viewportRows = Math.max(1, Math.floor((this.viewport?.clientHeight ?? 0) / this.#rowHeightPx) - 1);
|
|
114
|
+
switch (event.key) {
|
|
115
|
+
case "ArrowDown":
|
|
116
|
+
row = Math.min(last, row + 1);
|
|
117
|
+
break;
|
|
118
|
+
case "ArrowUp":
|
|
119
|
+
row = row <= first ? HEADER_ROW : row - 1;
|
|
120
|
+
break;
|
|
121
|
+
case "ArrowRight":
|
|
122
|
+
column = Math.min(columns.length - 1, column + 1);
|
|
123
|
+
break;
|
|
124
|
+
case "ArrowLeft":
|
|
125
|
+
column = Math.max(0, column - 1);
|
|
126
|
+
break;
|
|
127
|
+
case "Home":
|
|
128
|
+
if (event.ctrlKey || event.metaKey) row = HEADER_ROW;
|
|
129
|
+
column = 0;
|
|
130
|
+
break;
|
|
131
|
+
case "End":
|
|
132
|
+
if (event.ctrlKey || event.metaKey) row = last;
|
|
133
|
+
column = columns.length - 1;
|
|
134
|
+
break;
|
|
135
|
+
case "PageDown":
|
|
136
|
+
row = Math.min(last, Math.max(first, row) + viewportRows);
|
|
137
|
+
break;
|
|
138
|
+
case "PageUp":
|
|
139
|
+
row = Math.max(first, row - viewportRows);
|
|
140
|
+
break;
|
|
141
|
+
case "Enter":
|
|
142
|
+
case " ":
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
if (row === HEADER_ROW) this.#toggleSort(columns[column], event.shiftKey);
|
|
145
|
+
else if (this.selection !== "none") this.#applySelection(row, {
|
|
146
|
+
toggle: event.key === " " || event.ctrlKey || event.metaKey,
|
|
147
|
+
range: event.shiftKey
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
default: return;
|
|
151
|
+
}
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
this.focusedCell = {
|
|
154
|
+
row,
|
|
155
|
+
column
|
|
156
|
+
};
|
|
157
|
+
this.#pendingFocus = true;
|
|
158
|
+
if (row >= 0) this.scrollToIndex(row);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
static {
|
|
162
|
+
this.styles = unsafeCSS(table_default);
|
|
163
|
+
}
|
|
164
|
+
#sortedRows;
|
|
165
|
+
#blocks;
|
|
166
|
+
#pendingBlocks;
|
|
167
|
+
#measuredWidths;
|
|
168
|
+
#measuredRowHeight;
|
|
169
|
+
#selectionAnchor;
|
|
170
|
+
#pendingFocus;
|
|
171
|
+
#requestToken;
|
|
172
|
+
/** The pagers that have announced themselves, so only their events are swallowed — not a pager inside a cell. */
|
|
173
|
+
#pagers;
|
|
174
|
+
#pendingScrollTop;
|
|
175
|
+
#virtualizer;
|
|
176
|
+
/** Total number of rows the table knows about: `rows.length`, or the `dataSource` total. */
|
|
177
|
+
get totalRows() {
|
|
178
|
+
return this.dataSource ? Math.max(0, this.remoteTotal) : this.#sortedRows.length;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Number of rows on screen right now: the whole dataset, or one page of it while `paginated`. This is what the
|
|
182
|
+
* virtualizer windows and what `virtual="auto"` measures, so a 25-row page is never virtualized against a
|
|
183
|
+
* million-row total.
|
|
184
|
+
*/
|
|
185
|
+
get rowCount() {
|
|
186
|
+
if (!this.paginated) return this.totalRows;
|
|
187
|
+
const onPage = Math.max(0, Math.min(this.pageSize, this.totalRows - this.#pageStart));
|
|
188
|
+
if (!this.dataSource) return onPage;
|
|
189
|
+
const loaded = this.#blocks.get(this.page - 1)?.length;
|
|
190
|
+
if (loaded !== void 0) return loaded;
|
|
191
|
+
return this.remoteTotal < 0 ? 0 : onPage;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Whether the table is paging its rows. `rows` are sliced in place; a `dataSource` is asked for one page per
|
|
195
|
+
* request. Either way `page-size` is the switch, and a pager slotted into the `footer` sets it.
|
|
196
|
+
*/
|
|
197
|
+
get paginated() {
|
|
198
|
+
return this.pageSize > 0;
|
|
199
|
+
}
|
|
200
|
+
/** Number of pages, at least 1. */
|
|
201
|
+
get pageCount() {
|
|
202
|
+
if (!this.paginated) return 1;
|
|
203
|
+
return Math.max(1, Math.ceil(this.totalRows / this.pageSize));
|
|
204
|
+
}
|
|
205
|
+
/** Index of the first row of the current page within the whole dataset. */
|
|
206
|
+
get #pageStart() {
|
|
207
|
+
return this.paginated ? (this.page - 1) * this.pageSize : 0;
|
|
208
|
+
}
|
|
209
|
+
/** A page is one block, so the existing block cache and request de-duplication carry paging unchanged. */
|
|
210
|
+
get #effectiveBlockSize() {
|
|
211
|
+
return this.paginated ? this.pageSize : this.blockSize;
|
|
212
|
+
}
|
|
213
|
+
/** Whether the rows are currently windowed. */
|
|
214
|
+
get isVirtualized() {
|
|
215
|
+
if (this.virtual === "never") return false;
|
|
216
|
+
if (this.virtual === "always") return true;
|
|
217
|
+
return this.rowCount > this.virtualThreshold;
|
|
218
|
+
}
|
|
219
|
+
get #rowHeightPx() {
|
|
220
|
+
return this.#measuredRowHeight || this.rowHeight;
|
|
221
|
+
}
|
|
222
|
+
get #isBootstrapping() {
|
|
223
|
+
return Boolean(this.dataSource) && this.remoteTotal < 0 && !this.error;
|
|
224
|
+
}
|
|
225
|
+
/** The resolved, ordered, visible columns — children first, then the `columns` property, then one per key of the first row. */
|
|
226
|
+
get resolvedColumns() {
|
|
227
|
+
const visible = (this.columnElements.length ? this.columnElements : this.columns ?? this.#autoColumns()).filter((column) => column.field && !column.hidden);
|
|
228
|
+
return [
|
|
229
|
+
...visible.filter((column) => column.pinned === "start"),
|
|
230
|
+
...visible.filter((column) => !column.pinned),
|
|
231
|
+
...visible.filter((column) => column.pinned === "end")
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
/** The rows currently selected. Only the loaded ones when a `dataSource` is used. */
|
|
235
|
+
getSelectedRows() {
|
|
236
|
+
const keys = new Set(this.value);
|
|
237
|
+
const selected = [];
|
|
238
|
+
for (let index = 0; index < this.rowCount; index++) {
|
|
239
|
+
const row = this.#rowAt(index);
|
|
240
|
+
if (row && keys.has(this.#keyAt(index, row))) selected.push(row);
|
|
241
|
+
}
|
|
242
|
+
return selected;
|
|
243
|
+
}
|
|
244
|
+
/** Selects every loaded row. No-op unless `selection="multiple"`. */
|
|
245
|
+
selectAll() {
|
|
246
|
+
if (this.selection !== "multiple") return;
|
|
247
|
+
const keys = [];
|
|
248
|
+
for (let index = 0; index < this.rowCount; index++) {
|
|
249
|
+
const row = this.#rowAt(index);
|
|
250
|
+
if (row) keys.push(this.#keyAt(index, row));
|
|
251
|
+
}
|
|
252
|
+
this.#commitSelection(keys);
|
|
253
|
+
}
|
|
254
|
+
/** Clears the selection. */
|
|
255
|
+
clearSelection() {
|
|
256
|
+
this.#commitSelection([]);
|
|
257
|
+
}
|
|
258
|
+
/** Scrolls the row at `index` into view. */
|
|
259
|
+
scrollToIndex(index) {
|
|
260
|
+
this.#virtualizer.scrollToIndex(index - this.#pageStart, this.#headerHeight());
|
|
261
|
+
}
|
|
262
|
+
/** Drops the `dataSource` cache and reloads the visible rows. */
|
|
263
|
+
refresh() {
|
|
264
|
+
this.error = "";
|
|
265
|
+
if (this.dataSource) this.#resetRemote();
|
|
266
|
+
this.requestUpdate();
|
|
267
|
+
}
|
|
268
|
+
willUpdate(changed) {
|
|
269
|
+
if (!this.hasUpdated || changed.has("rows") || changed.has("sortModel") || changed.has("columns") || changed.has("columnElements")) this.#applySort();
|
|
270
|
+
if (changed.has("dataSource") || changed.has("blockSize") || changed.has("pageSize") || this.dataSource && changed.has("sortModel")) this.#resetRemote();
|
|
271
|
+
if (this.paginated) {
|
|
272
|
+
if (changed.has("sortModel") && changed.get("sortModel") !== void 0) this.page = 1;
|
|
273
|
+
if (!this.dataSource || this.remoteTotal >= 0) {
|
|
274
|
+
const clamped = Math.min(Math.max(1, Math.floor(this.page) || 1), this.pageCount);
|
|
275
|
+
if (clamped !== this.page) this.page = clamped;
|
|
276
|
+
}
|
|
277
|
+
if ((changed.has("page") || changed.has("pageSize")) && this.error) this.error = "";
|
|
278
|
+
if (changed.has("page") && this.hasUpdated) {
|
|
279
|
+
this.#selectionAnchor = -1;
|
|
280
|
+
this.#pendingScrollTop = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.#syncPagerContext();
|
|
284
|
+
if (this.rowCount === 0 && this.focusedCell.row !== HEADER_ROW) this.focusedCell = {
|
|
285
|
+
row: HEADER_ROW,
|
|
286
|
+
column: this.focusedCell.column
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
#syncPagerContext() {
|
|
290
|
+
const current = this.pager;
|
|
291
|
+
if (!this.paginated) {
|
|
292
|
+
if (current) this.pager = void 0;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const busy = this.loading || this.#isBootstrapping;
|
|
296
|
+
if (current && current.page === this.page && current.pageSize === this.pageSize && current.totalItems === this.totalRows && current.busy === busy) return;
|
|
297
|
+
this.pager = {
|
|
298
|
+
page: this.page,
|
|
299
|
+
pageSize: this.pageSize,
|
|
300
|
+
totalItems: this.totalRows,
|
|
301
|
+
busy,
|
|
302
|
+
pageChanged: (page) => this.goToPage(page),
|
|
303
|
+
pageSizeChanged: (pageSize) => this.setPageSize(pageSize)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/** Shows `page`, clamped to the available range, and loads it. Fires `page-change` when the page actually moves. */
|
|
307
|
+
goToPage(page) {
|
|
308
|
+
if (!this.paginated) return;
|
|
309
|
+
const previousPage = this.page;
|
|
310
|
+
const next = Math.min(Math.max(1, Math.floor(page) || 1), this.pageCount);
|
|
311
|
+
if (next === previousPage) return;
|
|
312
|
+
this.page = next;
|
|
313
|
+
this.dispatchEvent(new CustomEvent("page-change", {
|
|
314
|
+
bubbles: true,
|
|
315
|
+
composed: true,
|
|
316
|
+
detail: {
|
|
317
|
+
page: next,
|
|
318
|
+
previousPage,
|
|
319
|
+
pageSize: this.pageSize,
|
|
320
|
+
pageCount: this.pageCount,
|
|
321
|
+
start: (next - 1) * this.pageSize,
|
|
322
|
+
count: this.pageSize,
|
|
323
|
+
totalRows: this.totalRows
|
|
324
|
+
}
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
/** Changes the rows per page, keeping the first row of the current page on screen. */
|
|
328
|
+
setPageSize(pageSize) {
|
|
329
|
+
const next = Math.floor(pageSize);
|
|
330
|
+
if (!Number.isFinite(next) || next <= 0 || next === this.pageSize) return;
|
|
331
|
+
const firstRow = this.#pageStart;
|
|
332
|
+
this.pageSize = next;
|
|
333
|
+
this.page = Math.floor(firstRow / next) + 1;
|
|
334
|
+
}
|
|
335
|
+
updated(changed) {
|
|
336
|
+
super.updated(changed);
|
|
337
|
+
if (this.#pendingScrollTop) {
|
|
338
|
+
this.#pendingScrollTop = false;
|
|
339
|
+
if (this.viewport) this.viewport.scrollTop = 0;
|
|
340
|
+
}
|
|
341
|
+
this.#measureRowHeight();
|
|
342
|
+
this.#measureColumnWidths();
|
|
343
|
+
this.#syncFocusToWindow();
|
|
344
|
+
if (this.#pendingFocus) {
|
|
345
|
+
this.#pendingFocus = false;
|
|
346
|
+
this.renderRoot.querySelector(".cell[tabindex=\"0\"]")?.focus();
|
|
347
|
+
}
|
|
348
|
+
if (this.dataSource) this.#ensureBlocks();
|
|
349
|
+
}
|
|
350
|
+
render() {
|
|
351
|
+
const columns = this.#renderColumns();
|
|
352
|
+
const pins = this.#pinPlacements(columns);
|
|
353
|
+
const range = this.#virtualizer.range;
|
|
354
|
+
const offset = this.#pageStart;
|
|
355
|
+
return html`
|
|
356
|
+
<div class="toolbar" part="toolbar" ?hidden=${!this.hasToolbar}>
|
|
357
|
+
<slot name="toolbar" @slotchange=${(event) => this.hasToolbar = this.#slotHasContent(event)}></slot>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="viewport" part="viewport">
|
|
360
|
+
<div
|
|
361
|
+
class="grid"
|
|
362
|
+
role="grid"
|
|
363
|
+
part="grid"
|
|
364
|
+
aria-rowcount=${this.totalRows + 1}
|
|
365
|
+
aria-colcount=${columns.length}
|
|
366
|
+
aria-busy=${this.loading || this.#isBootstrapping ? "true" : "false"}
|
|
367
|
+
style=${styleMap({ "--_grid-template": this.#gridTemplate(columns) })}
|
|
368
|
+
@keydown=${this.#handleKeyDown}
|
|
369
|
+
>
|
|
370
|
+
${this.#renderHeaderRow(columns, pins)} ${range.paddingTop > 0 ? html`<div class="spacer" style="height:${range.paddingTop}px"></div>` : nothing}
|
|
371
|
+
${this.#renderBody(columns, pins, offset + range.start, offset + range.end)}
|
|
372
|
+
${range.paddingBottom > 0 ? html`<div class="spacer" style="height:${range.paddingBottom}px"></div>` : nothing}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="footer" part="footer" ?hidden=${!this.hasFooter}>
|
|
376
|
+
<slot name="footer" @slotchange=${(event) => this.hasFooter = this.#slotHasContent(event)}></slot>
|
|
377
|
+
</div>
|
|
378
|
+
<slot class="definitions" @slotchange=${this.#handleDefinitionsChange}></slot>
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
#renderBody(columns, pins, start, end) {
|
|
382
|
+
if (this.error) return html`<div class="row row--state" role="row">
|
|
383
|
+
<div class="cell cell--state cell--state-error" role="gridcell" part="state"><slot name="error">${this.error}</slot></div>
|
|
384
|
+
</div>`;
|
|
385
|
+
if (this.rowCount === 0) {
|
|
386
|
+
const isLoading = this.loading || this.#isBootstrapping;
|
|
387
|
+
return html`<div class="row row--state" role="row">
|
|
388
|
+
<div class="cell cell--state" role="gridcell" part="state">
|
|
389
|
+
${isLoading ? html`<slot name="loading"><c2-spinner></c2-spinner></slot>` : html`<slot name="empty">${this.emptyMessage}</slot>`}
|
|
390
|
+
</div>
|
|
391
|
+
</div>`;
|
|
392
|
+
}
|
|
393
|
+
const rows = [];
|
|
394
|
+
for (let index = start; index < end; index++) rows.push(this.#renderRow(index, columns, pins));
|
|
395
|
+
return rows;
|
|
396
|
+
}
|
|
397
|
+
#renderHeaderRow(columns, pins) {
|
|
398
|
+
return html`
|
|
399
|
+
<div class="row row--header" role="row" aria-rowindex="1" part="header-row">
|
|
400
|
+
${columns.map((column, columnIndex) => this.#renderHeaderCell(column, columnIndex, pins))}
|
|
401
|
+
</div>
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
#renderHeaderCell(column, columnIndex, pins) {
|
|
405
|
+
if (column.field === SELECTION_FIELD) {
|
|
406
|
+
const total = this.rowCount;
|
|
407
|
+
const selected = this.value.length;
|
|
408
|
+
const allSelected = total > 0 && selected >= total;
|
|
409
|
+
return html`
|
|
410
|
+
<div
|
|
411
|
+
class=${classMap({
|
|
412
|
+
cell: true,
|
|
413
|
+
"cell--header": true,
|
|
414
|
+
"cell--selection": true,
|
|
415
|
+
...this.#pinClasses(column, pins)
|
|
416
|
+
})}
|
|
417
|
+
role="columnheader"
|
|
418
|
+
part="header-cell selection-header-cell"
|
|
419
|
+
data-field=${column.field}
|
|
420
|
+
aria-colindex=${columnIndex + 1}
|
|
421
|
+
tabindex=${this.#tabIndexFor(HEADER_ROW, columnIndex)}
|
|
422
|
+
style=${styleMap(this.#pinStyle(column, pins))}
|
|
423
|
+
>
|
|
424
|
+
<c2-checkbox
|
|
425
|
+
aria-label="Select all rows"
|
|
426
|
+
?checked=${allSelected}
|
|
427
|
+
?indeterminate=${selected > 0 && !allSelected}
|
|
428
|
+
?disabled=${this.selection !== "multiple" || Boolean(this.dataSource)}
|
|
429
|
+
@change=${this.#handleSelectAll}
|
|
430
|
+
></c2-checkbox>
|
|
431
|
+
</div>
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
const sortable = column.sortable ?? this.sortable;
|
|
435
|
+
const sortIndex = this.sortModel.findIndex((entry) => entry.field === column.field);
|
|
436
|
+
const sort = sortIndex >= 0 ? this.sortModel[sortIndex] : void 0;
|
|
437
|
+
const resizable = column.resizable ?? this.resizable;
|
|
438
|
+
return html`
|
|
439
|
+
<div
|
|
440
|
+
class=${classMap({
|
|
441
|
+
cell: true,
|
|
442
|
+
"cell--header": true,
|
|
443
|
+
"cell--sortable": sortable,
|
|
444
|
+
"cell--sorted": Boolean(sort),
|
|
445
|
+
[`cell--align-${column.align ?? "start"}`]: true,
|
|
446
|
+
...this.#pinClasses(column, pins)
|
|
447
|
+
})}
|
|
448
|
+
role="columnheader"
|
|
449
|
+
part="header-cell"
|
|
450
|
+
data-field=${column.field}
|
|
451
|
+
aria-colindex=${columnIndex + 1}
|
|
452
|
+
aria-sort=${ifDefined(sortable ? sort ? sort.direction === "asc" ? "ascending" : "descending" : "none" : void 0)}
|
|
453
|
+
tabindex=${this.#tabIndexFor(HEADER_ROW, columnIndex)}
|
|
454
|
+
style=${styleMap(this.#pinStyle(column, pins))}
|
|
455
|
+
@click=${(event) => {
|
|
456
|
+
this.focusedCell = {
|
|
457
|
+
row: HEADER_ROW,
|
|
458
|
+
column: columnIndex
|
|
459
|
+
};
|
|
460
|
+
this.#toggleSort(column, event.shiftKey);
|
|
461
|
+
}}
|
|
462
|
+
>
|
|
463
|
+
<span class="cell-content">${column.renderHeader ? column.renderHeader({ column }) : column.header ?? humanize(column.field)}</span>
|
|
464
|
+
${sortable ? this.#renderSortIcon(sort?.direction) : nothing}
|
|
465
|
+
${sort && this.sortModel.length > 1 ? html`<span class="sort-order">${sortIndex + 1}</span>` : nothing}
|
|
466
|
+
${resizable ? html`<span class="resizer" part="resizer" @pointerdown=${(event) => this.#startResize(event, column)}></span>` : nothing}
|
|
467
|
+
</div>
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
#renderSortIcon(direction) {
|
|
471
|
+
return html`
|
|
472
|
+
<svg
|
|
473
|
+
class=${classMap({
|
|
474
|
+
"sort-icon": true,
|
|
475
|
+
"sort-icon--active": Boolean(direction),
|
|
476
|
+
"sort-icon--desc": direction === "desc"
|
|
477
|
+
})}
|
|
478
|
+
part="sort-icon"
|
|
479
|
+
viewBox="0 0 24 24"
|
|
480
|
+
fill="none"
|
|
481
|
+
stroke="currentColor"
|
|
482
|
+
stroke-width="2"
|
|
483
|
+
stroke-linecap="round"
|
|
484
|
+
stroke-linejoin="round"
|
|
485
|
+
aria-hidden="true"
|
|
486
|
+
>
|
|
487
|
+
<path d="M12 19V5" />
|
|
488
|
+
<path d="M5 12l7-7 7 7" />
|
|
489
|
+
</svg>
|
|
490
|
+
`;
|
|
491
|
+
}
|
|
492
|
+
#renderRow(index, columns, pins) {
|
|
493
|
+
const row = this.#rowAt(index);
|
|
494
|
+
const key = row ? this.#keyAt(index, row) : "";
|
|
495
|
+
const selected = key !== "" && this.value.includes(key);
|
|
496
|
+
const selectable = this.selection !== "none";
|
|
497
|
+
return html`
|
|
498
|
+
<div
|
|
499
|
+
class=${classMap({
|
|
500
|
+
row: true,
|
|
501
|
+
"row--odd": index % 2 === 1,
|
|
502
|
+
"row--selected": selected,
|
|
503
|
+
"row--clickable": selectable
|
|
504
|
+
})}
|
|
505
|
+
role="row"
|
|
506
|
+
part=${selected ? "row row-selected" : "row"}
|
|
507
|
+
aria-rowindex=${index + 2}
|
|
508
|
+
aria-selected=${ifDefined(selectable ? String(selected) : void 0)}
|
|
509
|
+
@click=${(event) => this.#handleRowClick(event, index)}
|
|
510
|
+
>
|
|
511
|
+
${columns.map((column, columnIndex) => this.#renderCell(row, index, column, columnIndex, pins))}
|
|
512
|
+
</div>
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
#renderCell(row, rowIndex, column, columnIndex, pins) {
|
|
516
|
+
const classes = {
|
|
517
|
+
cell: true,
|
|
518
|
+
[`cell--align-${column.align ?? "start"}`]: true,
|
|
519
|
+
"cell--selection": column.field === SELECTION_FIELD,
|
|
520
|
+
...this.#pinClasses(column, pins),
|
|
521
|
+
...column.cellClass ? { [column.cellClass]: true } : {}
|
|
522
|
+
};
|
|
523
|
+
const shared = {
|
|
524
|
+
class: classMap(classes),
|
|
525
|
+
tabindex: this.#tabIndexFor(rowIndex, columnIndex),
|
|
526
|
+
style: styleMap(this.#pinStyle(column, pins))
|
|
527
|
+
};
|
|
528
|
+
if (column.field === SELECTION_FIELD) {
|
|
529
|
+
const key = row ? this.#keyAt(rowIndex, row) : "";
|
|
530
|
+
return html`
|
|
531
|
+
<div
|
|
532
|
+
class=${shared.class}
|
|
533
|
+
role="gridcell"
|
|
534
|
+
part="cell selection-cell"
|
|
535
|
+
aria-colindex=${columnIndex + 1}
|
|
536
|
+
tabindex=${shared.tabindex}
|
|
537
|
+
style=${shared.style}
|
|
538
|
+
>
|
|
539
|
+
${row ? html`<c2-checkbox
|
|
540
|
+
aria-label="Select row"
|
|
541
|
+
?checked=${this.value.includes(key)}
|
|
542
|
+
@click=${(event) => event.stopPropagation()}
|
|
543
|
+
@change=${() => this.#handleCheckboxToggle(rowIndex)}
|
|
544
|
+
></c2-checkbox>` : nothing}
|
|
545
|
+
</div>
|
|
546
|
+
`;
|
|
547
|
+
}
|
|
548
|
+
const value = getFieldValue(row, column.field);
|
|
549
|
+
const rendered = row ? column.renderCell ? column.renderCell({
|
|
550
|
+
value,
|
|
551
|
+
row,
|
|
552
|
+
rowIndex,
|
|
553
|
+
column
|
|
554
|
+
}) : this.#formatValue(column, value) : void 0;
|
|
555
|
+
const content = row ? column.cellSlot ? html`<slot name=${`cell:${this.#keyAt(rowIndex, row)}:${column.field}`}>${rendered}</slot>` : rendered : html`<span class="skeleton" part="skeleton"></span>`;
|
|
556
|
+
const fieldPart = column.field.replace(/[^\w-]/g, "-");
|
|
557
|
+
return html`
|
|
558
|
+
<div
|
|
559
|
+
class=${shared.class}
|
|
560
|
+
role="gridcell"
|
|
561
|
+
part="cell cell-${fieldPart}"
|
|
562
|
+
aria-colindex=${columnIndex + 1}
|
|
563
|
+
tabindex=${shared.tabindex}
|
|
564
|
+
style=${shared.style}
|
|
565
|
+
@click=${() => this.#handleCellClick(rowIndex, columnIndex, column, value)}
|
|
566
|
+
>
|
|
567
|
+
<span class="cell-content" part="cell-content cell-content-${fieldPart}">${content}</span>
|
|
568
|
+
</div>
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
#renderColumns() {
|
|
572
|
+
const columns = this.resolvedColumns;
|
|
573
|
+
if (!this.checkboxSelection) return columns;
|
|
574
|
+
return [{
|
|
575
|
+
field: SELECTION_FIELD,
|
|
576
|
+
header: "",
|
|
577
|
+
width: "var(--c2-table__selection-cell--width, 44px)",
|
|
578
|
+
minWidth: 32,
|
|
579
|
+
align: "center",
|
|
580
|
+
pinned: "start"
|
|
581
|
+
}, ...columns];
|
|
582
|
+
}
|
|
583
|
+
#autoColumns() {
|
|
584
|
+
const first = Array.isArray(this.rows) ? this.rows[0] : void 0;
|
|
585
|
+
if (!first) return [];
|
|
586
|
+
return Object.keys(first).map((field) => ({
|
|
587
|
+
field,
|
|
588
|
+
header: humanize(field)
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
#gridTemplate(columns) {
|
|
592
|
+
return columns.map((column) => this.widthOverrides[column.field] ? `${this.widthOverrides[column.field]}px` : column.width || "1fr").join(" ");
|
|
593
|
+
}
|
|
594
|
+
#pinPlacements(columns) {
|
|
595
|
+
const placements = /* @__PURE__ */ new Map();
|
|
596
|
+
const startColumns = columns.filter((column) => column.pinned === "start");
|
|
597
|
+
const endColumns = columns.filter((column) => column.pinned === "end");
|
|
598
|
+
let offset = 0;
|
|
599
|
+
startColumns.forEach((column, index) => {
|
|
600
|
+
placements.set(column.field, {
|
|
601
|
+
side: "start",
|
|
602
|
+
offset,
|
|
603
|
+
edge: index === startColumns.length - 1
|
|
604
|
+
});
|
|
605
|
+
offset += this.#columnWidth(column);
|
|
606
|
+
});
|
|
607
|
+
offset = 0;
|
|
608
|
+
for (let index = endColumns.length - 1; index >= 0; index--) {
|
|
609
|
+
const column = endColumns[index];
|
|
610
|
+
placements.set(column.field, {
|
|
611
|
+
side: "end",
|
|
612
|
+
offset,
|
|
613
|
+
edge: index === 0
|
|
614
|
+
});
|
|
615
|
+
offset += this.#columnWidth(column);
|
|
616
|
+
}
|
|
617
|
+
return placements;
|
|
618
|
+
}
|
|
619
|
+
#columnWidth(column) {
|
|
620
|
+
return this.widthOverrides[column.field] ?? this.#measuredWidths.get(column.field) ?? 0;
|
|
621
|
+
}
|
|
622
|
+
#pinClasses(column, pins) {
|
|
623
|
+
const pin = pins.get(column.field);
|
|
624
|
+
if (!pin) return {};
|
|
625
|
+
return {
|
|
626
|
+
"cell--pinned": true,
|
|
627
|
+
"cell--pinned-edge-start": pin.side === "start" && pin.edge,
|
|
628
|
+
"cell--pinned-edge-end": pin.side === "end" && pin.edge
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
#pinStyle(column, pins) {
|
|
632
|
+
const pin = pins.get(column.field);
|
|
633
|
+
if (!pin) return {};
|
|
634
|
+
return pin.side === "start" ? { left: `${pin.offset}px` } : { right: `${pin.offset}px` };
|
|
635
|
+
}
|
|
636
|
+
#tabIndexFor(rowIndex, columnIndex) {
|
|
637
|
+
return this.focusedCell.row === rowIndex && this.focusedCell.column === columnIndex ? 0 : -1;
|
|
638
|
+
}
|
|
639
|
+
#formatValue(column, value) {
|
|
640
|
+
if (value === null || value === void 0 || value === "") return "";
|
|
641
|
+
const locale = column.locale;
|
|
642
|
+
const extra = column.formatOptions;
|
|
643
|
+
try {
|
|
644
|
+
switch (column.format) {
|
|
645
|
+
case "number": return new Intl.NumberFormat(locale, numberFormatOptions({}, extra)).format(Number(value));
|
|
646
|
+
case "percent": return new Intl.NumberFormat(locale, numberFormatOptions({ style: "percent" }, extra)).format(Number(value));
|
|
647
|
+
case "currency": return new Intl.NumberFormat(locale, numberFormatOptions({
|
|
648
|
+
style: "currency",
|
|
649
|
+
currency: column.currency ?? "USD"
|
|
650
|
+
}, extra)).format(Number(value));
|
|
651
|
+
case "date": return new Intl.DateTimeFormat(locale, dateFormatOptions({ dateStyle: "medium" }, extra)).format(new Date(value));
|
|
652
|
+
case "datetime": return new Intl.DateTimeFormat(locale, dateFormatOptions({
|
|
653
|
+
dateStyle: "medium",
|
|
654
|
+
timeStyle: "short"
|
|
655
|
+
}, extra)).format(new Date(value));
|
|
656
|
+
case "time": return new Intl.DateTimeFormat(locale, dateFormatOptions({ timeStyle: "short" }, extra)).format(new Date(value));
|
|
657
|
+
default: return String(value);
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
return String(value);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/** `index` is the row's position in the whole dataset, on every page. */
|
|
664
|
+
#rowAt(index) {
|
|
665
|
+
if (this.dataSource) {
|
|
666
|
+
const size = this.#effectiveBlockSize;
|
|
667
|
+
return this.#blocks.get(Math.floor(index / size))?.[index % size];
|
|
668
|
+
}
|
|
669
|
+
return this.#sortedRows[index];
|
|
670
|
+
}
|
|
671
|
+
#keyAt(index, row) {
|
|
672
|
+
if (!this.rowKey) return String(index);
|
|
673
|
+
const value = getFieldValue(row, this.rowKey);
|
|
674
|
+
return value === null || value === void 0 ? String(index) : String(value);
|
|
675
|
+
}
|
|
676
|
+
#applySort() {
|
|
677
|
+
const rows = Array.isArray(this.rows) ? this.rows : [];
|
|
678
|
+
if (this.dataSource || this.sortModel.length === 0) {
|
|
679
|
+
this.#sortedRows = rows;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const columns = this.resolvedColumns;
|
|
683
|
+
this.#sortedRows = [...rows].sort((left, right) => {
|
|
684
|
+
for (const entry of this.sortModel) {
|
|
685
|
+
const result = (columns.find((candidate) => candidate.field === entry.field)?.comparator ?? defaultCompare)(getFieldValue(left, entry.field), getFieldValue(right, entry.field));
|
|
686
|
+
if (result !== 0) return entry.direction === "desc" ? -result : result;
|
|
687
|
+
}
|
|
688
|
+
return 0;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
#resetRemote() {
|
|
692
|
+
this.#requestToken++;
|
|
693
|
+
this.#blocks.clear();
|
|
694
|
+
this.#pendingBlocks.clear();
|
|
695
|
+
this.remoteTotal = -1;
|
|
696
|
+
}
|
|
697
|
+
#ensureBlocks() {
|
|
698
|
+
if (!this.dataSource || this.error) return;
|
|
699
|
+
if (this.paginated) {
|
|
700
|
+
this.#fetchBlock(this.page - 1);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (this.remoteTotal < 0) {
|
|
704
|
+
this.#fetchBlock(0);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const { start, end } = this.#virtualizer.range;
|
|
708
|
+
if (end <= start) return;
|
|
709
|
+
const firstBlock = Math.floor(start / this.blockSize);
|
|
710
|
+
const lastBlock = Math.floor((end - 1) / this.blockSize);
|
|
711
|
+
for (let block = firstBlock; block <= lastBlock; block++) this.#fetchBlock(block);
|
|
712
|
+
}
|
|
713
|
+
async #fetchBlock(block) {
|
|
714
|
+
const dataSource = this.dataSource;
|
|
715
|
+
if (!dataSource || this.#blocks.has(block) || this.#pendingBlocks.has(block)) return;
|
|
716
|
+
this.#pendingBlocks.add(block);
|
|
717
|
+
const token = this.#requestToken;
|
|
718
|
+
try {
|
|
719
|
+
const size = this.#effectiveBlockSize;
|
|
720
|
+
const result = await dataSource.getRows({
|
|
721
|
+
start: block * size,
|
|
722
|
+
count: size,
|
|
723
|
+
sort: this.sortModel
|
|
724
|
+
});
|
|
725
|
+
if (token !== this.#requestToken) return;
|
|
726
|
+
this.#blocks.set(block, result.rows ?? []);
|
|
727
|
+
if (typeof result.total === "number") this.remoteTotal = result.total;
|
|
728
|
+
else if (this.remoteTotal < 0) this.remoteTotal = block * size + (result.rows?.length ?? 0);
|
|
729
|
+
this.requestUpdate();
|
|
730
|
+
} catch (error) {
|
|
731
|
+
if (token === this.#requestToken) {
|
|
732
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
733
|
+
if (this.remoteTotal < 0) this.remoteTotal = 0;
|
|
734
|
+
}
|
|
735
|
+
} finally {
|
|
736
|
+
this.#pendingBlocks.delete(block);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
connectedCallback() {
|
|
740
|
+
super.connectedCallback();
|
|
741
|
+
this.addEventListener(COLUMN_CHANGE_EVENT, this.#handleColumnChange);
|
|
742
|
+
this.addEventListener(PAGER_CONNECT_EVENT, this.#handlePagerConnect);
|
|
743
|
+
this.addEventListener("page-change", this.#handlePagerEvent);
|
|
744
|
+
this.addEventListener("page-size-change", this.#handlePagerEvent);
|
|
745
|
+
}
|
|
746
|
+
disconnectedCallback() {
|
|
747
|
+
super.disconnectedCallback();
|
|
748
|
+
this.removeEventListener(COLUMN_CHANGE_EVENT, this.#handleColumnChange);
|
|
749
|
+
}
|
|
750
|
+
#handleDefinitionsChange;
|
|
751
|
+
/** A column definition changed one of its own properties: re-read them all, keeping the event inside the table. */
|
|
752
|
+
#handleColumnChange;
|
|
753
|
+
/**
|
|
754
|
+
* A pager connected somewhere inside the table. Adopt its page size when the table has none, so the nested form
|
|
755
|
+
* needs no configuration, and keep the event inside the table.
|
|
756
|
+
*/
|
|
757
|
+
#handlePagerConnect;
|
|
758
|
+
/**
|
|
759
|
+
* A pager the table drives re-emits what it just asked for, so the table swallows it and answers with its own
|
|
760
|
+
* event — one `page-change` leaves the table, not two. Three things must still get through: the table's own event
|
|
761
|
+
* (it fires this listener at-target), a `c2-pagination` used inside a cell renderer, and any pager on a table that
|
|
762
|
+
* is not paging, which the author is wiring up by hand.
|
|
763
|
+
*/
|
|
764
|
+
#handlePagerEvent;
|
|
765
|
+
#collectColumns(slot) {
|
|
766
|
+
const target = slot ?? this.renderRoot.querySelector("slot.definitions");
|
|
767
|
+
if (!target) return;
|
|
768
|
+
this.columnElements = target.assignedElements({ flatten: true }).map((element) => element instanceof TableColumn ? element : element.firstElementChild).filter((element) => element instanceof TableColumn);
|
|
769
|
+
}
|
|
770
|
+
#slotHasContent(event) {
|
|
771
|
+
return event.target.assignedNodes({ flatten: true }).some((node) => node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()));
|
|
772
|
+
}
|
|
773
|
+
#toggleSort(column, additive) {
|
|
774
|
+
if (column.field === SELECTION_FIELD) return;
|
|
775
|
+
if (!(column.sortable ?? this.sortable)) return;
|
|
776
|
+
const existing = this.sortModel.find((entry) => entry.field === column.field);
|
|
777
|
+
const direction = !existing ? "asc" : existing.direction === "asc" ? "desc" : void 0;
|
|
778
|
+
const rest = additive && this.multiSort ? this.sortModel.filter((entry) => entry.field !== column.field) : [];
|
|
779
|
+
this.sortModel = direction ? [...rest, {
|
|
780
|
+
field: column.field,
|
|
781
|
+
direction
|
|
782
|
+
}] : rest;
|
|
783
|
+
this.dispatchEvent(new CustomEvent("sort-change", {
|
|
784
|
+
detail: { sort: this.sortModel },
|
|
785
|
+
bubbles: true,
|
|
786
|
+
composed: true
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
#handleRowClick(event, index) {
|
|
790
|
+
const row = this.#rowAt(index);
|
|
791
|
+
if (!row) return;
|
|
792
|
+
const key = this.#keyAt(index, row);
|
|
793
|
+
this.dispatchEvent(new CustomEvent("row-click", {
|
|
794
|
+
detail: {
|
|
795
|
+
row,
|
|
796
|
+
rowIndex: index,
|
|
797
|
+
key
|
|
798
|
+
},
|
|
799
|
+
bubbles: true,
|
|
800
|
+
composed: true
|
|
801
|
+
}));
|
|
802
|
+
if (this.selection === "none") return;
|
|
803
|
+
this.#applySelection(index, {
|
|
804
|
+
toggle: event.metaKey || event.ctrlKey,
|
|
805
|
+
range: event.shiftKey
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
#handleCellClick(rowIndex, columnIndex, column, value) {
|
|
809
|
+
const row = this.#rowAt(rowIndex);
|
|
810
|
+
if (!row) return;
|
|
811
|
+
this.focusedCell = {
|
|
812
|
+
row: rowIndex,
|
|
813
|
+
column: columnIndex
|
|
814
|
+
};
|
|
815
|
+
this.dispatchEvent(new CustomEvent("cell-click", {
|
|
816
|
+
detail: {
|
|
817
|
+
row,
|
|
818
|
+
rowIndex,
|
|
819
|
+
key: this.#keyAt(rowIndex, row),
|
|
820
|
+
column,
|
|
821
|
+
value
|
|
822
|
+
},
|
|
823
|
+
bubbles: true,
|
|
824
|
+
composed: true
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
#handleCheckboxToggle(index) {
|
|
828
|
+
this.#applySelection(index, {
|
|
829
|
+
toggle: true,
|
|
830
|
+
range: false
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
#handleSelectAll;
|
|
834
|
+
#applySelection(index, modifiers) {
|
|
835
|
+
const row = this.#rowAt(index);
|
|
836
|
+
if (!row) return;
|
|
837
|
+
const key = this.#keyAt(index, row);
|
|
838
|
+
if (this.selection === "single") {
|
|
839
|
+
this.#selectionAnchor = index;
|
|
840
|
+
this.#commitSelection(modifiers.toggle && this.value.includes(key) ? [] : [key]);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (modifiers.range && this.#selectionAnchor >= 0) {
|
|
844
|
+
const from = Math.min(this.#selectionAnchor, index);
|
|
845
|
+
const to = Math.max(this.#selectionAnchor, index);
|
|
846
|
+
const keys = new Set(modifiers.toggle ? this.value : []);
|
|
847
|
+
for (let cursor = from; cursor <= to; cursor++) {
|
|
848
|
+
const rangeRow = this.#rowAt(cursor);
|
|
849
|
+
if (rangeRow) keys.add(this.#keyAt(cursor, rangeRow));
|
|
850
|
+
}
|
|
851
|
+
this.#commitSelection([...keys]);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
this.#selectionAnchor = index;
|
|
855
|
+
if (modifiers.toggle) {
|
|
856
|
+
this.#commitSelection(this.value.includes(key) ? this.value.filter((entry) => entry !== key) : [...this.value, key]);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
this.#commitSelection([key]);
|
|
860
|
+
}
|
|
861
|
+
#commitSelection(keys) {
|
|
862
|
+
this.value = keys;
|
|
863
|
+
this.dispatchEvent(new CustomEvent("selection-change", {
|
|
864
|
+
detail: {
|
|
865
|
+
value: keys,
|
|
866
|
+
rows: this.getSelectedRows()
|
|
867
|
+
},
|
|
868
|
+
bubbles: false,
|
|
869
|
+
composed: true
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
#startResize(event, column) {
|
|
873
|
+
event.preventDefault();
|
|
874
|
+
event.stopPropagation();
|
|
875
|
+
const handle = event.currentTarget;
|
|
876
|
+
const headerCell = handle.parentElement;
|
|
877
|
+
if (!headerCell) return;
|
|
878
|
+
const startWidth = headerCell.getBoundingClientRect().width;
|
|
879
|
+
const startX = event.clientX;
|
|
880
|
+
const minWidth = column.minWidth ?? 64;
|
|
881
|
+
let width = Math.round(startWidth);
|
|
882
|
+
const move = (moveEvent) => {
|
|
883
|
+
width = Math.max(minWidth, Math.round(startWidth + moveEvent.clientX - startX));
|
|
884
|
+
this.widthOverrides = {
|
|
885
|
+
...this.widthOverrides,
|
|
886
|
+
[column.field]: width
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
const finish = (upEvent) => {
|
|
890
|
+
handle.releasePointerCapture(upEvent.pointerId);
|
|
891
|
+
handle.removeEventListener("pointermove", move);
|
|
892
|
+
handle.removeEventListener("pointerup", finish);
|
|
893
|
+
handle.removeEventListener("pointercancel", finish);
|
|
894
|
+
handle.classList.remove("resizer--active");
|
|
895
|
+
this.dispatchEvent(new CustomEvent("column-resize", {
|
|
896
|
+
detail: {
|
|
897
|
+
field: column.field,
|
|
898
|
+
width
|
|
899
|
+
},
|
|
900
|
+
bubbles: true,
|
|
901
|
+
composed: true
|
|
902
|
+
}));
|
|
903
|
+
};
|
|
904
|
+
handle.setPointerCapture(event.pointerId);
|
|
905
|
+
handle.classList.add("resizer--active");
|
|
906
|
+
handle.addEventListener("pointermove", move);
|
|
907
|
+
handle.addEventListener("pointerup", finish);
|
|
908
|
+
handle.addEventListener("pointercancel", finish);
|
|
909
|
+
}
|
|
910
|
+
#handleKeyDown;
|
|
911
|
+
#headerHeight() {
|
|
912
|
+
return this.renderRoot.querySelector(".row--header")?.getBoundingClientRect().height ?? 0;
|
|
913
|
+
}
|
|
914
|
+
#measureRowHeight() {
|
|
915
|
+
const first = this.renderRoot.querySelector(".row:not(.row--header):not(.row--state)");
|
|
916
|
+
if (!first) return;
|
|
917
|
+
const height = first.getBoundingClientRect().height;
|
|
918
|
+
if (height > 0 && Math.abs(height - this.#measuredRowHeight) > .01) {
|
|
919
|
+
this.#measuredRowHeight = height;
|
|
920
|
+
this.requestUpdate();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
#measureColumnWidths() {
|
|
924
|
+
const cells = this.renderRoot.querySelectorAll(".cell--header[data-field]");
|
|
925
|
+
let pinnedChanged = false;
|
|
926
|
+
for (const cell of cells) {
|
|
927
|
+
const field = cell.dataset.field;
|
|
928
|
+
if (!field) continue;
|
|
929
|
+
const width = Math.round(cell.getBoundingClientRect().width);
|
|
930
|
+
if (width > 0 && this.#measuredWidths.get(field) !== width) {
|
|
931
|
+
this.#measuredWidths.set(field, width);
|
|
932
|
+
if (cell.classList.contains("cell--pinned")) pinnedChanged = true;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (pinnedChanged) this.requestUpdate();
|
|
936
|
+
}
|
|
937
|
+
/** Keeps the roving tabindex on a rendered cell: follows the window when focused, falls back to the header otherwise. */
|
|
938
|
+
#syncFocusToWindow() {
|
|
939
|
+
const { row, column } = this.focusedCell;
|
|
940
|
+
if (row < 0) return;
|
|
941
|
+
const offset = this.#pageStart;
|
|
942
|
+
const start = offset + this.#virtualizer.range.start;
|
|
943
|
+
const end = offset + this.#virtualizer.range.end;
|
|
944
|
+
if (row >= start && row < end) return;
|
|
945
|
+
if (this.renderRoot.activeElement) {
|
|
946
|
+
this.focusedCell = {
|
|
947
|
+
row: Math.min(Math.max(row, start), Math.max(start, end - 1)),
|
|
948
|
+
column
|
|
949
|
+
};
|
|
950
|
+
this.#pendingFocus = true;
|
|
951
|
+
} else this.focusedCell = {
|
|
952
|
+
row: HEADER_ROW,
|
|
953
|
+
column
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
__decorate([query(".viewport")], Table.prototype, "viewport", void 0);
|
|
958
|
+
__decorate([property({ converter: jsonPropertyConverter })], Table.prototype, "rows", void 0);
|
|
959
|
+
__decorate([property({ converter: jsonPropertyConverter })], Table.prototype, "columns", void 0);
|
|
960
|
+
__decorate([property({
|
|
961
|
+
type: String,
|
|
962
|
+
attribute: "row-key"
|
|
963
|
+
})], Table.prototype, "rowKey", void 0);
|
|
964
|
+
__decorate([property({ type: String })], Table.prototype, "selection", void 0);
|
|
965
|
+
__decorate([property({
|
|
966
|
+
type: Boolean,
|
|
967
|
+
attribute: "checkbox-selection"
|
|
968
|
+
})], Table.prototype, "checkboxSelection", void 0);
|
|
969
|
+
__decorate([property({
|
|
970
|
+
converter: arrayPropertyConverter,
|
|
971
|
+
reflect: true
|
|
972
|
+
})], Table.prototype, "value", void 0);
|
|
973
|
+
__decorate([property({
|
|
974
|
+
converter: sortModelConverter,
|
|
975
|
+
attribute: "sort",
|
|
976
|
+
reflect: true
|
|
977
|
+
})], Table.prototype, "sortModel", void 0);
|
|
978
|
+
__decorate([property({
|
|
979
|
+
type: Boolean,
|
|
980
|
+
attribute: "multi-sort"
|
|
981
|
+
})], Table.prototype, "multiSort", void 0);
|
|
982
|
+
__decorate([property({ type: Boolean })], Table.prototype, "sortable", void 0);
|
|
983
|
+
__decorate([property({ type: Boolean })], Table.prototype, "resizable", void 0);
|
|
984
|
+
__decorate([property({
|
|
985
|
+
type: Boolean,
|
|
986
|
+
reflect: true
|
|
987
|
+
})], Table.prototype, "stripe", void 0);
|
|
988
|
+
__decorate([property({ type: String })], Table.prototype, "virtual", void 0);
|
|
989
|
+
__decorate([property({
|
|
990
|
+
type: Number,
|
|
991
|
+
attribute: "row-height"
|
|
992
|
+
})], Table.prototype, "rowHeight", void 0);
|
|
993
|
+
__decorate([property({ type: Number })], Table.prototype, "overscan", void 0);
|
|
994
|
+
__decorate([property({
|
|
995
|
+
type: Number,
|
|
996
|
+
attribute: "virtual-threshold"
|
|
997
|
+
})], Table.prototype, "virtualThreshold", void 0);
|
|
998
|
+
__decorate([property({ attribute: false })], Table.prototype, "dataSource", void 0);
|
|
999
|
+
__decorate([property({
|
|
1000
|
+
type: Number,
|
|
1001
|
+
attribute: "block-size"
|
|
1002
|
+
})], Table.prototype, "blockSize", void 0);
|
|
1003
|
+
__decorate([property({
|
|
1004
|
+
type: Boolean,
|
|
1005
|
+
reflect: true
|
|
1006
|
+
})], Table.prototype, "loading", void 0);
|
|
1007
|
+
__decorate([property({ type: String })], Table.prototype, "error", void 0);
|
|
1008
|
+
__decorate([property({
|
|
1009
|
+
type: String,
|
|
1010
|
+
attribute: "empty-message"
|
|
1011
|
+
})], Table.prototype, "emptyMessage", void 0);
|
|
1012
|
+
__decorate([property({
|
|
1013
|
+
type: Number,
|
|
1014
|
+
reflect: true
|
|
1015
|
+
})], Table.prototype, "page", void 0);
|
|
1016
|
+
__decorate([property({
|
|
1017
|
+
type: Number,
|
|
1018
|
+
attribute: "page-size"
|
|
1019
|
+
})], Table.prototype, "pageSize", void 0);
|
|
1020
|
+
__decorate([state()], Table.prototype, "columnElements", void 0);
|
|
1021
|
+
__decorate([state()], Table.prototype, "widthOverrides", void 0);
|
|
1022
|
+
__decorate([state()], Table.prototype, "focusedCell", void 0);
|
|
1023
|
+
__decorate([state()], Table.prototype, "hasToolbar", void 0);
|
|
1024
|
+
__decorate([state()], Table.prototype, "hasFooter", void 0);
|
|
1025
|
+
__decorate([state()], Table.prototype, "remoteTotal", void 0);
|
|
1026
|
+
__decorate([provide({ context: pagerContext })], Table.prototype, "pager", void 0);
|
|
1027
|
+
Table = __decorate([customElement("c2-table")], Table);
|
|
1028
|
+
//#endregion
|
|
1029
|
+
export { Table };
|