@wpmoo/ui 1.0.0-rc.1 → 1.0.0-rc.3
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/ASSET_LICENSE.md +2 -2
- package/README.md +32 -16
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/certification.json +8 -3
- package/dist/assets/css/moo-ui.css +10095 -8896
- package/dist/assets/css/moo-ui.min.css +5 -2
- package/dist/assets/css/moo.css +1425 -382
- package/dist/assets/css/moo.min.css +5 -2
- package/dist/js/chart.js +15258 -0
- package/dist/js/chart.min.js +27 -0
- package/dist/js/combobox.js +69 -31
- package/dist/js/context-menu.js +22 -1
- package/dist/js/datatable.js +198 -24
- package/dist/js/datepicker.js +1154 -0
- package/dist/js/datepicker.min.js +6 -0
- package/dist/js/sidebar.js +67 -25
- package/dist/js/slider.js +324 -0
- package/package.json +19 -7
- package/scss/_config.scss +24 -0
- package/scss/_facade-settings.scss +0 -51
- package/scss/settings/_facade_public.scss +0 -23
package/dist/js/combobox.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Moo UI combobox.js v1.0.0-rc.3 (https://ui.wpmoo.org/)
|
|
3
|
+
* Copyright 2026 WPMoo (https://wpmoo.org)
|
|
4
|
+
* Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
|
|
5
|
+
*/
|
|
1
6
|
const instances = new WeakMap();
|
|
2
7
|
const normalize = (value) => value.trim().toLowerCase();
|
|
3
8
|
|
|
@@ -30,36 +35,36 @@ export default class Combobox {
|
|
|
30
35
|
throw new TypeError("Combobox requires an input, menu, and at least one option.");
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
this._isMultiple = element.dataset.
|
|
38
|
+
this._isMultiple = element.dataset.comboboxMultiple === "true";
|
|
34
39
|
this._hidden = this._isMultiple ? null : element.querySelector('input[type="hidden"]');
|
|
35
40
|
this._chipValue = element.querySelector(".combobox-value");
|
|
36
|
-
this._valueStore = element.querySelector("[data-
|
|
37
|
-
this._chipIconTemplate = element.querySelector("[data-
|
|
38
|
-
this._clearTrigger = element.querySelector("[data-
|
|
41
|
+
this._valueStore = element.querySelector("[data-combobox-value-store]");
|
|
42
|
+
this._chipIconTemplate = element.querySelector("[data-combobox-chip-icon]");
|
|
43
|
+
this._clearTrigger = element.querySelector("[data-combobox-clear]");
|
|
39
44
|
this._indicator = element.querySelector(".combobox-indicator");
|
|
40
|
-
this._groups = Array.from(element.querySelectorAll("[data-
|
|
41
|
-
this._separators = Array.from(element.querySelectorAll("[data-
|
|
45
|
+
this._groups = Array.from(element.querySelectorAll("[data-combobox-group]"));
|
|
46
|
+
this._separators = Array.from(element.querySelectorAll("[data-combobox-separator]"));
|
|
42
47
|
this._listeners = [];
|
|
43
48
|
this._generated = [];
|
|
44
49
|
this._startsOpen = this._menu.classList.contains("show");
|
|
45
50
|
|
|
46
|
-
this._empty = this._menu.querySelector("[data-
|
|
51
|
+
this._empty = this._menu.querySelector("[data-combobox-empty]");
|
|
47
52
|
if (!this._empty) {
|
|
48
53
|
this._empty = this._document.createElement("li");
|
|
49
54
|
this._empty.className = "combobox-empty";
|
|
50
55
|
this._empty.hidden = true;
|
|
51
|
-
this._empty.dataset.
|
|
56
|
+
this._empty.dataset.comboboxEmpty = "true";
|
|
52
57
|
this._empty.textContent = this._config.noResultsText;
|
|
53
58
|
this._menu.appendChild(this._empty);
|
|
54
59
|
this._generated.push(this._empty);
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
this._liveRegion = element.querySelector("[data-
|
|
62
|
+
this._liveRegion = element.querySelector("[data-combobox-live]");
|
|
58
63
|
if (!this._liveRegion) {
|
|
59
64
|
this._liveRegion = this._document.createElement("span");
|
|
60
65
|
this._liveRegion.className = "visually-hidden";
|
|
61
66
|
this._liveRegion.setAttribute("aria-live", "polite");
|
|
62
|
-
this._liveRegion.dataset.
|
|
67
|
+
this._liveRegion.dataset.comboboxLive = "true";
|
|
63
68
|
element.appendChild(this._liveRegion);
|
|
64
69
|
this._generated.push(this._liveRegion);
|
|
65
70
|
}
|
|
@@ -67,8 +72,8 @@ export default class Combobox {
|
|
|
67
72
|
instances.set(element, this);
|
|
68
73
|
this._bindEvents();
|
|
69
74
|
this._toggleClear(
|
|
70
|
-
this._input.dataset.
|
|
71
|
-
element.dataset.
|
|
75
|
+
this._input.dataset.comboboxSelected === "true" ||
|
|
76
|
+
element.dataset.comboboxSelected === "true"
|
|
72
77
|
);
|
|
73
78
|
this._syncMultipleValue();
|
|
74
79
|
this._filterOptions({ open: this._startsOpen, activate: this._startsOpen });
|
|
@@ -85,8 +90,8 @@ export default class Combobox {
|
|
|
85
90
|
this._closeMenu(false);
|
|
86
91
|
this._generated.forEach((element) => element.remove());
|
|
87
92
|
this._generated = [];
|
|
88
|
-
this._chipValue?.querySelectorAll("[data-
|
|
89
|
-
this._valueStore?.querySelectorAll("[data-
|
|
93
|
+
this._chipValue?.querySelectorAll("[data-combobox-generated]").forEach((item) => item.remove());
|
|
94
|
+
this._valueStore?.querySelectorAll("[data-combobox-generated]").forEach((item) => item.remove());
|
|
90
95
|
instances.delete(this._element);
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -108,7 +113,7 @@ export default class Combobox {
|
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
_optionGroupIsHidden(option) {
|
|
111
|
-
return option.closest("[data-
|
|
116
|
+
return option.closest("[data-combobox-group]")?.hidden || false;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
_visibleOptions() {
|
|
@@ -173,8 +178,8 @@ export default class Combobox {
|
|
|
173
178
|
|
|
174
179
|
_toggleClear(selected) {
|
|
175
180
|
const value = selected ? "true" : "false";
|
|
176
|
-
this._element.dataset.
|
|
177
|
-
this._input.dataset.
|
|
181
|
+
this._element.dataset.comboboxSelected = value;
|
|
182
|
+
this._input.dataset.comboboxSelected = value;
|
|
178
183
|
if (this._clearTrigger) {
|
|
179
184
|
this._clearTrigger.hidden = !selected;
|
|
180
185
|
if (this._indicator) {
|
|
@@ -189,7 +194,7 @@ export default class Combobox {
|
|
|
189
194
|
const chip = this._document.createElement("span");
|
|
190
195
|
chip.className = "badge text-bg-secondary combobox-chip";
|
|
191
196
|
chip.dataset.value = value;
|
|
192
|
-
chip.dataset.
|
|
197
|
+
chip.dataset.comboboxGenerated = "true";
|
|
193
198
|
const labelElement = this._document.createElement("span");
|
|
194
199
|
labelElement.className = "combobox-chip__label";
|
|
195
200
|
labelElement.textContent = label;
|
|
@@ -198,7 +203,7 @@ export default class Combobox {
|
|
|
198
203
|
remove.className = "combobox-chip__remove";
|
|
199
204
|
remove.type = "button";
|
|
200
205
|
remove.dataset.value = value;
|
|
201
|
-
remove.dataset.
|
|
206
|
+
remove.dataset.comboboxChipRemove = "true";
|
|
202
207
|
remove.setAttribute("aria-label", `Remove ${label}`);
|
|
203
208
|
remove.append(this._chipIconTemplate?.content.cloneNode(true) || "x");
|
|
204
209
|
chip.appendChild(remove);
|
|
@@ -215,13 +220,13 @@ export default class Combobox {
|
|
|
215
220
|
}
|
|
216
221
|
if (this._valueStore) {
|
|
217
222
|
this._valueStore.replaceChildren();
|
|
218
|
-
const name = this._valueStore.dataset.
|
|
223
|
+
const name = this._valueStore.dataset.comboboxName || "";
|
|
219
224
|
selected.forEach((option) => {
|
|
220
225
|
const field = this._document.createElement("input");
|
|
221
226
|
field.type = "hidden";
|
|
222
227
|
field.name = name;
|
|
223
228
|
field.value = option.dataset.value || "";
|
|
224
|
-
field.dataset.
|
|
229
|
+
field.dataset.comboboxGenerated = "true";
|
|
225
230
|
this._valueStore.appendChild(field);
|
|
226
231
|
});
|
|
227
232
|
}
|
|
@@ -279,8 +284,26 @@ export default class Combobox {
|
|
|
279
284
|
this._input.value = "";
|
|
280
285
|
}
|
|
281
286
|
|
|
287
|
+
_activeOption() {
|
|
288
|
+
const activeId = this._input.getAttribute("aria-activedescendant");
|
|
289
|
+
if (!activeId) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return this._visibleOptions().find((option) => option.id === activeId) || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_activateFirstOptionIfNeeded() {
|
|
296
|
+
if (!this._activeOption()) {
|
|
297
|
+
this._setActiveOption(this._visibleOptions()[0] || null);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
282
301
|
_setActiveOption(option) {
|
|
283
|
-
this._options.forEach((candidate) =>
|
|
302
|
+
this._options.forEach((candidate) => {
|
|
303
|
+
const active = candidate === option;
|
|
304
|
+
candidate.toggleAttribute("aria-current", active);
|
|
305
|
+
candidate.classList.toggle("active", active);
|
|
306
|
+
});
|
|
284
307
|
if (!option) {
|
|
285
308
|
this._input.removeAttribute("aria-activedescendant");
|
|
286
309
|
return;
|
|
@@ -338,17 +361,17 @@ export default class Combobox {
|
|
|
338
361
|
this._empty.hidden = count !== 0;
|
|
339
362
|
this._liveRegion.textContent = count === 0 ? "No results" : `${count} result${count === 1 ? "" : "s"}`;
|
|
340
363
|
if (activate) {
|
|
341
|
-
this.
|
|
364
|
+
this._activateFirstOptionIfNeeded();
|
|
342
365
|
}
|
|
343
366
|
}
|
|
344
367
|
|
|
345
368
|
_bindEvents() {
|
|
346
369
|
this._listen(this._input, "focus", () => {
|
|
347
370
|
this._openMenu();
|
|
348
|
-
if (!this._isMultiple && this._input.dataset.
|
|
371
|
+
if (!this._isMultiple && this._input.dataset.comboboxSelected === "true") {
|
|
349
372
|
this._input.select();
|
|
350
373
|
}
|
|
351
|
-
this.
|
|
374
|
+
this._activateFirstOptionIfNeeded();
|
|
352
375
|
});
|
|
353
376
|
this._listen(this._input, "input", () => {
|
|
354
377
|
if (!this._isMultiple) {
|
|
@@ -356,10 +379,20 @@ export default class Combobox {
|
|
|
356
379
|
}
|
|
357
380
|
this._filterOptions();
|
|
358
381
|
});
|
|
359
|
-
this._listen(this._input, "blur", () =>
|
|
360
|
-
|
|
382
|
+
this._listen(this._input, "blur", (event) => {
|
|
383
|
+
const nextTarget = event.relatedTarget;
|
|
384
|
+
if (nextTarget instanceof this._window.Node && this._element.contains(nextTarget)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this._clearStaleSelection();
|
|
388
|
+
});
|
|
389
|
+
this._listen(this._input, "click", () => {
|
|
390
|
+
this._openMenu();
|
|
391
|
+
this._setActiveOption(this._visibleOptions()[0] || null);
|
|
392
|
+
});
|
|
361
393
|
this._listen(this._input, "keydown", (event) => this._handleKeydown(event));
|
|
362
394
|
this._options.forEach((option) => {
|
|
395
|
+
this._listen(option, "keydown", (event) => this._handleKeydown(event));
|
|
363
396
|
this._listen(option, "click", () => {
|
|
364
397
|
this._chooseOption(option);
|
|
365
398
|
if (this._isMultiple) {
|
|
@@ -387,7 +420,7 @@ export default class Combobox {
|
|
|
387
420
|
_handleRootClick(event) {
|
|
388
421
|
const target = event.target;
|
|
389
422
|
const trigger = target instanceof this._window.Element
|
|
390
|
-
? target.closest("[data-
|
|
423
|
+
? target.closest("[data-combobox-chip-remove]")
|
|
391
424
|
: null;
|
|
392
425
|
if (trigger) {
|
|
393
426
|
event.preventDefault();
|
|
@@ -407,6 +440,8 @@ export default class Combobox {
|
|
|
407
440
|
const current = available.findIndex(
|
|
408
441
|
(option) => option.id === this._input.getAttribute("aria-activedescendant")
|
|
409
442
|
);
|
|
443
|
+
const isNextKey = event.key === "ArrowDown" || event.key === "Down";
|
|
444
|
+
const isPreviousKey = event.key === "ArrowUp" || event.key === "Up";
|
|
410
445
|
if (this._isMultiple && event.key === "Backspace" && this._input.value === "") {
|
|
411
446
|
const selected = this._selectedOptions();
|
|
412
447
|
const last = selected[selected.length - 1];
|
|
@@ -414,12 +449,15 @@ export default class Combobox {
|
|
|
414
449
|
event.preventDefault();
|
|
415
450
|
this._removeChip(last.dataset.value || "");
|
|
416
451
|
}
|
|
417
|
-
} else if ((
|
|
452
|
+
} else if ((isNextKey || isPreviousKey) && available.length) {
|
|
418
453
|
event.preventDefault();
|
|
419
454
|
this._openMenu();
|
|
420
|
-
const offset =
|
|
455
|
+
const offset = isNextKey ? 1 : -1;
|
|
421
456
|
const next = current === -1 ? 0 : (current + offset + available.length) % available.length;
|
|
422
457
|
this._setActiveOption(available[next]);
|
|
458
|
+
if (event.target !== this._input) {
|
|
459
|
+
this._input.focus({ preventScroll: true });
|
|
460
|
+
}
|
|
423
461
|
} else if (event.key === "Enter") {
|
|
424
462
|
const option = available[current];
|
|
425
463
|
if (option) {
|
|
@@ -431,7 +469,7 @@ export default class Combobox {
|
|
|
431
469
|
}
|
|
432
470
|
} else if (event.key === "Escape") {
|
|
433
471
|
this._closeMenu();
|
|
434
|
-
this._input.
|
|
472
|
+
this._input.focus();
|
|
435
473
|
} else if (event.key === "Tab") {
|
|
436
474
|
this._closeMenu();
|
|
437
475
|
}
|
package/dist/js/context-menu.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Moo UI context-menu.js v1.0.0-rc.3 (https://ui.wpmoo.org/)
|
|
3
|
+
* Copyright 2026 WPMoo (https://wpmoo.org)
|
|
4
|
+
* Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
|
|
5
|
+
*/
|
|
1
6
|
const instances = new WeakMap();
|
|
2
7
|
|
|
3
8
|
function pointerRect(event) {
|
|
@@ -174,6 +179,19 @@ export default class ContextMenu {
|
|
|
174
179
|
this._openMethod = "pointer";
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
_handleFallbackKeydown(event) {
|
|
183
|
+
if (event.key !== " " && event.key !== "Spacebar") {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
if (this._menu.classList.contains("show")) {
|
|
189
|
+
this._dropdown.hide();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this._open(this._fallback, null, "fallback-keyboard");
|
|
193
|
+
}
|
|
194
|
+
|
|
177
195
|
_handleMenuClick(event) {
|
|
178
196
|
const item = menuItemFromEvent(event);
|
|
179
197
|
if (!item) {
|
|
@@ -217,6 +235,7 @@ export default class ContextMenu {
|
|
|
217
235
|
this._listen(this._element, "contextmenu", (event) => this._handleContextMenu(event));
|
|
218
236
|
this._listen(this._element, "keydown", (event) => this._handleKeydown(event));
|
|
219
237
|
this._listen(this._fallback, "click", () => this._handleFallbackClick());
|
|
238
|
+
this._listen(this._fallback, "keydown", (event) => this._handleFallbackKeydown(event));
|
|
220
239
|
this._listen(this._menu, "click", (event) => this._handleMenuClick(event));
|
|
221
240
|
this._listen(this._fallback, "show.bs.dropdown", (event) => {
|
|
222
241
|
if (!this._trigger("show", true)) {
|
|
@@ -231,7 +250,9 @@ export default class ContextMenu {
|
|
|
231
250
|
// highlight instead of a focus ring competing with it. A keyboard-
|
|
232
251
|
// driven open (Shift+F10/ContextMenu key) still lands on the first
|
|
233
252
|
// item, matching that same native arrow-key-open convention.
|
|
234
|
-
if (this._openMethod === "keyboard") {
|
|
253
|
+
if (this._openMethod === "fallback-keyboard") {
|
|
254
|
+
this._fallback.focus();
|
|
255
|
+
} else if (this._openMethod === "keyboard") {
|
|
235
256
|
this._menuItems()[0]?.focus();
|
|
236
257
|
} else {
|
|
237
258
|
this._menu.focus();
|
package/dist/js/datatable.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Moo UI datatable.js v1.0.0-rc.3 (https://ui.wpmoo.org/)
|
|
3
|
+
* Copyright 2026 WPMoo (https://wpmoo.org)
|
|
4
|
+
* Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
|
|
5
|
+
*/
|
|
1
6
|
const instances = new WeakMap();
|
|
2
7
|
|
|
3
8
|
function normalize(value) {
|
|
@@ -47,6 +52,8 @@ export default class DataTable {
|
|
|
47
52
|
this._searchTerm = "";
|
|
48
53
|
this._tooltips = [];
|
|
49
54
|
this._filterMode = element.dataset.datatableFilterMode || "inline";
|
|
55
|
+
this._reparentedRowMenus = new Map();
|
|
56
|
+
this._reparentedRowMenuByTrigger = new WeakMap();
|
|
50
57
|
|
|
51
58
|
this._rows = Array.from(this._tbody.querySelectorAll(":scope > tr[data-datatable-row]")).map((tr, index) => ({
|
|
52
59
|
element: tr,
|
|
@@ -63,9 +70,12 @@ export default class DataTable {
|
|
|
63
70
|
this._render();
|
|
64
71
|
this._initBulkTooltips();
|
|
65
72
|
this._initViewToggle();
|
|
73
|
+
this._initRowActionDropdowns();
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
dispose() {
|
|
77
|
+
this._disposeRowActionDropdowns();
|
|
78
|
+
this._restoreRowActionMenus();
|
|
69
79
|
this._listeners.forEach(({ target, type, handler, options }) => {
|
|
70
80
|
target.removeEventListener(type, handler, options);
|
|
71
81
|
});
|
|
@@ -87,6 +97,141 @@ export default class DataTable {
|
|
|
87
97
|
this._listeners.push({ target, type, handler, options });
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
// Row menus are authored next to their trigger so the public HTML stays
|
|
101
|
+
// inspectable, but when opened they are temporarily moved under body.
|
|
102
|
+
// That keeps the menu out of the scroll wrapper's layout math, so Safari
|
|
103
|
+
// cannot clip it at the rounded frame edge or shift the visible table
|
|
104
|
+
// slice while Popper positions it against the viewport.
|
|
105
|
+
// Table and card views both wrap row actions in .table-row-actions, but
|
|
106
|
+
// .table-responsive only contains the table view; card triggers must be
|
|
107
|
+
// selected separately so both views get the same fixed Popper config.
|
|
108
|
+
_rowActionTriggers() {
|
|
109
|
+
return this._element.querySelectorAll(
|
|
110
|
+
".table-responsive .table-row-actions [data-bs-toggle=\"dropdown\"], [data-datatable-card] .table-row-actions [data-bs-toggle=\"dropdown\"]"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_initRowActionDropdowns() {
|
|
115
|
+
const Dropdown = this._bootstrap("Dropdown");
|
|
116
|
+
if (!Dropdown) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this._rowActionTriggers().forEach((trigger) => {
|
|
120
|
+
Dropdown.getOrCreateInstance(trigger, {
|
|
121
|
+
popperConfig: (defaultConfig) => ({
|
|
122
|
+
...defaultConfig,
|
|
123
|
+
strategy: "fixed",
|
|
124
|
+
modifiers: [
|
|
125
|
+
...(defaultConfig.modifiers || []),
|
|
126
|
+
{
|
|
127
|
+
name: "flip",
|
|
128
|
+
options: {
|
|
129
|
+
fallbackPlacements: ["top-end", "top-start", "bottom-end", "bottom-start"],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "preventOverflow",
|
|
134
|
+
options: {
|
|
135
|
+
boundary: "viewport",
|
|
136
|
+
padding: 8,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// dispose() alone leaves any open row-action Dropdown's show/aria-expanded/
|
|
146
|
+
// data-bs-popper state and Bootstrap instance behind; hide and dispose each
|
|
147
|
+
// one first so a subsequent re-init starts from a clean state.
|
|
148
|
+
_disposeRowActionDropdowns() {
|
|
149
|
+
const Dropdown = this._bootstrap("Dropdown");
|
|
150
|
+
if (!Dropdown) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this._rowActionTriggers().forEach((trigger) => {
|
|
154
|
+
const instance = Dropdown.getInstance(trigger);
|
|
155
|
+
if (!instance) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
instance.hide();
|
|
159
|
+
instance.dispose();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_rowActionMenuForTrigger(trigger) {
|
|
164
|
+
if (!trigger?.closest?.(".table-row-actions")) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return trigger.closest(".dropdown")?.querySelector(":scope > .dropdown-menu") || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_rowActionOwnerIdForTrigger(trigger) {
|
|
171
|
+
const rowId = trigger?.closest?.("tr[data-datatable-row]")?.id;
|
|
172
|
+
if (rowId) {
|
|
173
|
+
return rowId;
|
|
174
|
+
}
|
|
175
|
+
return trigger?.closest?.("[data-datatable-card]")?.getAttribute("data-datatable-card-for") || "";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_reparentRowActionMenu(trigger) {
|
|
179
|
+
const menu = this._rowActionMenuForTrigger(trigger);
|
|
180
|
+
if (!menu || this._reparentedRowMenus.has(menu)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const ownerId = this._rowActionOwnerIdForTrigger(trigger);
|
|
184
|
+
if (ownerId) {
|
|
185
|
+
menu.setAttribute("data-datatable-row-action-owner", ownerId);
|
|
186
|
+
}
|
|
187
|
+
if (trigger.id) {
|
|
188
|
+
menu.setAttribute("data-datatable-row-action-trigger", trigger.id);
|
|
189
|
+
}
|
|
190
|
+
this._reparentedRowMenus.set(menu, {
|
|
191
|
+
parent: menu.parentNode,
|
|
192
|
+
nextSibling: menu.nextSibling,
|
|
193
|
+
trigger,
|
|
194
|
+
});
|
|
195
|
+
this._reparentedRowMenuByTrigger.set(trigger, menu);
|
|
196
|
+
this._document.body.appendChild(menu);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_restoreRowActionMenuForTrigger(trigger) {
|
|
200
|
+
const menu = this._reparentedRowMenuByTrigger.get(trigger) || this._rowActionMenuForTrigger(trigger);
|
|
201
|
+
if (menu) {
|
|
202
|
+
this._restoreRowActionMenu(menu);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_restoreRowActionMenu(menu) {
|
|
207
|
+
const original = this._reparentedRowMenus.get(menu);
|
|
208
|
+
if (!original) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const { parent, nextSibling, trigger } = original;
|
|
212
|
+
if (parent?.isConnected) {
|
|
213
|
+
if (nextSibling?.parentNode === parent) {
|
|
214
|
+
parent.insertBefore(menu, nextSibling);
|
|
215
|
+
} else {
|
|
216
|
+
parent.appendChild(menu);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
menu.remove();
|
|
220
|
+
}
|
|
221
|
+
if (trigger) {
|
|
222
|
+
this._reparentedRowMenuByTrigger.delete(trigger);
|
|
223
|
+
}
|
|
224
|
+
menu.removeAttribute("data-datatable-row-action-owner");
|
|
225
|
+
menu.removeAttribute("data-datatable-row-action-trigger");
|
|
226
|
+
this._reparentedRowMenus.delete(menu);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_restoreRowActionMenus() {
|
|
230
|
+
Array.from(this._reparentedRowMenus.keys()).forEach((menu) => {
|
|
231
|
+
this._restoreRowActionMenu(menu);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
90
235
|
_bootstrap(name) {
|
|
91
236
|
return this._window.bootstrap?.[name] || null;
|
|
92
237
|
}
|
|
@@ -504,35 +649,62 @@ export default class DataTable {
|
|
|
504
649
|
if (!template) {
|
|
505
650
|
return;
|
|
506
651
|
}
|
|
507
|
-
template.parentNode
|
|
508
|
-
.querySelectorAll("[data-datatable-page-number], [data-datatable-page-ellipsis]")
|
|
509
|
-
.forEach((node) => node.remove());
|
|
510
652
|
|
|
511
653
|
const pages = this._pageWindow(this._currentPage, pageCount);
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
654
|
+
const existing = Array.from(
|
|
655
|
+
template.parentNode.querySelectorAll(
|
|
656
|
+
"[data-datatable-page-number], [data-datatable-page-ellipsis]"
|
|
657
|
+
)
|
|
658
|
+
);
|
|
659
|
+
const existingWindow = existing.map((node) =>
|
|
660
|
+
node.dataset.datatablePageEllipsis === undefined
|
|
661
|
+
? Number(node.dataset.datatablePageNumber)
|
|
662
|
+
: "…"
|
|
663
|
+
);
|
|
664
|
+
// Same node-reuse rationale as the row-reorder skip above: rebuilding
|
|
665
|
+
// every page-number button on each click destroys and recreates the one
|
|
666
|
+
// the user just clicked, which blurs it (losing the focus ring and any
|
|
667
|
+
// keyboard position). The page window is unchanged on most clicks (any
|
|
668
|
+
// dataset small enough to show every page, or a prev/next inside an
|
|
669
|
+
// already-visible window), so only rebuild when the window itself
|
|
670
|
+
// differs; otherwise just resync which node is active below.
|
|
671
|
+
const windowChanged =
|
|
672
|
+
existingWindow.length !== pages.length ||
|
|
673
|
+
existingWindow.some((page, index) => page !== pages[index]);
|
|
674
|
+
|
|
675
|
+
if (windowChanged) {
|
|
676
|
+
existing.forEach((node) => node.remove());
|
|
677
|
+
const fragment = this._document.createDocumentFragment();
|
|
678
|
+
pages.forEach((page) => {
|
|
679
|
+
const li = this._document.createElement("li");
|
|
680
|
+
li.className = "page-item";
|
|
681
|
+
if (page === "…") {
|
|
682
|
+
li.dataset.datatablePageEllipsis = "";
|
|
683
|
+
li.innerHTML = '<span class="page-link">…</span>';
|
|
684
|
+
} else {
|
|
685
|
+
li.dataset.datatablePageNumber = String(page);
|
|
686
|
+
const button = this._document.createElement("button");
|
|
687
|
+
button.type = "button";
|
|
688
|
+
button.className = "page-link";
|
|
689
|
+
button.textContent = String(page);
|
|
690
|
+
button.setAttribute("aria-label", `Go to page ${page}`);
|
|
691
|
+
button.dataset.datatablePageGo = String(page);
|
|
692
|
+
li.appendChild(button);
|
|
524
693
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
694
|
+
fragment.appendChild(li);
|
|
695
|
+
});
|
|
696
|
+
template.parentNode.insertBefore(fragment, template);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
template.parentNode.querySelectorAll("[data-datatable-page-number]").forEach((li) => {
|
|
700
|
+
const isActive = Number(li.dataset.datatablePageNumber) === this._currentPage;
|
|
701
|
+
li.classList.toggle("active", isActive);
|
|
702
|
+
if (isActive) {
|
|
703
|
+
li.setAttribute("aria-current", "page");
|
|
704
|
+
} else {
|
|
705
|
+
li.removeAttribute("aria-current");
|
|
532
706
|
}
|
|
533
|
-
fragment.appendChild(li);
|
|
534
707
|
});
|
|
535
|
-
template.parentNode.insertBefore(fragment, template);
|
|
536
708
|
}
|
|
537
709
|
|
|
538
710
|
_renderPageSize() {
|
|
@@ -1040,11 +1212,13 @@ export default class DataTable {
|
|
|
1040
1212
|
// no-op for as long as the dropdown stays open, however it gets
|
|
1041
1213
|
// re-triggered.
|
|
1042
1214
|
this._listen(this._element, "show.bs.dropdown", (event) => {
|
|
1215
|
+
this._reparentRowActionMenu(event.target);
|
|
1043
1216
|
const tooltip = this._bootstrap("Tooltip")?.getInstance(event.target);
|
|
1044
1217
|
tooltip?.hide();
|
|
1045
1218
|
tooltip?.disable();
|
|
1046
1219
|
});
|
|
1047
1220
|
this._listen(this._element, "hidden.bs.dropdown", (event) => {
|
|
1221
|
+
this._restoreRowActionMenuForTrigger(event.target);
|
|
1048
1222
|
this._bootstrap("Tooltip")?.getInstance(event.target)?.enable();
|
|
1049
1223
|
if (event.target.matches("[data-datatable-filter-menu-trigger]")) {
|
|
1050
1224
|
this._showFilterPickerPanel();
|