@wpmoo/ui 0.7.1 → 0.8.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/README.md +18 -9
- package/certification.json +4 -2
- package/dist/assets/css/moo-ui.css +1116 -35
- package/dist/assets/css/moo-ui.min.css +2 -2
- package/dist/assets/css/moo.css +949 -17
- package/dist/assets/css/moo.min.css +2 -2
- package/dist/js/context-menu.js +274 -0
- package/dist/js/datatable.js +1080 -0
- package/package.json +6 -3
- package/THIRD_PARTY_NOTICES.md +0 -41
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
const instances = new WeakMap();
|
|
2
|
+
|
|
3
|
+
function normalize(value) {
|
|
4
|
+
return (value ?? "").toString().trim().toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class DataTable {
|
|
8
|
+
static getInstance(element) {
|
|
9
|
+
return element?.nodeType === 1 ? instances.get(element) || null : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getOrCreateInstance(element, config = {}) {
|
|
13
|
+
return DataTable.getInstance(element) || new DataTable(element, config);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(element, config = {}) {
|
|
17
|
+
if (element?.nodeType !== 1 || !element.matches(".datatable")) {
|
|
18
|
+
throw new TypeError("DataTable requires a .datatable root element.");
|
|
19
|
+
}
|
|
20
|
+
const existing = instances.get(element);
|
|
21
|
+
if (existing) {
|
|
22
|
+
return existing;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this._element = element;
|
|
26
|
+
this._document = element.ownerDocument;
|
|
27
|
+
this._window = this._document.defaultView;
|
|
28
|
+
this._table = element.querySelector(":scope .datatable-table");
|
|
29
|
+
this._tbody = this._table?.querySelector(":scope > tbody");
|
|
30
|
+
if (!this._table || !this._tbody) {
|
|
31
|
+
throw new TypeError("DataTable requires a .datatable-table with a tbody.");
|
|
32
|
+
}
|
|
33
|
+
this._cards = element.querySelector("[data-datatable-cards]");
|
|
34
|
+
const cardById = new Map(
|
|
35
|
+
Array.from(element.querySelectorAll("[data-datatable-card]")).map((card) => [card.dataset.datatableCardFor, card])
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
this._config = { ...config };
|
|
39
|
+
this._listeners = [];
|
|
40
|
+
this._pageSize = Number(element.dataset.datatablePageSize) || 10;
|
|
41
|
+
this._currentPage = 1;
|
|
42
|
+
this._sortKey = null;
|
|
43
|
+
this._sortDirection = null;
|
|
44
|
+
this._selectedIds = new Set();
|
|
45
|
+
this._facetSelections = new Map();
|
|
46
|
+
this._visibleFacets = new Map();
|
|
47
|
+
this._searchTerm = "";
|
|
48
|
+
this._tooltips = [];
|
|
49
|
+
this._filterMode = element.dataset.datatableFilterMode || "inline";
|
|
50
|
+
|
|
51
|
+
this._rows = Array.from(this._tbody.querySelectorAll(":scope > tr[data-datatable-row]")).map((tr, index) => ({
|
|
52
|
+
element: tr,
|
|
53
|
+
cardElement: cardById.get(tr.id) || null,
|
|
54
|
+
index,
|
|
55
|
+
search: normalize(tr.dataset.datatableSearch),
|
|
56
|
+
facets: this._readFacets(tr),
|
|
57
|
+
}));
|
|
58
|
+
this._readInitialFacetVisibility();
|
|
59
|
+
this._readInitialSort();
|
|
60
|
+
|
|
61
|
+
instances.set(element, this);
|
|
62
|
+
this._bindEvents();
|
|
63
|
+
this._render();
|
|
64
|
+
this._initBulkTooltips();
|
|
65
|
+
this._initViewToggle();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dispose() {
|
|
69
|
+
this._listeners.forEach(({ target, type, handler, options }) => {
|
|
70
|
+
target.removeEventListener(type, handler, options);
|
|
71
|
+
});
|
|
72
|
+
this._listeners = [];
|
|
73
|
+
this._tooltips.forEach((tooltip) => tooltip.dispose());
|
|
74
|
+
this._tooltips = [];
|
|
75
|
+
instances.delete(this._element);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getSelectedIds() {
|
|
79
|
+
return Array.from(this._selectedIds);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_listen(target, type, handler, options) {
|
|
83
|
+
if (!target) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
target.addEventListener(type, handler, options);
|
|
87
|
+
this._listeners.push({ target, type, handler, options });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_bootstrap(name) {
|
|
91
|
+
return this._window.bootstrap?.[name] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The bulk-actions bar's icon-only buttons carry data-bs-title instead of
|
|
95
|
+
// a bare title attribute so Bootstrap's own Tooltip owns the hover/focus
|
|
96
|
+
// affordance (matching the rest of Moo's icon-button contracts) rather
|
|
97
|
+
// than the browser's unstyled, delayed native tooltip. Tooltip has no
|
|
98
|
+
// Data API auto-init the way Dropdown does, so it needs this explicit,
|
|
99
|
+
// one-time setup; the bar's markup is static, so it never needs re-init.
|
|
100
|
+
// animation: false keeps show()/hide() synchronous -- with the default
|
|
101
|
+
// CSS fade, a click on a tooltip's own dropdown-toggle button fires
|
|
102
|
+
// "show.bs.dropdown" while the hover-triggered tooltip is still mid-fade,
|
|
103
|
+
// so the hide() below (see show.bs.dropdown in _bindEvents) would find
|
|
104
|
+
// it not yet fully shown and no-op, leaving it stuck open over the menu.
|
|
105
|
+
_initBulkTooltips() {
|
|
106
|
+
const Tooltip = this._bootstrap("Tooltip");
|
|
107
|
+
if (!Tooltip) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Bootstrap's Data store allows only one bound component instance per
|
|
111
|
+
// element (dom/data.js logs "doesn't allow more than one instance per
|
|
112
|
+
// element" and silently drops the second). The bulk-action option
|
|
113
|
+
// trigger (e.g. "Update status") carries both data-bs-title and
|
|
114
|
+
// data-bs-toggle="dropdown" on the same button, so it must keep its
|
|
115
|
+
// Dropdown instance and skip the JS-managed Tooltip here; its
|
|
116
|
+
// aria-label already gives it an accessible name without the hover
|
|
117
|
+
// affordance.
|
|
118
|
+
this._element
|
|
119
|
+
.querySelectorAll("[data-datatable-bulk-actions] [data-bs-title]:not([data-bs-toggle=\"dropdown\"])")
|
|
120
|
+
.forEach((trigger) => {
|
|
121
|
+
this._tooltips.push(Tooltip.getOrCreateInstance(trigger, { animation: false }));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_hideBulkTooltips({ blur = false } = {}) {
|
|
126
|
+
this._tooltips.forEach((tooltip) => {
|
|
127
|
+
tooltip.hide();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!blur) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const active = this._document.activeElement;
|
|
135
|
+
if (active?.closest?.("[data-datatable-bulk-actions]")) {
|
|
136
|
+
active.blur();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// responsive_mode="toggle" renders both the table and the card list and
|
|
141
|
+
// lets the reader pick between them (Odoo's List/Kanban switcher, not a
|
|
142
|
+
// developer-chosen breakpoint), so the choice belongs to the reader across
|
|
143
|
+
// visits, not just the current render.
|
|
144
|
+
_initViewToggle() {
|
|
145
|
+
const toggles = Array.from(this._element.querySelectorAll(".datatable-view-toggle"));
|
|
146
|
+
if (!toggles.length) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const inputs = toggles.flatMap((toggle) => Array.from(toggle.querySelectorAll("input")));
|
|
150
|
+
const storageKey = `moo-datatable-view:${this._element.id}`;
|
|
151
|
+
const setView = (value, { persist = false } = {}) => {
|
|
152
|
+
this._element.dataset.datatableView = value;
|
|
153
|
+
inputs.forEach((input) => {
|
|
154
|
+
input.checked = input.value === value;
|
|
155
|
+
});
|
|
156
|
+
if (persist) {
|
|
157
|
+
try {
|
|
158
|
+
this._window.localStorage.setItem(storageKey, value);
|
|
159
|
+
} catch {
|
|
160
|
+
// Storage may be unavailable (private browsing); the toggle still works for this session.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let stored = null;
|
|
165
|
+
try {
|
|
166
|
+
stored = this._window.localStorage.getItem(storageKey);
|
|
167
|
+
} catch {
|
|
168
|
+
stored = null;
|
|
169
|
+
}
|
|
170
|
+
if (stored === "table" || stored === "cards") {
|
|
171
|
+
setView(stored);
|
|
172
|
+
}
|
|
173
|
+
toggles.forEach((toggle) => {
|
|
174
|
+
this._listen(toggle, "change", (event) => {
|
|
175
|
+
const value = event.target.value;
|
|
176
|
+
if (value !== "table" && value !== "cards") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
setView(value, { persist: true });
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_readFacets(tr) {
|
|
185
|
+
const facets = {};
|
|
186
|
+
Array.from(tr.attributes).forEach((attr) => {
|
|
187
|
+
const match = /^data-datatable-facet-(.+)$/.exec(attr.name);
|
|
188
|
+
if (match) {
|
|
189
|
+
facets[match[1]] = attr.value;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return facets;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_readInitialFacetVisibility() {
|
|
196
|
+
this._element.querySelectorAll("[data-datatable-facet]").forEach((facetRoot) => {
|
|
197
|
+
const key = facetRoot.dataset.datatableFacet;
|
|
198
|
+
if (key) {
|
|
199
|
+
this._visibleFacets.set(key, !facetRoot.hidden);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
this._element.querySelectorAll("[data-datatable-filter-group]").forEach((group) => {
|
|
203
|
+
const key = group.dataset.datatableFilterGroup;
|
|
204
|
+
if (key && !this._visibleFacets.has(key)) {
|
|
205
|
+
this._visibleFacets.set(key, false);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
this._element.querySelectorAll("[data-datatable-filter-option-key]").forEach((option) => {
|
|
209
|
+
const key = option.dataset.datatableFilterOptionKey;
|
|
210
|
+
if (key && !this._visibleFacets.has(key)) {
|
|
211
|
+
this._visibleFacets.set(key, false);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_readInitialSort() {
|
|
217
|
+
const th = this._element.querySelector("th[data-datatable-initial-sort][data-datatable-column]");
|
|
218
|
+
const direction = th?.dataset.datatableInitialSort;
|
|
219
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this._sortKey = th.dataset.datatableColumn || null;
|
|
223
|
+
this._sortDirection = this._sortKey ? direction : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_trigger(name, detail = {}) {
|
|
227
|
+
this._element.dispatchEvent(
|
|
228
|
+
new this._window.CustomEvent(`${name}.moo.datatable`, {
|
|
229
|
+
bubbles: true,
|
|
230
|
+
detail,
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_sortValue(row, key) {
|
|
236
|
+
const cell = row.element.querySelector(`:scope > [data-datatable-column="${key}"]`);
|
|
237
|
+
if (!cell) {
|
|
238
|
+
return "";
|
|
239
|
+
}
|
|
240
|
+
return normalize(cell.dataset.datatableSortValue ?? cell.textContent);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_matchesFilters(row) {
|
|
244
|
+
if (this._searchTerm && !row.search.includes(this._searchTerm)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
for (const [facetKey, selected] of this._facetSelections) {
|
|
248
|
+
if (selected.size === 0) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!selected.has(row.facets[facetKey])) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_visibleRows() {
|
|
259
|
+
const filtered = this._rows.filter((row) => this._matchesFilters(row));
|
|
260
|
+
if (this._sortKey) {
|
|
261
|
+
const direction = this._sortDirection === "desc" ? -1 : 1;
|
|
262
|
+
filtered.sort((a, b) => {
|
|
263
|
+
const aValue = this._sortValue(a, this._sortKey);
|
|
264
|
+
const bValue = this._sortValue(b, this._sortKey);
|
|
265
|
+
if (aValue === bValue) {
|
|
266
|
+
return a.index - b.index;
|
|
267
|
+
}
|
|
268
|
+
return aValue > bValue ? direction : -direction;
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
filtered.sort((a, b) => a.index - b.index);
|
|
272
|
+
}
|
|
273
|
+
return filtered;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_pageCount(rowCount) {
|
|
277
|
+
return Math.max(1, Math.ceil(rowCount / this._pageSize));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_render() {
|
|
281
|
+
const filtered = this._visibleRows();
|
|
282
|
+
const pageCount = this._pageCount(filtered.length);
|
|
283
|
+
this._currentPage = Math.min(Math.max(1, this._currentPage), pageCount);
|
|
284
|
+
const start = (this._currentPage - 1) * this._pageSize;
|
|
285
|
+
const pageRows = filtered.slice(start, start + this._pageSize);
|
|
286
|
+
const pageRowIds = new Set(pageRows.map((row) => row.element.id));
|
|
287
|
+
|
|
288
|
+
this._rows.forEach((row) => {
|
|
289
|
+
const isVisible = pageRowIds.has(row.element.id);
|
|
290
|
+
row.element.hidden = !isVisible;
|
|
291
|
+
if (row.cardElement) {
|
|
292
|
+
row.cardElement.hidden = !isVisible;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// appendChild() always detaches-and-reinserts, which blurs a focused
|
|
297
|
+
// descendant (e.g. a row checkbox) even when the node is only moved to
|
|
298
|
+
// the position it already occupies. Selecting/deselecting a row never
|
|
299
|
+
// changes row order, so skip the reorder entirely unless the DOM order
|
|
300
|
+
// actually disagrees with the desired page order.
|
|
301
|
+
const currentOrder = Array.from(this._tbody.children)
|
|
302
|
+
.filter((element) => pageRowIds.has(element.id))
|
|
303
|
+
.map((element) => element.id);
|
|
304
|
+
const desiredOrder = pageRows.map((row) => row.element.id);
|
|
305
|
+
const orderChanged =
|
|
306
|
+
currentOrder.length !== desiredOrder.length ||
|
|
307
|
+
currentOrder.some((id, index) => id !== desiredOrder[index]);
|
|
308
|
+
if (orderChanged) {
|
|
309
|
+
pageRows.forEach((row) => {
|
|
310
|
+
this._tbody.appendChild(row.element);
|
|
311
|
+
if (this._cards && row.cardElement) {
|
|
312
|
+
this._cards.appendChild(row.cardElement);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this._renderToolbarState();
|
|
318
|
+
this._renderSortHeaders();
|
|
319
|
+
this._renderSelection(pageRows);
|
|
320
|
+
this._renderFrameState(filtered.length);
|
|
321
|
+
this._renderEmptyState(filtered.length);
|
|
322
|
+
this._renderPagination(filtered.length, pageCount);
|
|
323
|
+
this._renderPageSize();
|
|
324
|
+
this._renderBulkActions();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_renderToolbarState() {
|
|
328
|
+
const hasSearch = Boolean(this._searchTerm);
|
|
329
|
+
const hasFacets = Array.from(this._facetSelections.values()).some((set) => set.size > 0);
|
|
330
|
+
const resetButton = this._element.querySelector("[data-datatable-reset]");
|
|
331
|
+
if (resetButton) {
|
|
332
|
+
resetButton.hidden = !(hasSearch || hasFacets);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this._element.querySelectorAll("[data-datatable-facet]").forEach((facetRoot) => {
|
|
336
|
+
const key = facetRoot.dataset.datatableFacet;
|
|
337
|
+
const selected = this._facetSelections.get(key) || new Set();
|
|
338
|
+
if (this._visibleFacets.has(key)) {
|
|
339
|
+
facetRoot.hidden = !this._visibleFacets.get(key);
|
|
340
|
+
}
|
|
341
|
+
const trigger = facetRoot.querySelector(".datatable-facet-trigger");
|
|
342
|
+
trigger?.classList.toggle("datatable-facet-trigger-active", selected.size > 0);
|
|
343
|
+
const summary = facetRoot.querySelector("[data-datatable-facet-summary]");
|
|
344
|
+
if (summary) {
|
|
345
|
+
summary.hidden = selected.size === 0;
|
|
346
|
+
summary.replaceChildren();
|
|
347
|
+
if (selected.size > 2) {
|
|
348
|
+
summary.appendChild(this._createFacetBadge(`${selected.size} selected`));
|
|
349
|
+
} else {
|
|
350
|
+
selected.forEach((value) => {
|
|
351
|
+
const option = facetRoot.querySelector(`[data-datatable-facet-option="${value}"]`);
|
|
352
|
+
const label = option?.querySelector("[data-datatable-facet-option-label]")?.textContent ?? value;
|
|
353
|
+
summary.appendChild(this._createFacetBadge(label));
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
facetRoot.querySelectorAll("[data-datatable-facet-option]").forEach((option) => {
|
|
358
|
+
const isSelected = selected.has(option.dataset.datatableFacetOption);
|
|
359
|
+
option.classList.toggle("active", isSelected);
|
|
360
|
+
option.setAttribute("aria-pressed", String(isSelected));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
this._element.querySelectorAll("[data-datatable-filter-toggle]").forEach((toggle) => {
|
|
365
|
+
const key = toggle.dataset.datatableFilterToggle;
|
|
366
|
+
const visible = this._visibleFacets.get(key) ?? true;
|
|
367
|
+
toggle.classList.toggle("active", visible);
|
|
368
|
+
toggle.setAttribute("aria-pressed", visible ? "true" : "false");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
this._element.querySelectorAll("[data-datatable-filter-option]").forEach((option) => {
|
|
372
|
+
const key = option.dataset.datatableFilterOptionKey;
|
|
373
|
+
const selected = this._facetSelections.get(key) || new Set();
|
|
374
|
+
const active = selected.has(option.dataset.datatableFilterOption);
|
|
375
|
+
option.classList.toggle("active", active);
|
|
376
|
+
option.setAttribute("aria-pressed", active ? "true" : "false");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
this._element.querySelectorAll("[data-datatable-filter-group-summary]").forEach((summary) => {
|
|
380
|
+
const selected = this._facetSelections.get(summary.dataset.datatableFilterGroupSummary) || new Set();
|
|
381
|
+
summary.hidden = selected.size === 0;
|
|
382
|
+
summary.textContent = selected.size > 0 ? `${selected.size} selected` : "";
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_createFacetBadge(text) {
|
|
387
|
+
const badge = this._document.createElement("span");
|
|
388
|
+
badge.className = "datatable-facet-badge";
|
|
389
|
+
badge.textContent = text;
|
|
390
|
+
return badge;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_renderSelection(pageRows) {
|
|
394
|
+
const selectAllControls = Array.from(this._element.querySelectorAll("[data-datatable-select-all]"));
|
|
395
|
+
pageRows.forEach((row) => {
|
|
396
|
+
[row.element, row.cardElement].forEach((container) => {
|
|
397
|
+
const checkbox = container?.querySelector("[data-datatable-select-row]");
|
|
398
|
+
if (checkbox) {
|
|
399
|
+
checkbox.checked = this._selectedIds.has(row.element.id);
|
|
400
|
+
}
|
|
401
|
+
container?.classList.toggle("datatable-row-selected", this._selectedIds.has(row.element.id));
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
const total = pageRows.length;
|
|
405
|
+
const checked = pageRows.filter((row) => this._selectedIds.has(row.element.id)).length;
|
|
406
|
+
selectAllControls.forEach((control) => {
|
|
407
|
+
control.checked = total > 0 && checked === total;
|
|
408
|
+
control.indeterminate = checked > 0 && checked < total;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_renderBulkActions() {
|
|
413
|
+
const bar = this._element.querySelector("[data-datatable-bulk-actions]");
|
|
414
|
+
if (!bar) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
bar.hidden = this._selectedIds.size === 0;
|
|
418
|
+
const count = bar.querySelector("[data-datatable-bulk-count]");
|
|
419
|
+
if (count) {
|
|
420
|
+
count.textContent = String(this._selectedIds.size);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
_renderFrameState(totalRows) {
|
|
425
|
+
const isEmpty = totalRows === 0;
|
|
426
|
+
this._element.querySelectorAll(".datatable-frame, .datatable-card-frame").forEach((frame) => {
|
|
427
|
+
frame.hidden = isEmpty;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_renderEmptyState(totalRows) {
|
|
432
|
+
const empty = this._element.querySelector("[data-datatable-empty]");
|
|
433
|
+
if (!empty) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const hasRows = this._rows.length > 0;
|
|
437
|
+
empty.hidden = totalRows !== 0;
|
|
438
|
+
empty.querySelector(".datatable-empty-title")?.replaceChildren(
|
|
439
|
+
this._document.createTextNode(hasRows ? "No matching results" : "No rows to display")
|
|
440
|
+
);
|
|
441
|
+
const copy = empty.querySelector(".datatable-empty-copy");
|
|
442
|
+
if (copy) {
|
|
443
|
+
copy.hidden = !hasRows;
|
|
444
|
+
}
|
|
445
|
+
const reset = empty.querySelector("[data-datatable-empty-reset]");
|
|
446
|
+
if (reset) {
|
|
447
|
+
reset.hidden = !hasRows;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_sortAriaValue(direction) {
|
|
452
|
+
if (direction === "asc") {
|
|
453
|
+
return "ascending";
|
|
454
|
+
}
|
|
455
|
+
if (direction === "desc") {
|
|
456
|
+
return "descending";
|
|
457
|
+
}
|
|
458
|
+
return "none";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_renderSortHeaders() {
|
|
462
|
+
this._element.querySelectorAll("th[data-datatable-column]").forEach((th) => {
|
|
463
|
+
const trigger = th.querySelector("[data-datatable-sort-key]");
|
|
464
|
+
if (!trigger) {
|
|
465
|
+
delete th.dataset.datatableSortState;
|
|
466
|
+
delete th.dataset.datatableSortDirection;
|
|
467
|
+
th.removeAttribute("aria-sort");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const key = th.dataset.datatableColumn;
|
|
471
|
+
const direction = key && key === this._sortKey ? this._sortDirection : "none";
|
|
472
|
+
th.dataset.datatableSortState = direction || "none";
|
|
473
|
+
th.setAttribute("aria-sort", this._sortAriaValue(direction));
|
|
474
|
+
if (direction === "asc" || direction === "desc") {
|
|
475
|
+
th.dataset.datatableSortDirection = direction;
|
|
476
|
+
} else {
|
|
477
|
+
delete th.dataset.datatableSortDirection;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_renderPagination(totalRows, pageCount) {
|
|
483
|
+
const first = this._element.querySelector("[data-datatable-page-first]");
|
|
484
|
+
const prev = this._element.querySelector("[data-datatable-page-prev]");
|
|
485
|
+
const next = this._element.querySelector("[data-datatable-page-next]");
|
|
486
|
+
const last = this._element.querySelector("[data-datatable-page-last]");
|
|
487
|
+
const atStart = this._currentPage <= 1;
|
|
488
|
+
const atEnd = this._currentPage >= pageCount;
|
|
489
|
+
[first, prev].forEach((button) => this._setPageItemDisabled(button, atStart));
|
|
490
|
+
[next, last].forEach((button) => this._setPageItemDisabled(button, atEnd));
|
|
491
|
+
|
|
492
|
+
const summary = this._element.querySelector("[data-datatable-results-summary]");
|
|
493
|
+
if (summary) {
|
|
494
|
+
if (totalRows === 0) {
|
|
495
|
+
summary.textContent = "No results";
|
|
496
|
+
} else {
|
|
497
|
+
const start = (this._currentPage - 1) * this._pageSize + 1;
|
|
498
|
+
const end = Math.min(this._currentPage * this._pageSize, totalRows);
|
|
499
|
+
summary.textContent = `Showing ${start}-${end} of ${totalRows}`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const template = this._element.querySelector("template[data-datatable-page-numbers]");
|
|
504
|
+
if (!template) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
template.parentNode
|
|
508
|
+
.querySelectorAll("[data-datatable-page-number], [data-datatable-page-ellipsis]")
|
|
509
|
+
.forEach((node) => node.remove());
|
|
510
|
+
|
|
511
|
+
const pages = this._pageWindow(this._currentPage, pageCount);
|
|
512
|
+
const fragment = this._document.createDocumentFragment();
|
|
513
|
+
pages.forEach((page) => {
|
|
514
|
+
const li = this._document.createElement("li");
|
|
515
|
+
li.className = "page-item";
|
|
516
|
+
if (page === "…") {
|
|
517
|
+
li.dataset.datatablePageEllipsis = "";
|
|
518
|
+
li.innerHTML = '<span class="page-link">…</span>';
|
|
519
|
+
} else {
|
|
520
|
+
li.dataset.datatablePageNumber = String(page);
|
|
521
|
+
if (page === this._currentPage) {
|
|
522
|
+
li.classList.add("active");
|
|
523
|
+
li.setAttribute("aria-current", "page");
|
|
524
|
+
}
|
|
525
|
+
const button = this._document.createElement("button");
|
|
526
|
+
button.type = "button";
|
|
527
|
+
button.className = "page-link";
|
|
528
|
+
button.textContent = String(page);
|
|
529
|
+
button.setAttribute("aria-label", `Go to page ${page}`);
|
|
530
|
+
button.dataset.datatablePageGo = String(page);
|
|
531
|
+
li.appendChild(button);
|
|
532
|
+
}
|
|
533
|
+
fragment.appendChild(li);
|
|
534
|
+
});
|
|
535
|
+
template.parentNode.insertBefore(fragment, template);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_renderPageSize() {
|
|
539
|
+
const currentSize = String(this._pageSize);
|
|
540
|
+
const value = this._element.querySelector("[data-datatable-page-size-value]");
|
|
541
|
+
if (value) {
|
|
542
|
+
value.textContent = currentSize;
|
|
543
|
+
}
|
|
544
|
+
const select = this._element.querySelector("[data-datatable-page-size-select]");
|
|
545
|
+
if (select) {
|
|
546
|
+
select.value = currentSize;
|
|
547
|
+
}
|
|
548
|
+
this._element.querySelectorAll("[data-datatable-page-size-option]").forEach((option) => {
|
|
549
|
+
const active = option.dataset.datatablePageSizeOption === currentSize;
|
|
550
|
+
option.classList.toggle("active", active);
|
|
551
|
+
option.setAttribute("aria-pressed", active ? "true" : "false");
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
_setPageItemDisabled(button, disabled) {
|
|
556
|
+
if (!button) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
button.disabled = disabled;
|
|
560
|
+
button.closest(".page-item")?.classList.toggle("disabled", disabled);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
_pageWindow(current, total) {
|
|
564
|
+
if (total <= 7) {
|
|
565
|
+
return Array.from({ length: total }, (_, index) => index + 1);
|
|
566
|
+
}
|
|
567
|
+
const pages = new Set([1, total, current - 1, current, current + 1]);
|
|
568
|
+
const sorted = Array.from(pages)
|
|
569
|
+
.filter((page) => page >= 1 && page <= total)
|
|
570
|
+
.sort((a, b) => a - b);
|
|
571
|
+
const withEllipsis = [];
|
|
572
|
+
sorted.forEach((page, index) => {
|
|
573
|
+
if (index > 0 && page - sorted[index - 1] > 1) {
|
|
574
|
+
withEllipsis.push("…");
|
|
575
|
+
}
|
|
576
|
+
withEllipsis.push(page);
|
|
577
|
+
});
|
|
578
|
+
return withEllipsis;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
_isColumnFixed(key) {
|
|
582
|
+
return this._element
|
|
583
|
+
.querySelector(`th[data-datatable-column="${key}"]`)
|
|
584
|
+
?.hasAttribute("data-datatable-column-fixed") || false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_setColumnVisible(key, visible, { syncViewToggle = true } = {}) {
|
|
588
|
+
if (!visible && this._isColumnFixed(key)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this._element.querySelectorAll(`[data-datatable-column="${key}"], [data-datatable-detail-column="${key}"]`).forEach((cell) => {
|
|
592
|
+
cell.classList.toggle("datatable-col-hidden", !visible);
|
|
593
|
+
});
|
|
594
|
+
if (syncViewToggle) {
|
|
595
|
+
this._element.querySelectorAll(`[data-datatable-column-toggle="${key}"]`).forEach((toggle) => {
|
|
596
|
+
toggle.classList.toggle("active", visible);
|
|
597
|
+
toggle.setAttribute("aria-pressed", String(visible));
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
_setSort(key, direction) {
|
|
603
|
+
const validDirection = direction === "asc" || direction === "desc" ? direction : null;
|
|
604
|
+
this._sortKey = validDirection ? key : null;
|
|
605
|
+
this._sortDirection = validDirection;
|
|
606
|
+
this._render();
|
|
607
|
+
this._trigger("sort", { key: this._sortKey, direction: this._sortDirection });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_handleSortAction(event) {
|
|
611
|
+
const trigger = event.target.closest("[data-datatable-sort-action]");
|
|
612
|
+
if (!trigger) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const menu = trigger.closest(".dropdown-menu");
|
|
616
|
+
const key = menu
|
|
617
|
+
?.closest(".datatable-sort")
|
|
618
|
+
?.querySelector("[data-datatable-sort-key]")?.dataset.datatableSortKey;
|
|
619
|
+
if (!key) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const action = trigger.dataset.datatableSortAction;
|
|
623
|
+
if (action === "hide") {
|
|
624
|
+
this._setColumnVisible(key, false);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this._setSort(key, action);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
_handleFacetClick(event) {
|
|
631
|
+
const option = event.target.closest("[data-datatable-facet-option]");
|
|
632
|
+
const clear = event.target.closest("[data-datatable-facet-clear]");
|
|
633
|
+
if (!option && !clear) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const facetRoot = event.target.closest("[data-datatable-facet]");
|
|
637
|
+
const key = facetRoot?.dataset.datatableFacet;
|
|
638
|
+
if (!key) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const selected = this._facetSelections.get(key) || new Set();
|
|
642
|
+
if (clear) {
|
|
643
|
+
selected.clear();
|
|
644
|
+
} else {
|
|
645
|
+
const value = option.dataset.datatableFacetOption;
|
|
646
|
+
if (selected.has(value)) {
|
|
647
|
+
selected.delete(value);
|
|
648
|
+
} else {
|
|
649
|
+
selected.add(value);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
this._facetSelections.set(key, selected);
|
|
653
|
+
if (this._filterMode === "picker" && selected.size === 0) {
|
|
654
|
+
this._visibleFacets.set(key, false);
|
|
655
|
+
}
|
|
656
|
+
this._currentPage = 1;
|
|
657
|
+
this._render();
|
|
658
|
+
this._trigger("filter");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
_handleFacetSearch(event) {
|
|
662
|
+
const search = event.target.closest("[data-datatable-facet-search]");
|
|
663
|
+
if (!search) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const facetRoot = search.closest("[data-datatable-facet]");
|
|
667
|
+
const query = normalize(search.value);
|
|
668
|
+
facetRoot?.querySelectorAll("[data-datatable-facet-option]").forEach((option) => {
|
|
669
|
+
const label = normalize(option.querySelector("[data-datatable-facet-option-label]")?.textContent || option.textContent);
|
|
670
|
+
option.closest("li")?.classList.toggle("datatable-facet-search-hidden", query.length > 0 && !label.includes(query));
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Bootstrap's own dropdown keyboard navigation moves focus between
|
|
675
|
+
// .dropdown-item siblings, but it deliberately leaves focus alone while an
|
|
676
|
+
// <input> has it (arrow keys inside a text field move the caret, not a
|
|
677
|
+
// list). A facet's own search box needs an explicit bridge so ArrowDown
|
|
678
|
+
// hands off to the first result instead of doing nothing.
|
|
679
|
+
_handleFacetSearchKeydown(event) {
|
|
680
|
+
if (event.key !== "ArrowDown") {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const search = event.target.closest("[data-datatable-facet-search]");
|
|
684
|
+
if (!search) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const facetRoot = search.closest("[data-datatable-facet]");
|
|
688
|
+
const options = Array.from(facetRoot?.querySelectorAll("[data-datatable-facet-option]") || []);
|
|
689
|
+
const firstVisible = options.find(
|
|
690
|
+
(option) => !option.closest("li")?.classList.contains("datatable-facet-search-hidden")
|
|
691
|
+
);
|
|
692
|
+
if (firstVisible) {
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
firstVisible.focus();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
_handleFilterOptionClick(event) {
|
|
699
|
+
const option = event.target.closest("[data-datatable-filter-option]");
|
|
700
|
+
if (!option) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const key = option.dataset.datatableFilterOptionKey;
|
|
704
|
+
const value = option.dataset.datatableFilterOption;
|
|
705
|
+
if (!key || !this._visibleFacets.has(key)) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this._visibleFacets.set(key, true);
|
|
709
|
+
const selected = this._facetSelections.get(key) || new Set();
|
|
710
|
+
if (selected.has(value)) {
|
|
711
|
+
selected.delete(value);
|
|
712
|
+
} else {
|
|
713
|
+
selected.add(value);
|
|
714
|
+
}
|
|
715
|
+
this._facetSelections.set(key, selected);
|
|
716
|
+
if (selected.size === 0) {
|
|
717
|
+
this._visibleFacets.set(key, false);
|
|
718
|
+
}
|
|
719
|
+
this._currentPage = 1;
|
|
720
|
+
this._render();
|
|
721
|
+
this._trigger("filter");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
_showFilterPickerPanel(key = null) {
|
|
725
|
+
this._element.querySelectorAll("[data-datatable-filter-panel]").forEach((panel) => {
|
|
726
|
+
const isFacetList = panel.dataset.datatableFilterPanel === "facets";
|
|
727
|
+
const isRequestedOptions = panel.dataset.datatableFilterPanelKey === key;
|
|
728
|
+
panel.hidden = key ? !isRequestedOptions : !isFacetList;
|
|
729
|
+
});
|
|
730
|
+
this._element
|
|
731
|
+
.querySelector(".datatable-search-filter-menu")
|
|
732
|
+
?.setAttribute("data-datatable-filter-menu-panel", key ? "options" : "facets");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
_handleFilterPickerClick(event) {
|
|
736
|
+
const group = event.target.closest("[data-datatable-filter-group]");
|
|
737
|
+
if (group) {
|
|
738
|
+
const key = group.dataset.datatableFilterGroup;
|
|
739
|
+
this._showFilterPickerPanel(key);
|
|
740
|
+
const search = this._element.querySelector(`[data-datatable-filter-option-search="${key}"]`);
|
|
741
|
+
const option = this._element.querySelector(`[data-datatable-filter-option-key="${key}"]`);
|
|
742
|
+
(search || option)?.focus({ preventScroll: true });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (event.target.closest("[data-datatable-filter-back]")) {
|
|
746
|
+
this._showFilterPickerPanel();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
_handleFilterOptionSearch(event) {
|
|
751
|
+
const search = event.target.closest("[data-datatable-filter-option-search]");
|
|
752
|
+
if (!search) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const key = search.dataset.datatableFilterOptionSearch;
|
|
756
|
+
const query = normalize(search.value);
|
|
757
|
+
this._element.querySelectorAll(`[data-datatable-filter-option-key="${key}"]`).forEach((option) => {
|
|
758
|
+
const label = normalize(option.querySelector("[data-datatable-filter-option-label]")?.textContent || option.textContent);
|
|
759
|
+
option.closest("li")?.classList.toggle("datatable-filter-option-search-hidden", query.length > 0 && !label.includes(query));
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Same bridge as _handleFacetSearchKeydown, for the filter-picker's
|
|
764
|
+
// drill-down option search box.
|
|
765
|
+
_handleFilterOptionSearchKeydown(event) {
|
|
766
|
+
if (event.key !== "ArrowDown") {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const search = event.target.closest("[data-datatable-filter-option-search]");
|
|
770
|
+
if (!search) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const key = search.dataset.datatableFilterOptionSearch;
|
|
774
|
+
const options = Array.from(
|
|
775
|
+
this._element.querySelectorAll(`[data-datatable-filter-option-key="${key}"]`)
|
|
776
|
+
);
|
|
777
|
+
const firstVisible = options.find(
|
|
778
|
+
(option) => !option.closest("li")?.classList.contains("datatable-filter-option-search-hidden")
|
|
779
|
+
);
|
|
780
|
+
if (firstVisible) {
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
firstVisible.focus();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
_handleFilterToggleClick(event) {
|
|
787
|
+
const toggle = event.target.closest("[data-datatable-filter-toggle]");
|
|
788
|
+
if (!toggle) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const key = toggle.dataset.datatableFilterToggle;
|
|
792
|
+
if (!key || !this._visibleFacets.has(key)) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const nextVisible = !this._visibleFacets.get(key);
|
|
796
|
+
this._visibleFacets.set(key, nextVisible);
|
|
797
|
+
if (!nextVisible) {
|
|
798
|
+
this._facetSelections.get(key)?.clear();
|
|
799
|
+
this._currentPage = 1;
|
|
800
|
+
this._trigger("filter");
|
|
801
|
+
}
|
|
802
|
+
this._render();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
_handleColumnToggleClick(event) {
|
|
806
|
+
const toggle = event.target.closest("[data-datatable-column-toggle]");
|
|
807
|
+
if (!toggle) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const key = toggle.dataset.datatableColumnToggle;
|
|
811
|
+
const visible = toggle.getAttribute("aria-pressed") === "true";
|
|
812
|
+
this._setColumnVisible(key, visible, { syncViewToggle: false });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
_handleReset() {
|
|
816
|
+
const search = this._element.querySelector("[data-datatable-search]");
|
|
817
|
+
if (search) {
|
|
818
|
+
search.value = "";
|
|
819
|
+
}
|
|
820
|
+
this._searchTerm = "";
|
|
821
|
+
this._facetSelections.forEach((selected) => selected.clear());
|
|
822
|
+
if (this._filterMode === "picker") {
|
|
823
|
+
this._visibleFacets.forEach((value, key) => {
|
|
824
|
+
this._visibleFacets.set(key, false);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
this._currentPage = 1;
|
|
828
|
+
this._render();
|
|
829
|
+
this._trigger("filter");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
_handleSelectAll(event) {
|
|
833
|
+
const checked = event.target.checked;
|
|
834
|
+
const filtered = this._visibleRows();
|
|
835
|
+
const start = (this._currentPage - 1) * this._pageSize;
|
|
836
|
+
const pageRows = filtered.slice(start, start + this._pageSize);
|
|
837
|
+
pageRows.forEach((row) => {
|
|
838
|
+
if (checked) {
|
|
839
|
+
this._selectedIds.add(row.element.id);
|
|
840
|
+
} else {
|
|
841
|
+
this._selectedIds.delete(row.element.id);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
this._render();
|
|
845
|
+
this._trigger("select");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
_handleSelectRow(event) {
|
|
849
|
+
const checkbox = event.target.closest("[data-datatable-select-row]");
|
|
850
|
+
if (!checkbox) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const row = checkbox.closest("[data-datatable-row]");
|
|
854
|
+
const card = checkbox.closest("[data-datatable-card]");
|
|
855
|
+
const rowId = row?.id || card?.dataset.datatableCardFor;
|
|
856
|
+
if (!rowId) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (checkbox.checked) {
|
|
860
|
+
this._selectedIds.add(rowId);
|
|
861
|
+
} else {
|
|
862
|
+
this._selectedIds.delete(rowId);
|
|
863
|
+
}
|
|
864
|
+
this._render();
|
|
865
|
+
this._trigger("select");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
_handleBulkClick(event) {
|
|
869
|
+
if (event.target.closest("[data-datatable-bulk-clear]")) {
|
|
870
|
+
this._clearSelection();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const updateTrigger = event.target.closest("[data-datatable-bulk-update]");
|
|
874
|
+
if (updateTrigger) {
|
|
875
|
+
const key = updateTrigger.dataset.datatableBulkUpdate;
|
|
876
|
+
const value = updateTrigger.dataset.datatableBulkUpdateValue;
|
|
877
|
+
this._applyBulkUpdate(key, value, updateTrigger);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const actionTrigger = event.target.closest("[data-datatable-bulk-action]");
|
|
881
|
+
if (actionTrigger) {
|
|
882
|
+
const action = actionTrigger.dataset.datatableBulkAction;
|
|
883
|
+
if (action === "delete") {
|
|
884
|
+
this._applyBulkDelete();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
this._trigger(`bulk-${action}`, { ids: this.getSelectedIds() });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
_clearSelection() {
|
|
892
|
+
this._hideBulkTooltips({ blur: true });
|
|
893
|
+
this._selectedIds.clear();
|
|
894
|
+
this._render();
|
|
895
|
+
this._trigger("select");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
_applyBulkUpdate(key, value, trigger) {
|
|
899
|
+
const icon = trigger.querySelector("svg")?.cloneNode(true);
|
|
900
|
+
const label = trigger.querySelector("span")?.textContent?.trim() ?? value;
|
|
901
|
+
this._selectedIds.forEach((id) => {
|
|
902
|
+
const row = this._rows.find((candidate) => candidate.element.id === id);
|
|
903
|
+
if (!row) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
row.element.setAttribute(`data-datatable-facet-${key}`, value);
|
|
907
|
+
row.facets[key] = value;
|
|
908
|
+
const cell = row.element.querySelector(`[data-datatable-column="${key}"]`);
|
|
909
|
+
if (!cell) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const labelTarget = cell.querySelector("[data-datatable-cell-label]");
|
|
913
|
+
if (labelTarget) {
|
|
914
|
+
labelTarget.textContent = label;
|
|
915
|
+
} else {
|
|
916
|
+
cell.textContent = label;
|
|
917
|
+
}
|
|
918
|
+
const currentIcon = cell.querySelector("svg");
|
|
919
|
+
if (currentIcon && icon) {
|
|
920
|
+
currentIcon.replaceWith(icon.cloneNode(true));
|
|
921
|
+
}
|
|
922
|
+
const cardValue = row.cardElement?.querySelector(`[data-datatable-detail-column="${key}"] .datatable-card-value`);
|
|
923
|
+
if (cardValue) {
|
|
924
|
+
cardValue.innerHTML = cell.innerHTML;
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
this._render();
|
|
928
|
+
this._trigger("bulk-update", { key, value, ids: this.getSelectedIds() });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
_applyBulkDelete() {
|
|
932
|
+
const ids = this.getSelectedIds();
|
|
933
|
+
ids.forEach((id) => {
|
|
934
|
+
const index = this._rows.findIndex((row) => row.element.id === id);
|
|
935
|
+
if (index !== -1) {
|
|
936
|
+
this._rows[index].element.remove();
|
|
937
|
+
this._rows[index].cardElement?.remove();
|
|
938
|
+
this._rows.splice(index, 1);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
this._selectedIds.clear();
|
|
942
|
+
this._render();
|
|
943
|
+
this._trigger("bulk-delete", { ids });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
_handlePageClick(event) {
|
|
947
|
+
const goButton = event.target.closest("[data-datatable-page-go]");
|
|
948
|
+
if (goButton) {
|
|
949
|
+
this._currentPage = Number(goButton.dataset.datatablePageGo);
|
|
950
|
+
this._render();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (event.target.closest("[data-datatable-page-first]")) {
|
|
954
|
+
this._currentPage = 1;
|
|
955
|
+
this._render();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (event.target.closest("[data-datatable-page-prev]")) {
|
|
959
|
+
this._currentPage -= 1;
|
|
960
|
+
this._render();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (event.target.closest("[data-datatable-page-next]")) {
|
|
964
|
+
this._currentPage += 1;
|
|
965
|
+
this._render();
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (event.target.closest("[data-datatable-page-last]")) {
|
|
969
|
+
this._currentPage = this._pageCount(this._visibleRows().length);
|
|
970
|
+
this._render();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
_setPageSize(value) {
|
|
975
|
+
const nextPageSize = Number(value);
|
|
976
|
+
if (!Number.isFinite(nextPageSize) || nextPageSize <= 0) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
this._pageSize = nextPageSize;
|
|
980
|
+
this._element.dataset.datatablePageSize = String(nextPageSize);
|
|
981
|
+
this._currentPage = 1;
|
|
982
|
+
this._render();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
_bindEvents() {
|
|
986
|
+
const search = this._element.querySelector("[data-datatable-search]");
|
|
987
|
+
this._listen(search, "input", (event) => {
|
|
988
|
+
this._searchTerm = normalize(event.target.value);
|
|
989
|
+
this._currentPage = 1;
|
|
990
|
+
this._render();
|
|
991
|
+
this._trigger("filter");
|
|
992
|
+
});
|
|
993
|
+
this._listen(search, "keydown", (event) => {
|
|
994
|
+
if (event.key !== "Enter") {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
event.preventDefault();
|
|
998
|
+
const trigger = this._element.querySelector("[data-datatable-filter-menu-trigger]");
|
|
999
|
+
const Dropdown = this._bootstrap("Dropdown");
|
|
1000
|
+
if (trigger && Dropdown) {
|
|
1001
|
+
Dropdown.getOrCreateInstance(trigger).hide();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
this._listen(this._element, "click", (event) => {
|
|
1006
|
+
this._handleSortAction(event);
|
|
1007
|
+
this._handleFilterPickerClick(event);
|
|
1008
|
+
this._handleFilterOptionClick(event);
|
|
1009
|
+
this._handleFilterToggleClick(event);
|
|
1010
|
+
this._handleFacetClick(event);
|
|
1011
|
+
this._handleColumnToggleClick(event);
|
|
1012
|
+
this._handlePageClick(event);
|
|
1013
|
+
this._handleBulkClick(event);
|
|
1014
|
+
if (event.target.closest("[data-datatable-empty-reset]")) {
|
|
1015
|
+
this._handleReset();
|
|
1016
|
+
}
|
|
1017
|
+
if (event.target.closest("[data-datatable-select-row]")) {
|
|
1018
|
+
this._handleSelectRow(event);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// Bound to the document, not this._element: a checkbox click does not
|
|
1023
|
+
// reliably move focus into the table (WebKit never focuses form
|
|
1024
|
+
// controls on click, and even where it does the bulk-actions bar itself
|
|
1025
|
+
// sits outside the table's DOM flow), so a listener scoped to the table
|
|
1026
|
+
// would miss Escape whenever focus is elsewhere on the page.
|
|
1027
|
+
this._listen(this._document, "keydown", (event) => {
|
|
1028
|
+
if (event.key === "Escape" && this._selectedIds.size > 0) {
|
|
1029
|
+
this._clearSelection();
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// A hover-triggered tooltip does not hide itself when a click opens the
|
|
1034
|
+
// same button's dropdown (Bootstrap Tooltip only listens for
|
|
1035
|
+
// mouseleave/blur), so it would otherwise sit on top of the open menu.
|
|
1036
|
+
// hide() alone is not enough: opening the dropdown shifts layout under
|
|
1037
|
+
// the still-hovered cursor, which makes the browser refire mouseenter
|
|
1038
|
+
// and the tooltip re-show a moment later. disable()/enable() -- not
|
|
1039
|
+
// hide() -- is Bootstrap's own way to suppress that: it makes show() a
|
|
1040
|
+
// no-op for as long as the dropdown stays open, however it gets
|
|
1041
|
+
// re-triggered.
|
|
1042
|
+
this._listen(this._element, "show.bs.dropdown", (event) => {
|
|
1043
|
+
const tooltip = this._bootstrap("Tooltip")?.getInstance(event.target);
|
|
1044
|
+
tooltip?.hide();
|
|
1045
|
+
tooltip?.disable();
|
|
1046
|
+
});
|
|
1047
|
+
this._listen(this._element, "hidden.bs.dropdown", (event) => {
|
|
1048
|
+
this._bootstrap("Tooltip")?.getInstance(event.target)?.enable();
|
|
1049
|
+
if (event.target.matches("[data-datatable-filter-menu-trigger]")) {
|
|
1050
|
+
this._showFilterPickerPanel();
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
this._element.querySelectorAll("[data-datatable-filter-option-search]").forEach((searchInput) => {
|
|
1055
|
+
this._listen(searchInput, "input", (event) => this._handleFilterOptionSearch(event));
|
|
1056
|
+
this._listen(searchInput, "keydown", (event) => this._handleFilterOptionSearchKeydown(event));
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
this._element.querySelectorAll("[data-datatable-facet-search]").forEach((searchInput) => {
|
|
1060
|
+
this._listen(searchInput, "input", (event) => this._handleFacetSearch(event));
|
|
1061
|
+
this._listen(searchInput, "keydown", (event) => this._handleFacetSearchKeydown(event));
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
const resetButton = this._element.querySelector("[data-datatable-reset]");
|
|
1065
|
+
this._listen(resetButton, "click", () => this._handleReset());
|
|
1066
|
+
|
|
1067
|
+
this._element.querySelectorAll("[data-datatable-select-all]").forEach((selectAll) => {
|
|
1068
|
+
this._listen(selectAll, "change", (event) => this._handleSelectAll(event));
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const pageSizeSelect = this._element.querySelector("[data-datatable-page-size-select]");
|
|
1072
|
+
this._listen(pageSizeSelect, "change", (event) => this._setPageSize(event.target.value));
|
|
1073
|
+
|
|
1074
|
+
this._element.querySelectorAll("[data-datatable-page-size-option]").forEach((option) => {
|
|
1075
|
+
this._listen(option, "click", () => {
|
|
1076
|
+
this._setPageSize(option.dataset.datatablePageSizeOption);
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|