@wpmoo/ui 2.1.0 → 2.2.0

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 (40) hide show
  1. package/README.md +27 -2
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/certification.json +2 -1
  4. package/dist/assets/css/moo-ui.css +429 -1
  5. package/dist/assets/css/moo-ui.min.css +2 -2
  6. package/dist/assets/css/moo.css +349 -1
  7. package/dist/assets/css/moo.min.css +2 -2
  8. package/dist/js/chart.js +9 -1
  9. package/dist/js/chart.min.js +2 -2
  10. package/dist/js/combobox.js +17 -6
  11. package/dist/js/context-menu.js +8 -3
  12. package/dist/js/datatable.js +41 -19
  13. package/dist/js/datepicker.js +23 -9
  14. package/dist/js/datepicker.min.js +2 -2
  15. package/dist/js/floating-panel.js +1 -1
  16. package/dist/js/image-gallery.js +247 -0
  17. package/dist/js/moo-ui.js +360 -34
  18. package/dist/js/moo-ui.min.js +2 -2
  19. package/dist/js/overlay-scrollbar.js +1 -1
  20. package/dist/js/password-requirements.js +1 -1
  21. package/dist/js/sheet.js +1 -1
  22. package/dist/js/sidebar.js +42 -1
  23. package/dist/js/slider.js +1 -1
  24. package/dist/js/state.js +1 -1
  25. package/dist/js/table-of-contents.js +1 -1
  26. package/dist/js/toc.js +1 -1
  27. package/dist/release-manifest.json +4 -4
  28. package/package.json +3 -1
  29. package/scss/_components.scss +2 -0
  30. package/scss/components/_gallery.scss +73 -0
  31. package/scss/components/_item.scss +96 -0
  32. package/scss/foundations/_globals.scss +1 -0
  33. package/scss/foundations/_scope.scss +2 -0
  34. package/scss/mixins/_banner.scss +1 -1
  35. package/scss/moo-core.scss +1 -0
  36. package/scss/moo-ui.scss +2 -0
  37. package/scss/settings/_component_variables.scss +8 -0
  38. package/scss/utilities/_gallery.scss +25 -0
  39. package/scss/utilities/_motion.scss +5 -0
  40. package/scss/utilities/_motion_primitives.scss +10 -0
package/dist/js/moo-ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI moo-ui.js v2.1.0 (https://wpmoo.org/)
2
+ * Moo UI moo-ui.js v2.2.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -247,11 +247,7 @@ var Combobox = class _Combobox {
247
247
  _clearStaleSelection() {
248
248
  if (this._isMultiple) {
249
249
  this._input.value = "";
250
- this._options.forEach((option) => {
251
- option.hidden = false;
252
- });
253
- this._syncGroupVisibility();
254
- this._empty.hidden = true;
250
+ this._showAllOptions();
255
251
  return;
256
252
  }
257
253
  const selected = this._selectedOptions()[0] || null;
@@ -262,6 +258,14 @@ var Combobox = class _Combobox {
262
258
  this._clearSelection();
263
259
  this._input.value = "";
264
260
  }
261
+ _showAllOptions() {
262
+ this._options.forEach((option) => {
263
+ option.hidden = false;
264
+ });
265
+ this._syncGroupVisibility();
266
+ this._empty.hidden = true;
267
+ this._liveRegion.textContent = "";
268
+ }
265
269
  _activeOption() {
266
270
  const activeId = this._input.getAttribute("aria-activedescendant");
267
271
  if (!activeId) {
@@ -348,6 +352,7 @@ var Combobox = class _Combobox {
348
352
  if (this._hidden) {
349
353
  this._hidden.value = option.dataset.value || "";
350
354
  }
355
+ this._showAllOptions();
351
356
  this._toggleClear(true);
352
357
  this._setActiveOption(option);
353
358
  this._emitChange();
@@ -440,6 +445,12 @@ var Combobox = class _Combobox {
440
445
  }
441
446
  }
442
447
  _handleKeydown(event) {
448
+ if (event.isComposing || event.keyCode === 229) {
449
+ if (event.key === "Tab") {
450
+ this._closeMenu();
451
+ }
452
+ return;
453
+ }
443
454
  const available = this._visibleOptions();
444
455
  const current = available.findIndex(
445
456
  (option) => option.id === this._input.getAttribute("aria-activedescendant")
@@ -544,12 +555,17 @@ var ContextMenu = class _ContextMenu {
544
555
  this._bindEvents();
545
556
  }
546
557
  dispose() {
558
+ if (this._disposing) {
559
+ return;
560
+ }
561
+ this._disposing = true;
547
562
  this._listeners.forEach(({ target, type, handler, options }) => {
548
563
  target.removeEventListener(type, handler, options);
549
564
  });
550
565
  this._listeners = [];
551
- this._dropdown.dispose();
552
566
  this._syncExpanded(false);
567
+ this._dropdown.hide();
568
+ this._dropdown.dispose();
553
569
  this._invoker = null;
554
570
  this._pointerRect = null;
555
571
  instances2.delete(this._element);
@@ -661,7 +677,7 @@ var ContextMenu = class _ContextMenu {
661
677
  event.preventDefault();
662
678
  event.stopPropagation();
663
679
  const group = radioButton.dataset.contextMenuRadioGroup;
664
- this._menu.querySelectorAll(`[data-context-menu-radio-group="${group}"]`).forEach((button) => {
680
+ this._menu.querySelectorAll(`[data-context-menu-radio-group="${CSS.escape(String(group))}"]`).forEach((button) => {
665
681
  const checked = button === radioButton;
666
682
  button.classList.toggle("active", checked);
667
683
  button.setAttribute("aria-pressed", String(checked));
@@ -749,9 +765,30 @@ function ownerPortalRoot(owner) {
749
765
 
750
766
  // src/js/components/datatable.js
751
767
  var instances3 = /* @__PURE__ */ new WeakMap();
768
+ var SORT_COLLATOR = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
769
+ var PLAIN_NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)$/;
770
+ function sortNumber(value) {
771
+ return PLAIN_NUMBER.test(value) ? Number(value) : null;
772
+ }
773
+ function compareSortValues(a, b) {
774
+ const aNumber = sortNumber(a);
775
+ const bNumber = sortNumber(b);
776
+ if (aNumber !== null && bNumber !== null) {
777
+ return Math.sign(aNumber - bNumber);
778
+ }
779
+ if (aNumber !== null || bNumber !== null) {
780
+ return aNumber !== null ? -1 : 1;
781
+ }
782
+ return SORT_COLLATOR.compare(a, b);
783
+ }
752
784
  function normalize2(value) {
753
785
  return (value ?? "").toString().trim().toLowerCase();
754
786
  }
787
+ var ESCAPE_OWNER_SELECTOR = ".modal, .offcanvas, .dropdown-menu, .popover, [role='dialog'], [role='menu'], [role='listbox']";
788
+ function escapeBelongsElsewhere(event) {
789
+ const target = event.target;
790
+ return typeof target?.closest === "function" && (Boolean(target.closest(ESCAPE_OWNER_SELECTOR)) || target.getAttribute("aria-expanded") === "true");
791
+ }
755
792
  var DataTable = class _DataTable {
756
793
  static getInstance(element) {
757
794
  return element?.nodeType === 1 ? instances3.get(element) || null : null;
@@ -1201,7 +1238,7 @@ var DataTable = class _DataTable {
1201
1238
  );
1202
1239
  }
1203
1240
  _sortValue(row, key) {
1204
- const cell = row.element.querySelector(`:scope > [data-datatable-column="${key}"]`);
1241
+ const cell = row.element.querySelector(`:scope > [data-datatable-column="${CSS.escape(String(key))}"]`);
1205
1242
  if (!cell) {
1206
1243
  return "";
1207
1244
  }
@@ -1228,10 +1265,8 @@ var DataTable = class _DataTable {
1228
1265
  filtered.sort((a, b) => {
1229
1266
  const aValue = this._sortValue(a, this._sortKey);
1230
1267
  const bValue = this._sortValue(b, this._sortKey);
1231
- if (aValue === bValue) {
1232
- return a.index - b.index;
1233
- }
1234
- return aValue > bValue ? direction : -direction;
1268
+ const order = compareSortValues(aValue, bValue);
1269
+ return order ? order * direction : a.index - b.index;
1235
1270
  });
1236
1271
  } else {
1237
1272
  filtered.sort((a, b) => a.index - b.index);
@@ -1298,7 +1333,7 @@ var DataTable = class _DataTable {
1298
1333
  summary.appendChild(this._createFacetBadge(`${selected.size} selected`));
1299
1334
  } else {
1300
1335
  selected.forEach((value) => {
1301
- const option = facetRoot.querySelector(`[data-datatable-facet-option="${value}"]`);
1336
+ const option = facetRoot.querySelector(`[data-datatable-facet-option="${CSS.escape(String(value))}"]`);
1302
1337
  const label = option?.querySelector("[data-datatable-facet-option-label]")?.textContent ?? value;
1303
1338
  summary.appendChild(this._createFacetBadge(label));
1304
1339
  });
@@ -1527,17 +1562,17 @@ var DataTable = class _DataTable {
1527
1562
  return withEllipsis;
1528
1563
  }
1529
1564
  _isColumnFixed(key) {
1530
- return this._element.querySelector(`th[data-datatable-column="${key}"]`)?.hasAttribute("data-datatable-column-fixed") || false;
1565
+ return this._element.querySelector(`th[data-datatable-column="${CSS.escape(String(key))}"]`)?.hasAttribute("data-datatable-column-fixed") || false;
1531
1566
  }
1532
1567
  _setColumnVisible(key, visible, { syncViewToggle = true } = {}) {
1533
1568
  if (!visible && this._isColumnFixed(key)) {
1534
1569
  return;
1535
1570
  }
1536
- this._element.querySelectorAll(`[data-datatable-column="${key}"], [data-datatable-detail-column="${key}"]`).forEach((cell) => {
1571
+ this._element.querySelectorAll(`[data-datatable-column="${CSS.escape(String(key))}"], [data-datatable-detail-column="${CSS.escape(String(key))}"]`).forEach((cell) => {
1537
1572
  cell.classList.toggle("datatable-col-hidden", !visible);
1538
1573
  });
1539
1574
  if (syncViewToggle) {
1540
- this._element.querySelectorAll(`[data-datatable-column-toggle="${key}"]`).forEach((toggle) => {
1575
+ this._element.querySelectorAll(`[data-datatable-column-toggle="${CSS.escape(String(key))}"]`).forEach((toggle) => {
1541
1576
  toggle.classList.toggle("active", visible);
1542
1577
  toggle.setAttribute("aria-pressed", String(visible));
1543
1578
  });
@@ -1674,8 +1709,8 @@ var DataTable = class _DataTable {
1674
1709
  if (group) {
1675
1710
  const key = group.dataset.datatableFilterGroup;
1676
1711
  this._showFilterPickerPanel(key);
1677
- const search = this._element.querySelector(`[data-datatable-filter-option-search="${key}"]`);
1678
- const option = this._element.querySelector(`[data-datatable-filter-option-key="${key}"]`);
1712
+ const search = this._element.querySelector(`[data-datatable-filter-option-search="${CSS.escape(String(key))}"]`);
1713
+ const option = this._element.querySelector(`[data-datatable-filter-option-key="${CSS.escape(String(key))}"]`);
1679
1714
  (search || option)?.focus({ preventScroll: true });
1680
1715
  return;
1681
1716
  }
@@ -1690,7 +1725,7 @@ var DataTable = class _DataTable {
1690
1725
  }
1691
1726
  const key = search.dataset.datatableFilterOptionSearch;
1692
1727
  const query = normalize2(search.value);
1693
- this._element.querySelectorAll(`[data-datatable-filter-option-key="${key}"]`).forEach((option) => {
1728
+ this._element.querySelectorAll(`[data-datatable-filter-option-key="${CSS.escape(String(key))}"]`).forEach((option) => {
1694
1729
  const label = normalize2(option.querySelector("[data-datatable-filter-option-label]")?.textContent || option.textContent);
1695
1730
  option.closest("li")?.classList.toggle("datatable-filter-option-search-hidden", query.length > 0 && !label.includes(query));
1696
1731
  });
@@ -1707,7 +1742,7 @@ var DataTable = class _DataTable {
1707
1742
  }
1708
1743
  const key = search.dataset.datatableFilterOptionSearch;
1709
1744
  const options = Array.from(
1710
- this._element.querySelectorAll(`[data-datatable-filter-option-key="${key}"]`)
1745
+ this._element.querySelectorAll(`[data-datatable-filter-option-key="${CSS.escape(String(key))}"]`)
1711
1746
  );
1712
1747
  const firstVisible = options.find(
1713
1748
  (option) => !option.closest("li")?.classList.contains("datatable-filter-option-search-hidden")
@@ -1832,7 +1867,7 @@ var DataTable = class _DataTable {
1832
1867
  }
1833
1868
  row.element.setAttribute(`data-datatable-facet-${key}`, value);
1834
1869
  row.facets[key] = value;
1835
- const cell = row.element.querySelector(`[data-datatable-column="${key}"]`);
1870
+ const cell = row.element.querySelector(`[data-datatable-column="${CSS.escape(String(key))}"]`);
1836
1871
  if (!cell) {
1837
1872
  return;
1838
1873
  }
@@ -1846,7 +1881,7 @@ var DataTable = class _DataTable {
1846
1881
  if (currentIcon && icon) {
1847
1882
  currentIcon.replaceWith(icon.cloneNode(true));
1848
1883
  }
1849
- const cardValue = row.cardElement?.querySelector(`[data-datatable-detail-column="${key}"] .datatable-card-value`);
1884
+ const cardValue = row.cardElement?.querySelector(`[data-datatable-detail-column="${CSS.escape(String(key))}"] .datatable-card-value`);
1850
1885
  if (cardValue) {
1851
1886
  cardValue.innerHTML = cell.innerHTML;
1852
1887
  }
@@ -1940,11 +1975,11 @@ var DataTable = class _DataTable {
1940
1975
  }
1941
1976
  });
1942
1977
  this._listen(this._document, "click", (event) => this._handleSortAction(event));
1943
- this._listen(this._document, "keydown", (event) => {
1944
- if (event.key === "Escape" && this._selectedIds.size > 0) {
1978
+ this._listen(this._window, "keydown", (event) => {
1979
+ if (event.key === "Escape" && this._selectedIds.size > 0 && !escapeBelongsElsewhere(event)) {
1945
1980
  this._clearSelection();
1946
1981
  }
1947
- });
1982
+ }, { capture: true });
1948
1983
  this._listen(this._element, "show.bs.dropdown", (event) => {
1949
1984
  this._reparentRowActionMenu(event.target);
1950
1985
  this._reparentSortMenu(event.target);
@@ -2141,6 +2176,15 @@ function dispatch(element, window2, name, detail = {}, cancelable = false) {
2141
2176
  })
2142
2177
  );
2143
2178
  }
2179
+ function formDefaultValue(input) {
2180
+ if (!input) {
2181
+ return "";
2182
+ }
2183
+ if (!("datepickerFormDefault" in input.dataset)) {
2184
+ input.dataset.datepickerFormDefault = input.getAttribute("value") || "";
2185
+ }
2186
+ return input.dataset.datepickerFormDefault;
2187
+ }
2144
2188
  function setHiddenValue(input, value) {
2145
2189
  if (!input) return;
2146
2190
  input.value = value || "";
@@ -2544,6 +2588,7 @@ var MooCalendar = class _MooCalendar {
2544
2588
  ArrowUp: -7
2545
2589
  };
2546
2590
  let nextDate = null;
2591
+ let stepTowardActive = false;
2547
2592
  if (event.key in keyMap) {
2548
2593
  nextDate = addDays(this._activeDate, keyMap[event.key]);
2549
2594
  } else if (event.key === "Home") {
@@ -2554,10 +2599,13 @@ var MooCalendar = class _MooCalendar {
2554
2599
  const weekStart = firstDayOfWeek(this._config.locale);
2555
2600
  const weekdayOffset = (this._activeDate.getDay() - weekStart + 7) % 7;
2556
2601
  nextDate = addDays(this._activeDate, 6 - weekdayOffset);
2557
- } else if (event.key === "PageUp") {
2558
- nextDate = addMonths(this._activeDate, -1);
2559
- } else if (event.key === "PageDown") {
2560
- nextDate = addMonths(this._activeDate, 1);
2602
+ } else if (event.key === "PageUp" || event.key === "PageDown") {
2603
+ const target = addMonths(this._activeDate, event.key === "PageUp" ? -1 : 1);
2604
+ const { minDate, maxDate } = this._config;
2605
+ nextDate = limitToRange(target, minDate, maxDate);
2606
+ stepTowardActive = [minDate, maxDate].some(
2607
+ (edge) => edge && compareDates(nextDate, edge) === 0
2608
+ );
2561
2609
  } else if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
2562
2610
  event.preventDefault();
2563
2611
  this._selectDate(this._activeDate, true);
@@ -2566,7 +2614,8 @@ var MooCalendar = class _MooCalendar {
2566
2614
  return;
2567
2615
  }
2568
2616
  event.preventDefault();
2569
- this._setActiveDate(nextDate, true, compareDates(nextDate, this._activeDate) < 0 ? -1 : 1);
2617
+ const backward = compareDates(nextDate, this._activeDate) < 0;
2618
+ this._setActiveDate(nextDate, true, backward !== stepTowardActive ? -1 : 1);
2570
2619
  }
2571
2620
  _setActiveDate(date, focus = false, step = 0) {
2572
2621
  const nextDate = step === 0 ? this._nearestEnabledDate(date) : this._nextEnabledDate(date, step);
@@ -2832,7 +2881,7 @@ var MooDatepicker = class _MooDatepicker {
2832
2881
  value: config.value || this._input.value || element.dataset.datepickerValue
2833
2882
  });
2834
2883
  this._placeholder = this._config.placeholder;
2835
- this._defaultValue = this._input.defaultValue || this._input.getAttribute("value") || "";
2884
+ this._defaultValue = formDefaultValue(this._input);
2836
2885
  this.calendar = MooCalendar.getOrCreateInstance(this._calendarRoot, {
2837
2886
  ...this._config,
2838
2887
  mode: "single",
@@ -2997,8 +3046,8 @@ var MooDateRangePicker = class _MooDateRangePicker {
2997
3046
  endValue: config.endValue || this._endInput.value || element.dataset.datepickerEndValue
2998
3047
  });
2999
3048
  this._placeholder = this._config.placeholder;
3000
- this._defaultStartValue = this._startInput.defaultValue || this._startInput.getAttribute("value") || "";
3001
- this._defaultEndValue = this._endInput.defaultValue || this._endInput.getAttribute("value") || "";
3049
+ this._defaultStartValue = formDefaultValue(this._startInput);
3050
+ this._defaultEndValue = formDefaultValue(this._endInput);
3002
3051
  this.calendar = MooCalendar.getOrCreateInstance(this._calendarRoot, this._config);
3003
3052
  rangePickerInstances.set(element, this);
3004
3053
  this._syncFromCalendar();
@@ -3143,6 +3192,13 @@ var MooDateRangePicker = class _MooDateRangePicker {
3143
3192
 
3144
3193
  // src/js/components/sidebar.js
3145
3194
  var instances4 = /* @__PURE__ */ new WeakMap();
3195
+ var FLYOUT_FOCUSABLE_SELECTOR = "a[href], button, input, select, textarea, [tabindex]";
3196
+ function flyoutFocusables(flyout) {
3197
+ const view = flyout.ownerDocument.defaultView;
3198
+ return [...flyout.querySelectorAll(FLYOUT_FOCUSABLE_SELECTOR)].filter(
3199
+ (element) => !element.matches(":disabled") && element.tabIndex >= 0 && !element.closest("[hidden], [inert]") && element.getClientRects().length > 0 && view.getComputedStyle(element).visibility === "visible"
3200
+ );
3201
+ }
3146
3202
  var Sidebar = class _Sidebar {
3147
3203
  static getInstance(element) {
3148
3204
  return element?.nodeType === 1 ? instances4.get(element) || null : null;
@@ -3570,7 +3626,37 @@ var Sidebar = class _Sidebar {
3570
3626
  this._closeFlyouts();
3571
3627
  } else {
3572
3628
  this._openFlyout(item);
3629
+ if (event.detail === 0) {
3630
+ if (this._flyout) {
3631
+ flyoutFocusables(this._flyout)[0]?.focus();
3632
+ }
3633
+ }
3634
+ }
3635
+ }
3636
+ // The flyout lives in a portal, away from its trigger, so Escape and Tab
3637
+ // out of it return to the trigger instead of wandering. Tab between its
3638
+ // items stays native; a key a flyout control already handled is left alone.
3639
+ _handleFlyoutKeydown(event) {
3640
+ const owner = this._flyoutOwner;
3641
+ if (event.defaultPrevented || !this._flyout || !owner || !(event.key === "Escape" || event.key === "Tab")) {
3642
+ return false;
3643
+ }
3644
+ const inFlyout = this._flyout.contains(event.target);
3645
+ if (!inFlyout && !(event.key === "Escape" && owner.contains(event.target))) {
3646
+ return false;
3573
3647
  }
3648
+ if (event.key === "Tab") {
3649
+ const items = flyoutFocusables(this._flyout);
3650
+ const edge = event.shiftKey ? items[0] : items[items.length - 1];
3651
+ if (edge && event.target !== edge) {
3652
+ return false;
3653
+ }
3654
+ }
3655
+ event.preventDefault();
3656
+ const trigger = owner.querySelector(":scope > .sidebar-menu-sub-trigger");
3657
+ this._closeFlyouts();
3658
+ trigger?.focus();
3659
+ return true;
3574
3660
  }
3575
3661
  _handleOutsideFlyoutClick(event) {
3576
3662
  if (!this._flyout) {
@@ -3583,6 +3669,9 @@ var Sidebar = class _Sidebar {
3583
3669
  }
3584
3670
  }
3585
3671
  _handleShortcut(event) {
3672
+ if (this._handleFlyoutKeydown(event)) {
3673
+ return;
3674
+ }
3586
3675
  const target = event.target;
3587
3676
  const isEditable = target instanceof this._window.Element && (target.matches("input, textarea, select") || target.isContentEditable);
3588
3677
  if (event.defaultPrevented || event.isComposing || isEditable || !this._config.keyboard || !(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "b" || !this._isDesktop()) {
@@ -4508,6 +4597,241 @@ var FloatingPanel = class _FloatingPanel {
4508
4597
  }
4509
4598
  };
4510
4599
 
4600
+ // src/js/components/image-gallery.js
4601
+ var instances9 = /* @__PURE__ */ new WeakMap();
4602
+ var SLIDESHOW_INTERVAL = 5e3;
4603
+ var RESIZE_DURATION = 250;
4604
+ var ImageGallery = class _ImageGallery {
4605
+ static getInstance(element) {
4606
+ return element?.nodeType === 1 ? instances9.get(element) || null : null;
4607
+ }
4608
+ static getOrCreateInstance(element) {
4609
+ return _ImageGallery.getInstance(element) || new _ImageGallery(element);
4610
+ }
4611
+ constructor(element) {
4612
+ if (element?.nodeType !== 1 || !element.matches("[data-image-gallery]")) {
4613
+ throw new TypeError("ImageGallery requires a [data-image-gallery] root element.");
4614
+ }
4615
+ const existing = instances9.get(element);
4616
+ if (existing) return existing;
4617
+ this._element = element;
4618
+ this._window = element.ownerDocument.defaultView;
4619
+ this._dialog = element.querySelector(".modal");
4620
+ this._carousel = this._dialog?.querySelector(".carousel");
4621
+ if (!this._dialog || !this._carousel) {
4622
+ throw new TypeError("ImageGallery requires a lightbox Dialog with a Carousel.");
4623
+ }
4624
+ this._slides = Array.from(this._carousel.querySelectorAll(".carousel-item"));
4625
+ this._counter = this._dialog.querySelector("[data-gallery-counter]");
4626
+ this._caption = this._dialog.querySelector("[data-gallery-caption]");
4627
+ this._credit = this._dialog.querySelector("[data-gallery-credit]");
4628
+ this._stage = this._dialog.querySelector("[data-gallery-stage]");
4629
+ this._frame = this._dialog.querySelector(".modal-dialog");
4630
+ this._progress = this._dialog.querySelector(".progress");
4631
+ this._progressBar = this._progress?.querySelector(".progress-bar");
4632
+ this._progress?.setAttribute("aria-hidden", "true");
4633
+ const controls = (suffix) => this._dialog.id ? element.ownerDocument.getElementById(`${this._dialog.id}-${suffix}`) : null;
4634
+ this._enterButton = controls("fullscreen");
4635
+ this._exitButton = controls("windowed");
4636
+ this._playButton = controls("play");
4637
+ this._pauseButton = controls("pause");
4638
+ this._listeners = [];
4639
+ this._listen(this._dialog, "show.bs.modal", (event) => this._onShow(event));
4640
+ this._listen(this._window, "resize", () => this._layoutStage());
4641
+ this._listen(this._dialog, "shown.bs.modal", () => this._carouselInstance());
4642
+ this._listen(this._carousel, "slide.bs.carousel", (event) => this.show(event.to, false));
4643
+ this._listen(this._carousel, "slid.bs.carousel", () => this._restartTimer());
4644
+ this._listen(this._dialog, "keydown", (event) => this._onKeydown(event));
4645
+ this._listen(this._dialog, "hidden.bs.modal", () => {
4646
+ this.play(false);
4647
+ this.fullscreen(false);
4648
+ });
4649
+ const actions = [
4650
+ [controls("previous"), () => this._step(false)],
4651
+ [controls("next"), () => this._step(true)],
4652
+ [this._playButton, () => this.play(true, true)],
4653
+ [this._pauseButton, () => this.play(false, true)],
4654
+ [this._enterButton, () => this.fullscreen(true, true)],
4655
+ [this._exitButton, () => this.fullscreen(false, true)]
4656
+ ];
4657
+ for (const [button, action] of actions) {
4658
+ if (button) this._listen(button, "click", action);
4659
+ }
4660
+ instances9.set(element, this);
4661
+ }
4662
+ get index() {
4663
+ const active = this._slides.findIndex((slide) => slide.classList.contains("active"));
4664
+ return Math.max(active, 0);
4665
+ }
4666
+ /** Marks photo `index` current, loads it and its neighbours. */
4667
+ show(index, activate = true) {
4668
+ const count = this._slides.length;
4669
+ if (!count) return this;
4670
+ const current = (Number(index) % count + count) % count;
4671
+ if (activate) {
4672
+ this._slides.forEach((slide2, position) => {
4673
+ slide2.classList.toggle("active", position === current);
4674
+ });
4675
+ }
4676
+ for (const offset of [0, 1, -1]) {
4677
+ this._load(this._slides[(current + offset + count) % count]);
4678
+ }
4679
+ const slide = this._slides[current];
4680
+ if (this._caption) {
4681
+ this._caption.textContent = slide.getAttribute("data-gallery-slide-title") || slide.querySelector("img")?.alt || "";
4682
+ }
4683
+ if (this._credit) {
4684
+ const credit = slide.getAttribute("data-gallery-slide-credit") || "";
4685
+ this._credit.textContent = credit;
4686
+ this._credit.classList.toggle("d-none", !credit);
4687
+ }
4688
+ if (this._counter) {
4689
+ const template = this._counter.getAttribute("data-gallery-counter") || "{current} / {total}";
4690
+ this._counter.textContent = template.replaceAll("{current}", String(current + 1)).replaceAll("{total}", String(count));
4691
+ }
4692
+ return this;
4693
+ }
4694
+ /** Starts or stops the slideshow. */
4695
+ play(on = true, moveFocus = false) {
4696
+ this._playing = Boolean(on) && Boolean(this._carouselInstance());
4697
+ this._progress?.classList.toggle("d-none", !this._playing);
4698
+ this._restartTimer();
4699
+ this._playButton?.classList.toggle("d-none", this._playing);
4700
+ this._pauseButton?.classList.toggle("d-none", !this._playing);
4701
+ this._counter?.setAttribute("aria-live", this._playing ? "off" : "polite");
4702
+ if (moveFocus) (this._playing ? this._pauseButton : this._playButton)?.focus();
4703
+ return this;
4704
+ }
4705
+ get isFullscreen() {
4706
+ return Boolean(this._frame?.classList.contains("modal-fullscreen"));
4707
+ }
4708
+ /** Switches between the large dialog and full screen. */
4709
+ fullscreen(on = true, moveFocus = false) {
4710
+ if (!this._frame) return this;
4711
+ const value = Boolean(on);
4712
+ this._resize(() => {
4713
+ this._frame.classList.toggle("modal-fullscreen", value);
4714
+ this._layoutStage();
4715
+ });
4716
+ if (moveFocus) (value ? this._exitButton : this._enterButton)?.focus();
4717
+ return this;
4718
+ }
4719
+ dispose() {
4720
+ this._stopTimer();
4721
+ for (const [target, type, listener] of this._listeners) {
4722
+ target.removeEventListener(type, listener);
4723
+ }
4724
+ this._listeners = [];
4725
+ instances9.delete(this._element);
4726
+ }
4727
+ _listen(target, type, listener) {
4728
+ target.addEventListener(type, listener);
4729
+ this._listeners.push([target, type, listener]);
4730
+ }
4731
+ // The dialog fills the screen after the full-screen button, or below the
4732
+ // breakpoint of Bootstrap's .modal-fullscreen-{breakpoint}-down.
4733
+ _fillsScreen() {
4734
+ if (this.isFullscreen) return true;
4735
+ const breakpoint = this._frame?.className.match(/\bmodal-fullscreen-(\w+)-down\b/)?.[1];
4736
+ if (!breakpoint) return false;
4737
+ const width = Number.parseFloat(this._window.getComputedStyle(this._element.ownerDocument.documentElement).getPropertyValue(`--bs-breakpoint-${breakpoint}`));
4738
+ return Number.isFinite(width) && this._window.matchMedia(`(max-width: ${width - 0.02}px)`).matches;
4739
+ }
4740
+ // Runs one photo's time on the Progress bar, then shows the next photo.
4741
+ _restartTimer() {
4742
+ this._stopTimer();
4743
+ if (!this._playing) return;
4744
+ const advance = () => {
4745
+ if (this._playing) this._step(true);
4746
+ };
4747
+ if (typeof this._progressBar?.animate === "function") {
4748
+ const timer = this._progressBar.animate(
4749
+ [{ width: "0%" }, { width: "100%" }],
4750
+ { duration: SLIDESHOW_INTERVAL, easing: "linear", fill: "forwards" }
4751
+ );
4752
+ timer.addEventListener("finish", () => {
4753
+ if (this._timer === timer) advance();
4754
+ });
4755
+ this._timer = timer;
4756
+ } else {
4757
+ this._timeout = this._window.setTimeout(advance, SLIDESHOW_INTERVAL);
4758
+ }
4759
+ }
4760
+ _stopTimer() {
4761
+ this._timer?.cancel();
4762
+ this._timer = null;
4763
+ this._window.clearTimeout(this._timeout);
4764
+ }
4765
+ // Animates the dialog from its old box to the new one: the content keeps
4766
+ // its final layout and starts scaled to where it was (first, last, invert,
4767
+ // play). Without motion the change applies at once.
4768
+ _resize(change) {
4769
+ const content = this._frame.querySelector(".modal-content");
4770
+ const reduced = this._window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
4771
+ if (!content || typeof content.animate !== "function" || reduced || !this._dialog.classList.contains("show")) {
4772
+ change();
4773
+ return;
4774
+ }
4775
+ this._resizing?.cancel();
4776
+ const first = content.getBoundingClientRect();
4777
+ change();
4778
+ const last = content.getBoundingClientRect();
4779
+ if (!first.width || !first.height || !last.width || !last.height) return;
4780
+ this._resizing = content.animate([
4781
+ {
4782
+ transformOrigin: "0 0",
4783
+ transform: `translate(${first.left - last.left}px, ${first.top - last.top}px) scale(${first.width / last.width}, ${first.height / last.height})`
4784
+ },
4785
+ { transformOrigin: "0 0", transform: "none" }
4786
+ ], { duration: RESIZE_DURATION, easing: "cubic-bezier(0.2, 0, 0, 1)" });
4787
+ }
4788
+ _layoutStage() {
4789
+ const fill = this._fillsScreen();
4790
+ this._stage?.classList.toggle("gallery-stage", !fill);
4791
+ this._enterButton?.classList.toggle("d-none", fill);
4792
+ this._exitButton?.classList.toggle("d-none", !this.isFullscreen);
4793
+ }
4794
+ _onShow(event) {
4795
+ this._layoutStage();
4796
+ const trigger = event.relatedTarget;
4797
+ const value = trigger?.closest?.("[data-gallery-index]")?.getAttribute("data-gallery-index");
4798
+ this.show(value === null || value === void 0 ? 0 : Number.parseInt(value, 10) || 0);
4799
+ }
4800
+ _onKeydown(event) {
4801
+ if (!["ArrowLeft", "ArrowRight"].includes(event.key)) return;
4802
+ if (this._carousel.contains(event.target)) return;
4803
+ event.preventDefault();
4804
+ const rtl = this._element.ownerDocument.documentElement.dir === "rtl";
4805
+ this._step(event.key === "ArrowRight" !== rtl);
4806
+ }
4807
+ _step(forward) {
4808
+ const carousel = this._carouselInstance();
4809
+ if (!carousel) return;
4810
+ if (forward) carousel.next();
4811
+ else carousel.prev();
4812
+ }
4813
+ _carouselInstance(create = true) {
4814
+ const Carousel = this._window.bootstrap?.Carousel;
4815
+ if (!Carousel) return null;
4816
+ if (!create) return Carousel.getInstance(this._carousel);
4817
+ return Carousel.getOrCreateInstance(this._carousel, {
4818
+ interval: false,
4819
+ pause: false,
4820
+ ride: false,
4821
+ keyboard: true,
4822
+ touch: true,
4823
+ wrap: true
4824
+ });
4825
+ }
4826
+ _load(slide) {
4827
+ const image = slide?.querySelector("img[data-gallery-src]");
4828
+ if (!image || image.hasAttribute("src")) return;
4829
+ const srcset = image.getAttribute("data-gallery-srcset");
4830
+ if (srcset) image.setAttribute("srcset", srcset);
4831
+ image.setAttribute("src", image.getAttribute("data-gallery-src"));
4832
+ }
4833
+ };
4834
+
4511
4835
  // src/js/moo-ui.js
4512
4836
  import PasswordRequirements from "./password-requirements.js";
4513
4837
  async function loadChart() {
@@ -4517,6 +4841,7 @@ async function loadChart() {
4517
4841
  var MooUI = {
4518
4842
  PasswordRequirements,
4519
4843
  FloatingPanel,
4844
+ ImageGallery,
4520
4845
  loadChart,
4521
4846
  Combobox,
4522
4847
  ContextMenu,
@@ -4537,6 +4862,7 @@ export {
4537
4862
  DataTable,
4538
4863
  MooDatepicker as Datepicker,
4539
4864
  FloatingPanel,
4865
+ ImageGallery,
4540
4866
  MooCalendar,
4541
4867
  MooDateRangePicker,
4542
4868
  OverlayScrollbar,