@wpmoo/ui 0.9.0 → 1.0.0-rc.10

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.
Files changed (99) hide show
  1. package/ASSET_LICENSE.md +4 -2
  2. package/LICENSE +28 -0
  3. package/README.md +105 -100
  4. package/certification.json +25 -3
  5. package/dist/assets/css/moo-ui.css +14005 -12457
  6. package/dist/assets/css/moo-ui.min.css +10 -28
  7. package/dist/assets/css/moo.css +2606 -902
  8. package/dist/assets/css/moo.min.css +10 -29
  9. package/dist/js/chart.js +15312 -0
  10. package/dist/js/chart.min.js +27 -0
  11. package/dist/js/combobox.js +129 -85
  12. package/dist/js/context-menu.js +35 -66
  13. package/dist/js/datatable.js +390 -188
  14. package/dist/js/datepicker.js +1197 -0
  15. package/dist/js/datepicker.min.js +7 -0
  16. package/dist/js/moo-ui.js +3947 -0
  17. package/dist/js/moo-ui.min.js +7 -0
  18. package/dist/js/sheet.js +46 -0
  19. package/dist/js/sidebar.js +190 -172
  20. package/dist/js/slider.js +289 -0
  21. package/dist/js/state.js +76 -0
  22. package/dist/release-manifest.json +24 -0
  23. package/package.json +39 -9
  24. package/scss/_components.scss +70 -0
  25. package/scss/_config.scss +18 -0
  26. package/scss/_settings.scss +5 -0
  27. package/scss/components/_accordion.scss +54 -0
  28. package/scss/components/_alert.scss +82 -0
  29. package/scss/components/_avatar.scss +139 -0
  30. package/scss/components/_badge.scss +34 -0
  31. package/scss/components/_breadcrumb.scss +119 -0
  32. package/scss/components/_button.scss +200 -0
  33. package/scss/components/_button_group.scss +57 -0
  34. package/scss/components/_card.scss +114 -0
  35. package/scss/components/_checkbox.scss +78 -0
  36. package/scss/components/_close_button.scss +56 -0
  37. package/scss/components/_collapsible.scss +44 -0
  38. package/scss/components/_combobox.scss +253 -0
  39. package/scss/components/_context-menu.scss +18 -0
  40. package/scss/components/_datatable.scss +1067 -0
  41. package/scss/components/_datepicker.scss +237 -0
  42. package/scss/components/_dialog.scss +69 -0
  43. package/scss/components/_dropdown.scss +201 -0
  44. package/scss/components/_field.scss +94 -0
  45. package/scss/components/_input.scss +31 -0
  46. package/scss/components/_input_group.scss +151 -0
  47. package/scss/components/_kbd.scss +11 -0
  48. package/scss/components/_menubar.scss +17 -0
  49. package/scss/components/_navigation.scss +36 -0
  50. package/scss/components/_pagination.scss +58 -0
  51. package/scss/components/_popover.scss +26 -0
  52. package/scss/components/_progress.scss +5 -0
  53. package/scss/components/_radio_group.scss +108 -0
  54. package/scss/components/_search_trigger.scss +61 -0
  55. package/scss/components/_select.scss +8 -0
  56. package/scss/components/_separator.scss +10 -0
  57. package/scss/components/_sheet.scss +16 -0
  58. package/scss/components/_sidebar.scss +5 -0
  59. package/scss/components/_skeleton.scss +10 -0
  60. package/scss/components/_slider.scss +213 -0
  61. package/scss/components/_spinner.scss +11 -0
  62. package/scss/components/_switch.scss +30 -0
  63. package/scss/components/_table.scss +12 -0
  64. package/scss/components/_tabs.scss +116 -0
  65. package/scss/components/_textarea.scss +8 -0
  66. package/scss/components/_toast.scss +116 -0
  67. package/scss/components/_toggle_group.scss +17 -0
  68. package/scss/components/_tooltip.scss +64 -0
  69. package/scss/components/sidebar/_base.scss +86 -0
  70. package/scss/components/sidebar/_collapsed.scss +112 -0
  71. package/scss/components/sidebar/_identity.scss +71 -0
  72. package/scss/components/sidebar/_menus.scss +219 -0
  73. package/scss/foundations/_backdrop.scss +102 -0
  74. package/scss/foundations/_focus.scss +83 -0
  75. package/scss/foundations/_globals.scss +5 -0
  76. package/scss/foundations/_scope.scss +26 -0
  77. package/scss/layouts/_app.scss +75 -0
  78. package/scss/layouts/_page_grid.scss +27 -0
  79. package/scss/layouts/app/sidebar/_base.scss +84 -0
  80. package/scss/layouts/app/sidebar/_default.scss +20 -0
  81. package/scss/layouts/app/sidebar/_floating.scss +36 -0
  82. package/scss/layouts/app/sidebar/_inset.scss +32 -0
  83. package/scss/mixins/_banner.scss +9 -0
  84. package/scss/moo-core.scss +16 -0
  85. package/scss/moo-ui.scss +54 -0
  86. package/scss/settings/_bootstrap_overrides.scss +7 -0
  87. package/scss/settings/_component_variables.scss +244 -0
  88. package/scss/settings/_forms.scss +28 -0
  89. package/scss/settings/_options.scss +2 -0
  90. package/scss/settings/_palette.scss +90 -0
  91. package/scss/themes/_forms.scss +15 -0
  92. package/scss/themes/_root.scss +20 -0
  93. package/scss/themes/_standalone.scss +3 -0
  94. package/scss/themes/_theme.scss +244 -0
  95. package/scss/utilities/_background_color.scss +18 -0
  96. package/scss/utilities/_scroll_fade.scss +193 -0
  97. package/scss/utilities/_scroll_fade_primitives.scss +43 -0
  98. package/scss/_facade-settings.scss +0 -51
  99. package/scss/settings/_facade_public.scss +0 -23
@@ -1,18 +1,61 @@
1
- const instances = new WeakMap();
1
+ /*!
2
+ * Moo UI datatable.js v1.0.0-rc.10 (https://wpmoo.org/)
3
+ * Copyright 2026 WPMoo Authors
4
+ * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
+ */
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+
10
+ // src/js/theme-owner.js
11
+ var OWNER_SELECTOR = '.moo-ui[data-bs-theme="light"], .moo-ui[data-bs-theme="dark"]';
12
+ var PORTAL_SELECTOR = "[data-moo-overlay-host]";
13
+ function documentFor(node) {
14
+ if (node?.nodeType === 9) return node;
15
+ if (node?.ownerDocument) return node.ownerDocument;
16
+ return typeof document === "undefined" ? null : document;
17
+ }
18
+ __name(documentFor, "documentFor");
19
+ function isResolvedOwner(node) {
20
+ return Boolean(node?.matches?.(OWNER_SELECTOR));
21
+ }
22
+ __name(isResolvedOwner, "isResolvedOwner");
23
+ function ownerRootFor(node) {
24
+ const ownerDocument = documentFor(node);
25
+ if (node?.nodeType === 9 || node === ownerDocument?.body) {
26
+ return ownerDocument?.body?.firstElementChild || null;
27
+ }
28
+ return node;
29
+ }
30
+ __name(ownerRootFor, "ownerRootFor");
31
+ function findThemeOwner(node = typeof document === "undefined" ? null : document) {
32
+ const start = ownerRootFor(node);
33
+ if (!start) return null;
34
+ if (isResolvedOwner(start)) return start;
35
+ return start.closest?.(OWNER_SELECTOR) || null;
36
+ }
37
+ __name(findThemeOwner, "findThemeOwner");
38
+ function ownerPortalRoot(owner) {
39
+ if (!owner) return null;
40
+ return Array.from(owner.children || []).find(
41
+ (child) => child.matches?.(PORTAL_SELECTOR)
42
+ ) || owner;
43
+ }
44
+ __name(ownerPortalRoot, "ownerPortalRoot");
2
45
 
46
+ // src/js/components/datatable.js
47
+ var instances = /* @__PURE__ */ new WeakMap();
3
48
  function normalize(value) {
4
49
  return (value ?? "").toString().trim().toLowerCase();
5
50
  }
6
-
7
- export default class DataTable {
51
+ __name(normalize, "normalize");
52
+ var _DataTable = class _DataTable {
8
53
  static getInstance(element) {
9
54
  return element?.nodeType === 1 ? instances.get(element) || null : null;
10
55
  }
11
-
12
56
  static getOrCreateInstance(element, config = {}) {
13
- return DataTable.getInstance(element) || new DataTable(element, config);
57
+ return _DataTable.getInstance(element) || new _DataTable(element, config);
14
58
  }
15
-
16
59
  constructor(element, config = {}) {
17
60
  if (element?.nodeType !== 1 || !element.matches(".datatable")) {
18
61
  throw new TypeError("DataTable requires a .datatable root element.");
@@ -21,7 +64,6 @@ export default class DataTable {
21
64
  if (existing) {
22
65
  return existing;
23
66
  }
24
-
25
67
  this._element = element;
26
68
  this._document = element.ownerDocument;
27
69
  this._window = this._document.defaultView;
@@ -34,38 +76,44 @@ export default class DataTable {
34
76
  const cardById = new Map(
35
77
  Array.from(element.querySelectorAll("[data-datatable-card]")).map((card) => [card.dataset.datatableCardFor, card])
36
78
  );
37
-
38
79
  this._config = { ...config };
39
80
  this._listeners = [];
40
81
  this._pageSize = Number(element.dataset.datatablePageSize) || 10;
41
82
  this._currentPage = 1;
42
83
  this._sortKey = null;
43
84
  this._sortDirection = null;
44
- this._selectedIds = new Set();
45
- this._facetSelections = new Map();
46
- this._visibleFacets = new Map();
85
+ this._selectedIds = /* @__PURE__ */ new Set();
86
+ this._facetSelections = /* @__PURE__ */ new Map();
87
+ this._visibleFacets = /* @__PURE__ */ new Map();
47
88
  this._searchTerm = "";
48
89
  this._tooltips = [];
49
90
  this._filterMode = element.dataset.datatableFilterMode || "inline";
50
-
91
+ this._reparentedRowMenus = /* @__PURE__ */ new Map();
92
+ this._reparentedRowMenuByTrigger = /* @__PURE__ */ new WeakMap();
93
+ this._reparentedSortMenus = /* @__PURE__ */ new Map();
94
+ this._reparentedSortMenuByTrigger = /* @__PURE__ */ new WeakMap();
51
95
  this._rows = Array.from(this._tbody.querySelectorAll(":scope > tr[data-datatable-row]")).map((tr, index) => ({
52
96
  element: tr,
53
97
  cardElement: cardById.get(tr.id) || null,
54
98
  index,
55
99
  search: normalize(tr.dataset.datatableSearch),
56
- facets: this._readFacets(tr),
100
+ facets: this._readFacets(tr)
57
101
  }));
58
102
  this._readInitialFacetVisibility();
59
103
  this._readInitialSort();
60
-
61
104
  instances.set(element, this);
62
105
  this._bindEvents();
63
106
  this._render();
64
107
  this._initBulkTooltips();
65
108
  this._initViewToggle();
109
+ this._initRowActionDropdowns();
110
+ this._initSortDropdowns();
66
111
  }
67
-
68
112
  dispose() {
113
+ this._disposeRowActionDropdowns();
114
+ this._disposeSortDropdowns();
115
+ this._restoreRowActionMenus();
116
+ this._restoreSortMenus();
69
117
  this._listeners.forEach(({ target, type, handler, options }) => {
70
118
  target.removeEventListener(type, handler, options);
71
119
  });
@@ -74,11 +122,9 @@ export default class DataTable {
74
122
  this._tooltips = [];
75
123
  instances.delete(this._element);
76
124
  }
77
-
78
125
  getSelectedIds() {
79
126
  return Array.from(this._selectedIds);
80
127
  }
81
-
82
128
  _listen(target, type, handler, options) {
83
129
  if (!target) {
84
130
  return;
@@ -86,11 +132,231 @@ export default class DataTable {
86
132
  target.addEventListener(type, handler, options);
87
133
  this._listeners.push({ target, type, handler, options });
88
134
  }
89
-
135
+ // Row menus are authored next to their trigger so the public HTML stays
136
+ // inspectable, but when opened they are temporarily moved to their nearest
137
+ // Moo owner portal.
138
+ // That keeps the menu out of the scroll wrapper's layout math, so Safari
139
+ // cannot clip it at the rounded frame edge or shift the visible table
140
+ // slice while Popper positions it against the viewport.
141
+ // Public Dropdown compositions need the same behavior as the built-in
142
+ // row actions. Scope by row/card ownership so toolbar and sort menus keep
143
+ // their own lifecycle.
144
+ _rowActionTriggers() {
145
+ return this._element.querySelectorAll(
146
+ 'tr[data-datatable-row] .dropdown [data-bs-toggle="dropdown"], [data-datatable-card] .dropdown [data-bs-toggle="dropdown"]'
147
+ );
148
+ }
149
+ _initRowActionDropdowns() {
150
+ const Dropdown = this._bootstrap("Dropdown");
151
+ if (!Dropdown) {
152
+ return;
153
+ }
154
+ this._rowActionTriggers().forEach((trigger) => {
155
+ Dropdown.getOrCreateInstance(trigger, {
156
+ popperConfig: /* @__PURE__ */ __name((defaultConfig) => this._clippingSafePopperConfig(defaultConfig), "popperConfig")
157
+ });
158
+ });
159
+ }
160
+ _sortTriggers() {
161
+ return this._element.querySelectorAll(
162
+ '.datatable-sort [data-bs-toggle="dropdown"]'
163
+ );
164
+ }
165
+ _initSortDropdowns() {
166
+ const Dropdown = this._bootstrap("Dropdown");
167
+ if (!Dropdown) {
168
+ return;
169
+ }
170
+ this._sortTriggers().forEach((trigger) => {
171
+ Dropdown.getOrCreateInstance(trigger, {
172
+ popperConfig: /* @__PURE__ */ __name((defaultConfig) => this._clippingSafePopperConfig(defaultConfig), "popperConfig")
173
+ });
174
+ });
175
+ }
176
+ _clippingSafePopperConfig(defaultConfig) {
177
+ return {
178
+ ...defaultConfig,
179
+ strategy: "fixed",
180
+ modifiers: [
181
+ ...defaultConfig.modifiers || [],
182
+ {
183
+ name: "flip",
184
+ options: {
185
+ fallbackPlacements: ["top-end", "top-start", "bottom-end", "bottom-start"]
186
+ }
187
+ },
188
+ {
189
+ name: "preventOverflow",
190
+ options: {
191
+ boundary: "viewport",
192
+ padding: 8
193
+ }
194
+ }
195
+ ]
196
+ };
197
+ }
198
+ // dispose() alone leaves any open row-action Dropdown's show/aria-expanded/
199
+ // data-bs-popper state and Bootstrap instance behind; hide and dispose each
200
+ // one first so a subsequent re-init starts from a clean state.
201
+ _disposeRowActionDropdowns() {
202
+ const Dropdown = this._bootstrap("Dropdown");
203
+ if (!Dropdown) {
204
+ return;
205
+ }
206
+ this._rowActionTriggers().forEach((trigger) => {
207
+ const instance = Dropdown.getInstance(trigger);
208
+ if (!instance) {
209
+ return;
210
+ }
211
+ instance.hide();
212
+ instance.dispose();
213
+ });
214
+ }
215
+ _disposeSortDropdowns() {
216
+ const Dropdown = this._bootstrap("Dropdown");
217
+ if (!Dropdown) {
218
+ return;
219
+ }
220
+ this._sortTriggers().forEach((trigger) => {
221
+ const instance = Dropdown.getInstance(trigger);
222
+ if (!instance) {
223
+ return;
224
+ }
225
+ instance.hide();
226
+ instance.dispose();
227
+ });
228
+ }
229
+ _rowActionMenuForTrigger(trigger) {
230
+ if (!trigger?.closest?.("tr[data-datatable-row], [data-datatable-card]")) {
231
+ return null;
232
+ }
233
+ return trigger.closest(".dropdown")?.querySelector(":scope > .dropdown-menu") || null;
234
+ }
235
+ _rowActionOwnerIdForTrigger(trigger) {
236
+ const rowId = trigger?.closest?.("tr[data-datatable-row]")?.id;
237
+ if (rowId) {
238
+ return rowId;
239
+ }
240
+ return trigger?.closest?.("[data-datatable-card]")?.getAttribute("data-datatable-card-for") || "";
241
+ }
242
+ _reparentRowActionMenu(trigger) {
243
+ const menu = this._rowActionMenuForTrigger(trigger);
244
+ if (!menu || this._reparentedRowMenus.has(menu)) {
245
+ return;
246
+ }
247
+ const ownerId = this._rowActionOwnerIdForTrigger(trigger);
248
+ if (ownerId) {
249
+ menu.setAttribute("data-datatable-row-action-owner", ownerId);
250
+ }
251
+ if (trigger.id) {
252
+ menu.setAttribute("data-datatable-row-action-trigger", trigger.id);
253
+ }
254
+ this._reparentedRowMenus.set(menu, {
255
+ parent: menu.parentNode,
256
+ nextSibling: menu.nextSibling,
257
+ trigger,
258
+ owner: findThemeOwner(trigger)
259
+ });
260
+ this._reparentedRowMenuByTrigger.set(trigger, menu);
261
+ this._portalRoot(trigger).appendChild(menu);
262
+ }
263
+ _restoreRowActionMenuForTrigger(trigger) {
264
+ const menu = this._reparentedRowMenuByTrigger.get(trigger) || this._rowActionMenuForTrigger(trigger);
265
+ if (menu) {
266
+ this._restoreRowActionMenu(menu);
267
+ }
268
+ }
269
+ _restoreRowActionMenu(menu) {
270
+ const original = this._reparentedRowMenus.get(menu);
271
+ if (!original) {
272
+ return;
273
+ }
274
+ const { parent, nextSibling, trigger, owner } = original;
275
+ if (owner?.isConnected === false) {
276
+ menu.remove();
277
+ } else if (parent?.isConnected) {
278
+ if (nextSibling?.parentNode === parent) {
279
+ parent.insertBefore(menu, nextSibling);
280
+ } else {
281
+ parent.appendChild(menu);
282
+ }
283
+ } else {
284
+ menu.remove();
285
+ }
286
+ if (trigger) {
287
+ this._reparentedRowMenuByTrigger.delete(trigger);
288
+ }
289
+ menu.removeAttribute("data-datatable-row-action-owner");
290
+ menu.removeAttribute("data-datatable-row-action-trigger");
291
+ this._reparentedRowMenus.delete(menu);
292
+ }
293
+ _restoreRowActionMenus() {
294
+ Array.from(this._reparentedRowMenus.keys()).forEach((menu) => {
295
+ this._restoreRowActionMenu(menu);
296
+ });
297
+ }
298
+ _sortMenuForTrigger(trigger) {
299
+ if (!trigger?.closest?.(".datatable-sort")) {
300
+ return null;
301
+ }
302
+ return this._reparentedSortMenuByTrigger.get(trigger) || trigger?.closest?.(".dropdown")?.querySelector(":scope > .dropdown-menu") || null;
303
+ }
304
+ // Sort menus are authored inside their table header, but must leave the
305
+ // overflow frame while open just like row-action menus do.
306
+ _reparentSortMenu(trigger) {
307
+ const menu = this._sortMenuForTrigger(trigger);
308
+ if (!menu || this._reparentedSortMenus.has(menu)) {
309
+ return;
310
+ }
311
+ this._reparentedSortMenus.set(menu, {
312
+ parent: menu.parentNode,
313
+ nextSibling: menu.nextSibling,
314
+ trigger,
315
+ owner: trigger.closest(".datatable-sort"),
316
+ themeOwner: findThemeOwner(trigger)
317
+ });
318
+ this._reparentedSortMenuByTrigger.set(trigger, menu);
319
+ this._portalRoot(trigger).appendChild(menu);
320
+ }
321
+ _restoreSortMenuForTrigger(trigger) {
322
+ const menu = this._reparentedSortMenuByTrigger.get(trigger) || this._sortMenuForTrigger(trigger);
323
+ if (menu) {
324
+ this._restoreSortMenu(menu);
325
+ }
326
+ }
327
+ _restoreSortMenu(menu) {
328
+ const original = this._reparentedSortMenus.get(menu);
329
+ if (!original) {
330
+ return;
331
+ }
332
+ const { parent, nextSibling, trigger, themeOwner } = original;
333
+ if (themeOwner?.isConnected === false) {
334
+ menu.remove();
335
+ } else if (parent?.isConnected) {
336
+ if (nextSibling?.parentNode === parent) {
337
+ parent.insertBefore(menu, nextSibling);
338
+ } else {
339
+ parent.appendChild(menu);
340
+ }
341
+ } else {
342
+ menu.remove();
343
+ }
344
+ if (trigger) {
345
+ this._reparentedSortMenuByTrigger.delete(trigger);
346
+ }
347
+ this._reparentedSortMenus.delete(menu);
348
+ }
349
+ _restoreSortMenus() {
350
+ Array.from(this._reparentedSortMenus.keys()).forEach((menu) => {
351
+ this._restoreSortMenu(menu);
352
+ });
353
+ }
90
354
  _bootstrap(name) {
91
355
  return this._window.bootstrap?.[name] || null;
92
356
  }
93
-
357
+ _portalRoot(trigger = this._element) {
358
+ return ownerPortalRoot(findThemeOwner(trigger)) || this._document.body || this._document.documentElement;
359
+ }
94
360
  // The bulk-actions bar's icon-only buttons carry data-bs-title instead of
95
361
  // a bare title attribute so Bootstrap's own Tooltip owns the hover/focus
96
362
  // affordance (matching the rest of Moo's icon-button contracts) rather
@@ -107,69 +373,73 @@ export default class DataTable {
107
373
  if (!Tooltip) {
108
374
  return;
109
375
  }
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
- });
376
+ this._element.querySelectorAll('[data-datatable-bulk-actions] [data-bs-title]:not([data-bs-toggle="dropdown"])').forEach((trigger) => {
377
+ this._tooltips.push(Tooltip.getOrCreateInstance(trigger, {
378
+ animation: false,
379
+ container: this._portalRoot(trigger)
380
+ }));
381
+ });
123
382
  }
124
-
125
383
  _hideBulkTooltips({ blur = false } = {}) {
126
384
  this._tooltips.forEach((tooltip) => {
127
385
  tooltip.hide();
128
386
  });
129
-
130
387
  if (!blur) {
131
388
  return;
132
389
  }
133
-
134
390
  const active = this._document.activeElement;
135
391
  if (active?.closest?.("[data-datatable-bulk-actions]")) {
136
392
  active.blur();
137
393
  }
138
394
  }
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.
395
+ // Auto mode remembers independent narrow/wide choices; toggle mode keeps
396
+ // its existing viewport-independent preference.
144
397
  _initViewToggle() {
145
398
  const toggles = Array.from(this._element.querySelectorAll(".datatable-view-toggle"));
146
399
  if (!toggles.length) {
147
400
  return;
148
401
  }
149
402
  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 } = {}) => {
403
+ const breakpoint = this._window.getComputedStyle(this._element).getPropertyValue("--moo-datatable-responsive-breakpoint").trim();
404
+ const auto = this._element.hasAttribute("data-datatable-responsive-breakpoint");
405
+ const media = auto && breakpoint ? this._window.matchMedia(`(min-width: ${breakpoint})`) : null;
406
+ const preferences = /* @__PURE__ */ new Map();
407
+ const storageKey = /* @__PURE__ */ __name(() => `moo-datatable-view:${this._element.id}${media ? media.matches ? ":wide" : ":narrow" : ""}`, "storageKey");
408
+ const setView = /* @__PURE__ */ __name((value, { persist = false } = {}) => {
409
+ if (this._element.dataset.datatableView !== value) {
410
+ const Dropdown = this._bootstrap("Dropdown");
411
+ [...this._rowActionTriggers(), ...this._sortTriggers()].forEach((trigger) => {
412
+ Dropdown?.getInstance(trigger)?.hide();
413
+ });
414
+ }
152
415
  this._element.dataset.datatableView = value;
153
416
  inputs.forEach((input) => {
154
417
  input.checked = input.value === value;
155
418
  });
156
419
  if (persist) {
420
+ preferences.set(storageKey(), value);
157
421
  try {
158
- this._window.localStorage.setItem(storageKey, value);
422
+ this._window.localStorage.setItem(storageKey(), value);
159
423
  } catch {
160
- // Storage may be unavailable (private browsing); the toggle still works for this session.
161
424
  }
162
425
  }
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);
426
+ }, "setView");
427
+ const restoreView = /* @__PURE__ */ __name(() => {
428
+ let stored = preferences.get(storageKey());
429
+ try {
430
+ stored ?? (stored = this._window.localStorage.getItem(storageKey()));
431
+ } catch {
432
+ }
433
+ setView(stored === "table" || stored === "cards" ? stored : media ? media.matches ? "table" : "cards" : this._element.dataset.datatableView);
434
+ }, "restoreView");
435
+ if (!auto || media) {
436
+ restoreView();
437
+ } else {
438
+ inputs.forEach((input) => {
439
+ input.checked = false;
440
+ });
172
441
  }
442
+ if (media) this._listen(media, "change", restoreView);
173
443
  toggles.forEach((toggle) => {
174
444
  this._listen(toggle, "change", (event) => {
175
445
  const value = event.target.value;
@@ -180,7 +450,6 @@ export default class DataTable {
180
450
  });
181
451
  });
182
452
  }
183
-
184
453
  _readFacets(tr) {
185
454
  const facets = {};
186
455
  Array.from(tr.attributes).forEach((attr) => {
@@ -191,7 +460,6 @@ export default class DataTable {
191
460
  });
192
461
  return facets;
193
462
  }
194
-
195
463
  _readInitialFacetVisibility() {
196
464
  this._element.querySelectorAll("[data-datatable-facet]").forEach((facetRoot) => {
197
465
  const key = facetRoot.dataset.datatableFacet;
@@ -212,7 +480,6 @@ export default class DataTable {
212
480
  }
213
481
  });
214
482
  }
215
-
216
483
  _readInitialSort() {
217
484
  const th = this._element.querySelector("th[data-datatable-initial-sort][data-datatable-column]");
218
485
  const direction = th?.dataset.datatableInitialSort;
@@ -222,16 +489,14 @@ export default class DataTable {
222
489
  this._sortKey = th.dataset.datatableColumn || null;
223
490
  this._sortDirection = this._sortKey ? direction : null;
224
491
  }
225
-
226
492
  _trigger(name, detail = {}) {
227
493
  this._element.dispatchEvent(
228
494
  new this._window.CustomEvent(`${name}.moo.datatable`, {
229
495
  bubbles: true,
230
- detail,
496
+ detail
231
497
  })
232
498
  );
233
499
  }
234
-
235
500
  _sortValue(row, key) {
236
501
  const cell = row.element.querySelector(`:scope > [data-datatable-column="${key}"]`);
237
502
  if (!cell) {
@@ -239,7 +504,6 @@ export default class DataTable {
239
504
  }
240
505
  return normalize(cell.dataset.datatableSortValue ?? cell.textContent);
241
506
  }
242
-
243
507
  _matchesFilters(row) {
244
508
  if (this._searchTerm && !row.search.includes(this._searchTerm)) {
245
509
  return false;
@@ -254,7 +518,6 @@ export default class DataTable {
254
518
  }
255
519
  return true;
256
520
  }
257
-
258
521
  _visibleRows() {
259
522
  const filtered = this._rows.filter((row) => this._matchesFilters(row));
260
523
  if (this._sortKey) {
@@ -272,11 +535,9 @@ export default class DataTable {
272
535
  }
273
536
  return filtered;
274
537
  }
275
-
276
538
  _pageCount(rowCount) {
277
539
  return Math.max(1, Math.ceil(rowCount / this._pageSize));
278
540
  }
279
-
280
541
  _render() {
281
542
  const filtered = this._visibleRows();
282
543
  const pageCount = this._pageCount(filtered.length);
@@ -284,7 +545,6 @@ export default class DataTable {
284
545
  const start = (this._currentPage - 1) * this._pageSize;
285
546
  const pageRows = filtered.slice(start, start + this._pageSize);
286
547
  const pageRowIds = new Set(pageRows.map((row) => row.element.id));
287
-
288
548
  this._rows.forEach((row) => {
289
549
  const isVisible = pageRowIds.has(row.element.id);
290
550
  row.element.hidden = !isVisible;
@@ -292,19 +552,9 @@ export default class DataTable {
292
552
  row.cardElement.hidden = !isVisible;
293
553
  }
294
554
  });
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);
555
+ const currentOrder = Array.from(this._tbody.children).filter((element) => pageRowIds.has(element.id)).map((element) => element.id);
304
556
  const desiredOrder = pageRows.map((row) => row.element.id);
305
- const orderChanged =
306
- currentOrder.length !== desiredOrder.length ||
307
- currentOrder.some((id, index) => id !== desiredOrder[index]);
557
+ const orderChanged = currentOrder.length !== desiredOrder.length || currentOrder.some((id, index) => id !== desiredOrder[index]);
308
558
  if (orderChanged) {
309
559
  pageRows.forEach((row) => {
310
560
  this._tbody.appendChild(row.element);
@@ -313,7 +563,6 @@ export default class DataTable {
313
563
  }
314
564
  });
315
565
  }
316
-
317
566
  this._renderToolbarState();
318
567
  this._renderSortHeaders();
319
568
  this._renderSelection(pageRows);
@@ -323,7 +572,6 @@ export default class DataTable {
323
572
  this._renderPageSize();
324
573
  this._renderBulkActions();
325
574
  }
326
-
327
575
  _renderToolbarState() {
328
576
  const hasSearch = Boolean(this._searchTerm);
329
577
  const hasFacets = Array.from(this._facetSelections.values()).some((set) => set.size > 0);
@@ -331,10 +579,9 @@ export default class DataTable {
331
579
  if (resetButton) {
332
580
  resetButton.hidden = !(hasSearch || hasFacets);
333
581
  }
334
-
335
582
  this._element.querySelectorAll("[data-datatable-facet]").forEach((facetRoot) => {
336
583
  const key = facetRoot.dataset.datatableFacet;
337
- const selected = this._facetSelections.get(key) || new Set();
584
+ const selected = this._facetSelections.get(key) || /* @__PURE__ */ new Set();
338
585
  if (this._visibleFacets.has(key)) {
339
586
  facetRoot.hidden = !this._visibleFacets.get(key);
340
587
  }
@@ -360,36 +607,31 @@ export default class DataTable {
360
607
  option.setAttribute("aria-pressed", String(isSelected));
361
608
  });
362
609
  });
363
-
364
610
  this._element.querySelectorAll("[data-datatable-filter-toggle]").forEach((toggle) => {
365
611
  const key = toggle.dataset.datatableFilterToggle;
366
612
  const visible = this._visibleFacets.get(key) ?? true;
367
613
  toggle.classList.toggle("active", visible);
368
614
  toggle.setAttribute("aria-pressed", visible ? "true" : "false");
369
615
  });
370
-
371
616
  this._element.querySelectorAll("[data-datatable-filter-option]").forEach((option) => {
372
617
  const key = option.dataset.datatableFilterOptionKey;
373
- const selected = this._facetSelections.get(key) || new Set();
618
+ const selected = this._facetSelections.get(key) || /* @__PURE__ */ new Set();
374
619
  const active = selected.has(option.dataset.datatableFilterOption);
375
620
  option.classList.toggle("active", active);
376
621
  option.setAttribute("aria-pressed", active ? "true" : "false");
377
622
  });
378
-
379
623
  this._element.querySelectorAll("[data-datatable-filter-group-summary]").forEach((summary) => {
380
- const selected = this._facetSelections.get(summary.dataset.datatableFilterGroupSummary) || new Set();
624
+ const selected = this._facetSelections.get(summary.dataset.datatableFilterGroupSummary) || /* @__PURE__ */ new Set();
381
625
  summary.hidden = selected.size === 0;
382
626
  summary.textContent = selected.size > 0 ? `${selected.size} selected` : "";
383
627
  });
384
628
  }
385
-
386
629
  _createFacetBadge(text) {
387
630
  const badge = this._document.createElement("span");
388
631
  badge.className = "datatable-facet-badge";
389
632
  badge.textContent = text;
390
633
  return badge;
391
634
  }
392
-
393
635
  _renderSelection(pageRows) {
394
636
  const selectAllControls = Array.from(this._element.querySelectorAll("[data-datatable-select-all]"));
395
637
  pageRows.forEach((row) => {
@@ -408,7 +650,6 @@ export default class DataTable {
408
650
  control.indeterminate = checked > 0 && checked < total;
409
651
  });
410
652
  }
411
-
412
653
  _renderBulkActions() {
413
654
  const bar = this._element.querySelector("[data-datatable-bulk-actions]");
414
655
  if (!bar) {
@@ -420,14 +661,12 @@ export default class DataTable {
420
661
  count.textContent = String(this._selectedIds.size);
421
662
  }
422
663
  }
423
-
424
664
  _renderFrameState(totalRows) {
425
665
  const isEmpty = totalRows === 0;
426
666
  this._element.querySelectorAll(".datatable-frame, .datatable-card-frame").forEach((frame) => {
427
667
  frame.hidden = isEmpty;
428
668
  });
429
669
  }
430
-
431
670
  _renderEmptyState(totalRows) {
432
671
  const empty = this._element.querySelector("[data-datatable-empty]");
433
672
  if (!empty) {
@@ -447,7 +686,6 @@ export default class DataTable {
447
686
  reset.hidden = !hasRows;
448
687
  }
449
688
  }
450
-
451
689
  _sortAriaValue(direction) {
452
690
  if (direction === "asc") {
453
691
  return "ascending";
@@ -457,7 +695,6 @@ export default class DataTable {
457
695
  }
458
696
  return "none";
459
697
  }
460
-
461
698
  _renderSortHeaders() {
462
699
  this._element.querySelectorAll("th[data-datatable-column]").forEach((th) => {
463
700
  const trigger = th.querySelector("[data-datatable-sort-key]");
@@ -478,7 +715,6 @@ export default class DataTable {
478
715
  }
479
716
  });
480
717
  }
481
-
482
718
  _renderPagination(totalRows, pageCount) {
483
719
  const first = this._element.querySelector("[data-datatable-page-first]");
484
720
  const prev = this._element.querySelector("[data-datatable-page-prev]");
@@ -488,7 +724,6 @@ export default class DataTable {
488
724
  const atEnd = this._currentPage >= pageCount;
489
725
  [first, prev].forEach((button) => this._setPageItemDisabled(button, atStart));
490
726
  [next, last].forEach((button) => this._setPageItemDisabled(button, atEnd));
491
-
492
727
  const summary = this._element.querySelector("[data-datatable-results-summary]");
493
728
  if (summary) {
494
729
  if (totalRows === 0) {
@@ -499,42 +734,53 @@ export default class DataTable {
499
734
  summary.textContent = `Showing ${start}-${end} of ${totalRows}`;
500
735
  }
501
736
  }
502
-
503
737
  const template = this._element.querySelector("template[data-datatable-page-numbers]");
504
738
  if (!template) {
505
739
  return;
506
740
  }
507
- template.parentNode
508
- .querySelectorAll("[data-datatable-page-number], [data-datatable-page-ellipsis]")
509
- .forEach((node) => node.remove());
510
-
511
741
  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");
742
+ const existing = Array.from(
743
+ template.parentNode.querySelectorAll(
744
+ "[data-datatable-page-number], [data-datatable-page-ellipsis]"
745
+ )
746
+ );
747
+ const existingWindow = existing.map(
748
+ (node) => node.dataset.datatablePageEllipsis === void 0 ? Number(node.dataset.datatablePageNumber) : "\u2026"
749
+ );
750
+ const windowChanged = existingWindow.length !== pages.length || existingWindow.some((page, index) => page !== pages[index]);
751
+ if (windowChanged) {
752
+ existing.forEach((node) => node.remove());
753
+ const fragment = this._document.createDocumentFragment();
754
+ pages.forEach((page) => {
755
+ const li = this._document.createElement("li");
756
+ li.className = "page-item";
757
+ if (page === "\u2026") {
758
+ li.dataset.datatablePageEllipsis = "";
759
+ li.innerHTML = '<span class="page-link">\u2026</span>';
760
+ } else {
761
+ li.dataset.datatablePageNumber = String(page);
762
+ const button = this._document.createElement("button");
763
+ button.type = "button";
764
+ button.className = "page-link";
765
+ button.textContent = String(page);
766
+ button.setAttribute("aria-label", `Go to page ${page}`);
767
+ button.dataset.datatablePageGo = String(page);
768
+ li.appendChild(button);
524
769
  }
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);
770
+ fragment.appendChild(li);
771
+ });
772
+ template.parentNode.insertBefore(fragment, template);
773
+ }
774
+ template.parentNode.querySelectorAll("[data-datatable-page-number]").forEach((li) => {
775
+ const isActive = Number(li.dataset.datatablePageNumber) === this._currentPage;
776
+ li.classList.toggle("active", isActive);
777
+ if (isActive) {
778
+ li.setAttribute("aria-current", "page");
779
+ } else {
780
+ li.removeAttribute("aria-current");
532
781
  }
533
- fragment.appendChild(li);
534
782
  });
535
- template.parentNode.insertBefore(fragment, template);
536
783
  }
537
-
538
784
  _renderPageSize() {
539
785
  const currentSize = String(this._pageSize);
540
786
  const value = this._element.querySelector("[data-datatable-page-size-value]");
@@ -551,7 +797,6 @@ export default class DataTable {
551
797
  option.setAttribute("aria-pressed", active ? "true" : "false");
552
798
  });
553
799
  }
554
-
555
800
  _setPageItemDisabled(button, disabled) {
556
801
  if (!button) {
557
802
  return;
@@ -559,31 +804,24 @@ export default class DataTable {
559
804
  button.disabled = disabled;
560
805
  button.closest(".page-item")?.classList.toggle("disabled", disabled);
561
806
  }
562
-
563
807
  _pageWindow(current, total) {
564
808
  if (total <= 7) {
565
809
  return Array.from({ length: total }, (_, index) => index + 1);
566
810
  }
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);
811
+ const pages = /* @__PURE__ */ new Set([1, total, current - 1, current, current + 1]);
812
+ const sorted = Array.from(pages).filter((page) => page >= 1 && page <= total).sort((a, b) => a - b);
571
813
  const withEllipsis = [];
572
814
  sorted.forEach((page, index) => {
573
815
  if (index > 0 && page - sorted[index - 1] > 1) {
574
- withEllipsis.push("…");
816
+ withEllipsis.push("\u2026");
575
817
  }
576
818
  withEllipsis.push(page);
577
819
  });
578
820
  return withEllipsis;
579
821
  }
580
-
581
822
  _isColumnFixed(key) {
582
- return this._element
583
- .querySelector(`th[data-datatable-column="${key}"]`)
584
- ?.hasAttribute("data-datatable-column-fixed") || false;
823
+ return this._element.querySelector(`th[data-datatable-column="${key}"]`)?.hasAttribute("data-datatable-column-fixed") || false;
585
824
  }
586
-
587
825
  _setColumnVisible(key, visible, { syncViewToggle = true } = {}) {
588
826
  if (!visible && this._isColumnFixed(key)) {
589
827
  return;
@@ -598,7 +836,6 @@ export default class DataTable {
598
836
  });
599
837
  }
600
838
  }
601
-
602
839
  _setSort(key, direction) {
603
840
  const validDirection = direction === "asc" || direction === "desc" ? direction : null;
604
841
  this._sortKey = validDirection ? key : null;
@@ -606,16 +843,17 @@ export default class DataTable {
606
843
  this._render();
607
844
  this._trigger("sort", { key: this._sortKey, direction: this._sortDirection });
608
845
  }
609
-
610
846
  _handleSortAction(event) {
611
847
  const trigger = event.target.closest("[data-datatable-sort-action]");
612
848
  if (!trigger) {
613
849
  return;
614
850
  }
615
851
  const menu = trigger.closest(".dropdown-menu");
616
- const key = menu
617
- ?.closest(".datatable-sort")
618
- ?.querySelector("[data-datatable-sort-key]")?.dataset.datatableSortKey;
852
+ const sort = menu?.closest(".datatable-sort") || this._reparentedSortMenus.get(menu)?.owner;
853
+ if (!sort || !this._element.contains(sort)) {
854
+ return;
855
+ }
856
+ const key = sort.querySelector("[data-datatable-sort-key]")?.dataset.datatableSortKey;
619
857
  if (!key) {
620
858
  return;
621
859
  }
@@ -626,7 +864,6 @@ export default class DataTable {
626
864
  }
627
865
  this._setSort(key, action);
628
866
  }
629
-
630
867
  _handleFacetClick(event) {
631
868
  const option = event.target.closest("[data-datatable-facet-option]");
632
869
  const clear = event.target.closest("[data-datatable-facet-clear]");
@@ -638,7 +875,7 @@ export default class DataTable {
638
875
  if (!key) {
639
876
  return;
640
877
  }
641
- const selected = this._facetSelections.get(key) || new Set();
878
+ const selected = this._facetSelections.get(key) || /* @__PURE__ */ new Set();
642
879
  if (clear) {
643
880
  selected.clear();
644
881
  } else {
@@ -657,7 +894,6 @@ export default class DataTable {
657
894
  this._render();
658
895
  this._trigger("filter");
659
896
  }
660
-
661
897
  _handleFacetSearch(event) {
662
898
  const search = event.target.closest("[data-datatable-facet-search]");
663
899
  if (!search) {
@@ -670,7 +906,6 @@ export default class DataTable {
670
906
  option.closest("li")?.classList.toggle("datatable-facet-search-hidden", query.length > 0 && !label.includes(query));
671
907
  });
672
908
  }
673
-
674
909
  // Bootstrap's own dropdown keyboard navigation moves focus between
675
910
  // .dropdown-item siblings, but it deliberately leaves focus alone while an
676
911
  // <input> has it (arrow keys inside a text field move the caret, not a
@@ -694,7 +929,6 @@ export default class DataTable {
694
929
  firstVisible.focus();
695
930
  }
696
931
  }
697
-
698
932
  _handleFilterOptionClick(event) {
699
933
  const option = event.target.closest("[data-datatable-filter-option]");
700
934
  if (!option) {
@@ -706,7 +940,7 @@ export default class DataTable {
706
940
  return;
707
941
  }
708
942
  this._visibleFacets.set(key, true);
709
- const selected = this._facetSelections.get(key) || new Set();
943
+ const selected = this._facetSelections.get(key) || /* @__PURE__ */ new Set();
710
944
  if (selected.has(value)) {
711
945
  selected.delete(value);
712
946
  } else {
@@ -720,18 +954,14 @@ export default class DataTable {
720
954
  this._render();
721
955
  this._trigger("filter");
722
956
  }
723
-
724
957
  _showFilterPickerPanel(key = null) {
725
958
  this._element.querySelectorAll("[data-datatable-filter-panel]").forEach((panel) => {
726
959
  const isFacetList = panel.dataset.datatableFilterPanel === "facets";
727
960
  const isRequestedOptions = panel.dataset.datatableFilterPanelKey === key;
728
961
  panel.hidden = key ? !isRequestedOptions : !isFacetList;
729
962
  });
730
- this._element
731
- .querySelector(".datatable-search-filter-menu")
732
- ?.setAttribute("data-datatable-filter-menu-panel", key ? "options" : "facets");
963
+ this._element.querySelector(".datatable-search-filter-menu")?.setAttribute("data-datatable-filter-menu-panel", key ? "options" : "facets");
733
964
  }
734
-
735
965
  _handleFilterPickerClick(event) {
736
966
  const group = event.target.closest("[data-datatable-filter-group]");
737
967
  if (group) {
@@ -746,7 +976,6 @@ export default class DataTable {
746
976
  this._showFilterPickerPanel();
747
977
  }
748
978
  }
749
-
750
979
  _handleFilterOptionSearch(event) {
751
980
  const search = event.target.closest("[data-datatable-filter-option-search]");
752
981
  if (!search) {
@@ -759,7 +988,6 @@ export default class DataTable {
759
988
  option.closest("li")?.classList.toggle("datatable-filter-option-search-hidden", query.length > 0 && !label.includes(query));
760
989
  });
761
990
  }
762
-
763
991
  // Same bridge as _handleFacetSearchKeydown, for the filter-picker's
764
992
  // drill-down option search box.
765
993
  _handleFilterOptionSearchKeydown(event) {
@@ -782,7 +1010,6 @@ export default class DataTable {
782
1010
  firstVisible.focus();
783
1011
  }
784
1012
  }
785
-
786
1013
  _handleFilterToggleClick(event) {
787
1014
  const toggle = event.target.closest("[data-datatable-filter-toggle]");
788
1015
  if (!toggle) {
@@ -801,7 +1028,6 @@ export default class DataTable {
801
1028
  }
802
1029
  this._render();
803
1030
  }
804
-
805
1031
  _handleColumnToggleClick(event) {
806
1032
  const toggle = event.target.closest("[data-datatable-column-toggle]");
807
1033
  if (!toggle) {
@@ -811,7 +1037,6 @@ export default class DataTable {
811
1037
  const visible = toggle.getAttribute("aria-pressed") === "true";
812
1038
  this._setColumnVisible(key, visible, { syncViewToggle: false });
813
1039
  }
814
-
815
1040
  _handleReset() {
816
1041
  const search = this._element.querySelector("[data-datatable-search]");
817
1042
  if (search) {
@@ -828,7 +1053,6 @@ export default class DataTable {
828
1053
  this._render();
829
1054
  this._trigger("filter");
830
1055
  }
831
-
832
1056
  _handleSelectAll(event) {
833
1057
  const checked = event.target.checked;
834
1058
  const filtered = this._visibleRows();
@@ -844,7 +1068,6 @@ export default class DataTable {
844
1068
  this._render();
845
1069
  this._trigger("select");
846
1070
  }
847
-
848
1071
  _handleSelectRow(event) {
849
1072
  const checkbox = event.target.closest("[data-datatable-select-row]");
850
1073
  if (!checkbox) {
@@ -864,7 +1087,6 @@ export default class DataTable {
864
1087
  this._render();
865
1088
  this._trigger("select");
866
1089
  }
867
-
868
1090
  _handleBulkClick(event) {
869
1091
  if (event.target.closest("[data-datatable-bulk-clear]")) {
870
1092
  this._clearSelection();
@@ -887,14 +1109,12 @@ export default class DataTable {
887
1109
  this._trigger(`bulk-${action}`, { ids: this.getSelectedIds() });
888
1110
  }
889
1111
  }
890
-
891
1112
  _clearSelection() {
892
1113
  this._hideBulkTooltips({ blur: true });
893
1114
  this._selectedIds.clear();
894
1115
  this._render();
895
1116
  this._trigger("select");
896
1117
  }
897
-
898
1118
  _applyBulkUpdate(key, value, trigger) {
899
1119
  const icon = trigger.querySelector("svg")?.cloneNode(true);
900
1120
  const label = trigger.querySelector("span")?.textContent?.trim() ?? value;
@@ -927,7 +1147,6 @@ export default class DataTable {
927
1147
  this._render();
928
1148
  this._trigger("bulk-update", { key, value, ids: this.getSelectedIds() });
929
1149
  }
930
-
931
1150
  _applyBulkDelete() {
932
1151
  const ids = this.getSelectedIds();
933
1152
  ids.forEach((id) => {
@@ -942,7 +1161,6 @@ export default class DataTable {
942
1161
  this._render();
943
1162
  this._trigger("bulk-delete", { ids });
944
1163
  }
945
-
946
1164
  _handlePageClick(event) {
947
1165
  const goButton = event.target.closest("[data-datatable-page-go]");
948
1166
  if (goButton) {
@@ -970,7 +1188,6 @@ export default class DataTable {
970
1188
  this._render();
971
1189
  }
972
1190
  }
973
-
974
1191
  _setPageSize(value) {
975
1192
  const nextPageSize = Number(value);
976
1193
  if (!Number.isFinite(nextPageSize) || nextPageSize <= 0) {
@@ -981,7 +1198,6 @@ export default class DataTable {
981
1198
  this._currentPage = 1;
982
1199
  this._render();
983
1200
  }
984
-
985
1201
  _bindEvents() {
986
1202
  const search = this._element.querySelector("[data-datatable-search]");
987
1203
  this._listen(search, "input", (event) => {
@@ -1001,9 +1217,7 @@ export default class DataTable {
1001
1217
  Dropdown.getOrCreateInstance(trigger).hide();
1002
1218
  }
1003
1219
  });
1004
-
1005
1220
  this._listen(this._element, "click", (event) => {
1006
- this._handleSortAction(event);
1007
1221
  this._handleFilterPickerClick(event);
1008
1222
  this._handleFilterOptionClick(event);
1009
1223
  this._handleFilterToggleClick(event);
@@ -1018,63 +1232,51 @@ export default class DataTable {
1018
1232
  this._handleSelectRow(event);
1019
1233
  }
1020
1234
  });
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.
1235
+ this._listen(this._document, "click", (event) => this._handleSortAction(event));
1027
1236
  this._listen(this._document, "keydown", (event) => {
1028
1237
  if (event.key === "Escape" && this._selectedIds.size > 0) {
1029
1238
  this._clearSelection();
1030
1239
  }
1031
1240
  });
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
1241
  this._listen(this._element, "show.bs.dropdown", (event) => {
1242
+ this._reparentRowActionMenu(event.target);
1243
+ this._reparentSortMenu(event.target);
1043
1244
  const tooltip = this._bootstrap("Tooltip")?.getInstance(event.target);
1044
1245
  tooltip?.hide();
1045
1246
  tooltip?.disable();
1046
1247
  });
1047
1248
  this._listen(this._element, "hidden.bs.dropdown", (event) => {
1249
+ this._restoreRowActionMenuForTrigger(event.target);
1250
+ this._restoreSortMenuForTrigger(event.target);
1048
1251
  this._bootstrap("Tooltip")?.getInstance(event.target)?.enable();
1049
1252
  if (event.target.matches("[data-datatable-filter-menu-trigger]")) {
1050
1253
  this._showFilterPickerPanel();
1051
1254
  }
1052
1255
  });
1053
-
1054
1256
  this._element.querySelectorAll("[data-datatable-filter-option-search]").forEach((searchInput) => {
1055
1257
  this._listen(searchInput, "input", (event) => this._handleFilterOptionSearch(event));
1056
1258
  this._listen(searchInput, "keydown", (event) => this._handleFilterOptionSearchKeydown(event));
1057
1259
  });
1058
-
1059
1260
  this._element.querySelectorAll("[data-datatable-facet-search]").forEach((searchInput) => {
1060
1261
  this._listen(searchInput, "input", (event) => this._handleFacetSearch(event));
1061
1262
  this._listen(searchInput, "keydown", (event) => this._handleFacetSearchKeydown(event));
1062
1263
  });
1063
-
1064
1264
  const resetButton = this._element.querySelector("[data-datatable-reset]");
1065
1265
  this._listen(resetButton, "click", () => this._handleReset());
1066
-
1067
1266
  this._element.querySelectorAll("[data-datatable-select-all]").forEach((selectAll) => {
1068
1267
  this._listen(selectAll, "change", (event) => this._handleSelectAll(event));
1069
1268
  });
1070
-
1071
1269
  const pageSizeSelect = this._element.querySelector("[data-datatable-page-size-select]");
1072
1270
  this._listen(pageSizeSelect, "change", (event) => this._setPageSize(event.target.value));
1073
-
1074
1271
  this._element.querySelectorAll("[data-datatable-page-size-option]").forEach((option) => {
1075
1272
  this._listen(option, "click", () => {
1076
1273
  this._setPageSize(option.dataset.datatablePageSizeOption);
1077
1274
  });
1078
1275
  });
1079
1276
  }
1080
- }
1277
+ };
1278
+ __name(_DataTable, "DataTable");
1279
+ var DataTable = _DataTable;
1280
+ export {
1281
+ DataTable as default
1282
+ };