@unabridged/midwest 0.17.0 → 0.19.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 (45) hide show
  1. package/README.md +4 -2
  2. package/app/assets/javascript/midwest/index.ts +10 -0
  3. package/app/assets/javascript/midwest.js +733 -603
  4. package/app/assets/javascript/midwest.js.map +1 -1
  5. package/app/assets/stylesheets/midwest/form-group.css +4 -0
  6. package/app/assets/stylesheets/midwest.css +1 -1
  7. package/app/assets/stylesheets/midwest.tailwind.css +7 -1
  8. package/dist/css/midwest.css +1 -1
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +11 -5
  10. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  11. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +106 -6
  12. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
  13. package/dist/javascript/collection/app/components/midwest/color_picker_component/color_picker_component_controller.js +135 -0
  14. package/dist/javascript/collection/app/components/midwest/color_picker_component/color_picker_component_controller.js.map +1 -0
  15. package/dist/javascript/collection/app/components/midwest/countdown_timer_component/countdown_timer_component_controller.js +27 -6
  16. package/dist/javascript/collection/app/components/midwest/countdown_timer_component/countdown_timer_component_controller.js.map +1 -1
  17. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +1 -1
  18. package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js +109 -21
  19. package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js.map +1 -1
  20. package/dist/javascript/collection/app/components/midwest/form/color_picker_component/color_picker_component_controller.js +129 -0
  21. package/dist/javascript/collection/app/components/midwest/form/color_picker_component/color_picker_component_controller.js.map +1 -0
  22. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js +165 -1
  23. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js.map +1 -1
  24. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +38 -5
  25. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  26. package/dist/javascript/collection/app/components/midwest/horizontal_scroll_component/horizontal_scroll_component_controller.js +24 -0
  27. package/dist/javascript/collection/app/components/midwest/horizontal_scroll_component/horizontal_scroll_component_controller.js.map +1 -0
  28. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js +60 -0
  29. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js.map +1 -0
  30. package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js +60 -0
  31. package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js.map +1 -0
  32. package/dist/javascript/collection/app/components/midwest/table_component/table_component_controller.js +30 -0
  33. package/dist/javascript/collection/app/components/midwest/table_component/table_component_controller.js.map +1 -0
  34. package/dist/javascript/collection/node_modules/@floating-ui/core/dist/floating-ui.core.js +561 -0
  35. package/dist/javascript/collection/node_modules/@floating-ui/core/dist/floating-ui.core.js.map +1 -0
  36. package/dist/javascript/collection/node_modules/@floating-ui/dom/dist/floating-ui.dom.js +736 -0
  37. package/dist/javascript/collection/node_modules/@floating-ui/dom/dist/floating-ui.dom.js.map +1 -0
  38. package/dist/javascript/collection/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.js +166 -0
  39. package/dist/javascript/collection/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.js.map +1 -0
  40. package/dist/javascript/collection/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +105 -0
  41. package/dist/javascript/collection/node_modules/@floating-ui/utils/dist/floating-ui.utils.js.map +1 -0
  42. package/dist/javascript/midwest.d.ts +5 -0
  43. package/dist/javascript/midwest.js +733 -603
  44. package/dist/javascript/midwest.js.map +1 -1
  45. package/package.json +3 -4
@@ -548,14 +548,48 @@ class Banner extends Controller {
548
548
  }
549
549
  }
550
550
 
551
+ const FORMATTERS = {
552
+ phone: (v) => {
553
+ const d = v.replace(/\D/g, "").slice(0, 10);
554
+ if (d.length <= 3)
555
+ return d;
556
+ if (d.length <= 6)
557
+ return `(${d.slice(0, 3)}) ${d.slice(3)}`;
558
+ return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`;
559
+ },
560
+ credit_card: (v) => {
561
+ const d = v.replace(/\D/g, "").slice(0, 16);
562
+ return d.match(/.{1,4}/g)?.join(" ") ?? d;
563
+ },
564
+ zip: (v) => {
565
+ const d = v.replace(/\D/g, "");
566
+ if (d.length <= 5)
567
+ return d;
568
+ return `${d.slice(0, 5)}-${d.slice(5, 9)}`;
569
+ }
570
+ };
551
571
  class Input extends Controller {
552
- static targets = ["input", "counter"];
572
+ static targets = ["input", "counter", "capsLockWarning", "matchError"];
573
+ static values = {
574
+ match: { type: String, default: "" },
575
+ autoformat: { type: String, default: "" }
576
+ };
553
577
  updateClearButton = null;
554
578
  updateCounter = null;
579
+ updateCapsLock = null;
580
+ hideCapsLock = null;
581
+ matchListener = null;
582
+ matchInvalidListener = null;
583
+ matchSourceListener = null;
584
+ matchSourceElement = null;
585
+ autoformatListener = null;
555
586
  connect() {
556
587
  this.applyColor();
557
588
  this.applySearchClear();
558
589
  this.applyCounter();
590
+ this.applyPassword();
591
+ this.applyMatch();
592
+ this.applyAutoformat();
559
593
  }
560
594
  disconnect() {
561
595
  if (this.updateClearButton) {
@@ -566,6 +600,56 @@ class Input extends Controller {
566
600
  this.inputTarget.removeEventListener("input", this.updateCounter);
567
601
  this.updateCounter = null;
568
602
  }
603
+ if (this.updateCapsLock) {
604
+ this.inputTarget.removeEventListener("keydown", this.updateCapsLock);
605
+ this.inputTarget.removeEventListener("keyup", this.updateCapsLock);
606
+ this.inputTarget.removeEventListener("mousedown", this.updateCapsLock);
607
+ this.updateCapsLock = null;
608
+ }
609
+ if (this.hideCapsLock) {
610
+ this.inputTarget.removeEventListener("blur", this.hideCapsLock);
611
+ this.hideCapsLock = null;
612
+ }
613
+ if (this.matchListener) {
614
+ this.inputTarget.removeEventListener("input", this.matchListener);
615
+ this.inputTarget.removeEventListener("blur", this.matchListener);
616
+ this.matchListener = null;
617
+ }
618
+ if (this.matchInvalidListener) {
619
+ this.inputTarget.removeEventListener("invalid", this.matchInvalidListener);
620
+ this.matchInvalidListener = null;
621
+ }
622
+ if (this.matchSourceListener && this.matchSourceElement) {
623
+ this.matchSourceElement.removeEventListener("input", this.matchSourceListener);
624
+ this.matchSourceListener = null;
625
+ this.matchSourceElement = null;
626
+ }
627
+ if (this.autoformatListener) {
628
+ this.inputTarget.removeEventListener("blur", this.autoformatListener);
629
+ this.autoformatListener = null;
630
+ }
631
+ }
632
+ increment() {
633
+ const input = this.inputTarget;
634
+ const step = parseFloat(input.step) || 1;
635
+ const max = input.max !== "" ? parseFloat(input.max) : Infinity;
636
+ const newValue = (parseFloat(input.value) || 0) + step;
637
+ if (newValue <= max) {
638
+ input.value = String(newValue);
639
+ input.dispatchEvent(new Event("input", { bubbles: true }));
640
+ input.dispatchEvent(new Event("change", { bubbles: true }));
641
+ }
642
+ }
643
+ decrement() {
644
+ const input = this.inputTarget;
645
+ const step = parseFloat(input.step) || 1;
646
+ const min = input.min !== "" ? parseFloat(input.min) : -Infinity;
647
+ const newValue = (parseFloat(input.value) || 0) - step;
648
+ if (newValue >= min) {
649
+ input.value = String(newValue);
650
+ input.dispatchEvent(new Event("input", { bubbles: true }));
651
+ input.dispatchEvent(new Event("change", { bubbles: true }));
652
+ }
569
653
  }
570
654
  applyColor() {
571
655
  if (this.inputTarget.type !== "color") {
@@ -584,6 +668,20 @@ class Input extends Controller {
584
668
  };
585
669
  this.inputTarget.addEventListener("input", this.updateCounter);
586
670
  }
671
+ applyPassword() {
672
+ if (!this.hasCapsLockWarningTarget)
673
+ return;
674
+ this.updateCapsLock = (e) => {
675
+ this.capsLockWarningTarget.hidden = !e.getModifierState("CapsLock");
676
+ };
677
+ this.hideCapsLock = () => {
678
+ this.capsLockWarningTarget.hidden = true;
679
+ };
680
+ this.inputTarget.addEventListener("keydown", this.updateCapsLock);
681
+ this.inputTarget.addEventListener("keyup", this.updateCapsLock);
682
+ this.inputTarget.addEventListener("mousedown", this.updateCapsLock);
683
+ this.inputTarget.addEventListener("blur", this.hideCapsLock);
684
+ }
587
685
  applySearchClear() {
588
686
  if (this.inputTarget.type !== "search")
589
687
  return;
@@ -624,6 +722,72 @@ class Input extends Controller {
624
722
  this.inputTarget.focus();
625
723
  });
626
724
  }
725
+ // Validates that this field's value matches the value of the element
726
+ // identified by matchValue (a CSS selector). Uses setCustomValidity to
727
+ // block form submission. Shows the error inline via the matchError target
728
+ // rather than the browser's native tooltip (invalid event is suppressed).
729
+ applyMatch() {
730
+ if (!this.matchValue)
731
+ return;
732
+ const showError = () => {
733
+ this.element.classList.add("has-error", "theme-red");
734
+ if (this.hasMatchErrorTarget)
735
+ this.matchErrorTarget.hidden = false;
736
+ };
737
+ const clearError = () => {
738
+ this.element.classList.remove("has-error", "theme-red");
739
+ if (this.hasMatchErrorTarget)
740
+ this.matchErrorTarget.hidden = true;
741
+ };
742
+ const validate = () => {
743
+ const other2 = document.querySelector(this.matchValue);
744
+ if (!this.inputTarget.value) {
745
+ this.inputTarget.setCustomValidity("");
746
+ clearError();
747
+ return;
748
+ }
749
+ if (!other2 || this.inputTarget.value === other2.value) {
750
+ this.inputTarget.setCustomValidity("");
751
+ clearError();
752
+ } else {
753
+ this.inputTarget.setCustomValidity("Values do not match");
754
+ showError();
755
+ }
756
+ };
757
+ this.matchListener = validate;
758
+ this.inputTarget.addEventListener("input", validate);
759
+ this.inputTarget.addEventListener("blur", validate);
760
+ this.matchInvalidListener = (e) => {
761
+ e.preventDefault();
762
+ showError();
763
+ };
764
+ this.inputTarget.addEventListener("invalid", this.matchInvalidListener);
765
+ const other = document.querySelector(this.matchValue);
766
+ if (other) {
767
+ this.matchSourceElement = other;
768
+ this.matchSourceListener = validate;
769
+ other.addEventListener("input", validate);
770
+ }
771
+ }
772
+ // Reformats the input value on blur using a named formatter from FORMATTERS.
773
+ // Dispatches input + change events after reformatting so downstream
774
+ // listeners (counters, form state) stay in sync.
775
+ applyAutoformat() {
776
+ if (!this.autoformatValue)
777
+ return;
778
+ const fmt = FORMATTERS[this.autoformatValue];
779
+ if (!fmt)
780
+ return;
781
+ this.autoformatListener = () => {
782
+ const formatted = fmt(this.inputTarget.value);
783
+ if (formatted !== this.inputTarget.value) {
784
+ this.inputTarget.value = formatted;
785
+ this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }));
786
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
787
+ }
788
+ };
789
+ this.inputTarget.addEventListener("blur", this.autoformatListener);
790
+ }
627
791
  }
628
792
 
629
793
  class File extends Controller {
@@ -749,41 +913,9 @@ function getAlignmentSides(placement, rects, rtl) {
749
913
  }
750
914
  return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)];
751
915
  }
752
- function getExpandedPlacements(placement) {
753
- const oppositePlacement = getOppositePlacement(placement);
754
- return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)];
755
- }
756
916
  function getOppositeAlignmentPlacement(placement) {
757
917
  return placement.includes('start') ? placement.replace('start', 'end') : placement.replace('end', 'start');
758
918
  }
759
- const lrPlacement = ['left', 'right'];
760
- const rlPlacement = ['right', 'left'];
761
- const tbPlacement = ['top', 'bottom'];
762
- const btPlacement = ['bottom', 'top'];
763
- function getSideList(side, isStart, rtl) {
764
- switch (side) {
765
- case 'top':
766
- case 'bottom':
767
- if (rtl) return isStart ? rlPlacement : lrPlacement;
768
- return isStart ? lrPlacement : rlPlacement;
769
- case 'left':
770
- case 'right':
771
- return isStart ? tbPlacement : btPlacement;
772
- default:
773
- return [];
774
- }
775
- }
776
- function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) {
777
- const alignment = getAlignment(placement);
778
- let list = getSideList(getSide(placement), direction === 'start', rtl);
779
- if (alignment) {
780
- list = list.map(side => side + "-" + alignment);
781
- if (flipAlignment) {
782
- list = list.concat(list.map(getOppositeAlignmentPlacement));
783
- }
784
- }
785
- return list;
786
- }
787
919
  function getOppositePlacement(placement) {
788
920
  const side = getSide(placement);
789
921
  return oppositeSideMap[side] + placement.slice(side.length);
@@ -1043,88 +1175,6 @@ const computePosition$1 = async (reference, floating, config) => {
1043
1175
  };
1044
1176
  };
1045
1177
 
1046
- /**
1047
- * Provides data to position an inner element of the floating element so that it
1048
- * appears centered to the reference element.
1049
- * @see https://floating-ui.com/docs/arrow
1050
- */
1051
- const arrow = options => ({
1052
- name: 'arrow',
1053
- options,
1054
- async fn(state) {
1055
- const {
1056
- x,
1057
- y,
1058
- placement,
1059
- rects,
1060
- platform,
1061
- elements,
1062
- middlewareData
1063
- } = state;
1064
- // Since `element` is required, we don't Partial<> the type.
1065
- const {
1066
- element,
1067
- padding = 0
1068
- } = evaluate(options, state) || {};
1069
- if (element == null) {
1070
- return {};
1071
- }
1072
- const paddingObject = getPaddingObject(padding);
1073
- const coords = {
1074
- x,
1075
- y
1076
- };
1077
- const axis = getAlignmentAxis(placement);
1078
- const length = getAxisLength(axis);
1079
- const arrowDimensions = await platform.getDimensions(element);
1080
- const isYAxis = axis === 'y';
1081
- const minProp = isYAxis ? 'top' : 'left';
1082
- const maxProp = isYAxis ? 'bottom' : 'right';
1083
- const clientProp = isYAxis ? 'clientHeight' : 'clientWidth';
1084
- const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length];
1085
- const startDiff = coords[axis] - rects.reference[axis];
1086
- const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element));
1087
- let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0;
1088
-
1089
- // DOM platform can return `window` as the `offsetParent`.
1090
- if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) {
1091
- clientSize = elements.floating[clientProp] || rects.floating[length];
1092
- }
1093
- const centerToReference = endDiff / 2 - startDiff / 2;
1094
-
1095
- // If the padding is large enough that it causes the arrow to no longer be
1096
- // centered, modify the padding so that it is centered.
1097
- const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1;
1098
- const minPadding = min(paddingObject[minProp], largestPossiblePadding);
1099
- const maxPadding = min(paddingObject[maxProp], largestPossiblePadding);
1100
-
1101
- // Make sure the arrow doesn't overflow the floating element if the center
1102
- // point is outside the floating element's bounds.
1103
- const min$1 = minPadding;
1104
- const max = clientSize - arrowDimensions[length] - maxPadding;
1105
- const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
1106
- const offset = clamp(min$1, center, max);
1107
-
1108
- // If the reference is small enough that the arrow's padding causes it to
1109
- // to point to nothing for an aligned placement, adjust the offset of the
1110
- // floating element itself. To ensure `shift()` continues to take action,
1111
- // a single reset is performed when this is true.
1112
- const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;
1113
- const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0;
1114
- return {
1115
- [axis]: coords[axis] + alignmentOffset,
1116
- data: {
1117
- [axis]: offset,
1118
- centerOffset: center - offset - alignmentOffset,
1119
- ...(shouldAddOffset && {
1120
- alignmentOffset
1121
- })
1122
- },
1123
- reset: shouldAddOffset
1124
- };
1125
- }
1126
- });
1127
-
1128
1178
  function getPlacementList(alignment, autoAlignment, allowedPlacements) {
1129
1179
  const allowedPlacementsSortedByAlignment = alignment ? [...allowedPlacements.filter(placement => getAlignment(placement) === alignment), ...allowedPlacements.filter(placement => getAlignment(placement) !== alignment)] : allowedPlacements.filter(placement => getSide(placement) === placement);
1130
1180
  return allowedPlacementsSortedByAlignment.filter(placement => {
@@ -1228,138 +1278,6 @@ const autoPlacement$1 = function (options) {
1228
1278
  };
1229
1279
  };
1230
1280
 
1231
- /**
1232
- * Optimizes the visibility of the floating element by flipping the `placement`
1233
- * in order to keep it in view when the preferred placement(s) will overflow the
1234
- * clipping boundary. Alternative to `autoPlacement`.
1235
- * @see https://floating-ui.com/docs/flip
1236
- */
1237
- const flip = function (options) {
1238
- if (options === void 0) {
1239
- options = {};
1240
- }
1241
- return {
1242
- name: 'flip',
1243
- options,
1244
- async fn(state) {
1245
- var _middlewareData$arrow, _middlewareData$flip;
1246
- const {
1247
- placement,
1248
- middlewareData,
1249
- rects,
1250
- initialPlacement,
1251
- platform,
1252
- elements
1253
- } = state;
1254
- const {
1255
- mainAxis: checkMainAxis = true,
1256
- crossAxis: checkCrossAxis = true,
1257
- fallbackPlacements: specifiedFallbackPlacements,
1258
- fallbackStrategy = 'bestFit',
1259
- fallbackAxisSideDirection = 'none',
1260
- flipAlignment = true,
1261
- ...detectOverflowOptions
1262
- } = evaluate(options, state);
1263
-
1264
- // If a reset by the arrow was caused due to an alignment offset being
1265
- // added, we should skip any logic now since `flip()` has already done its
1266
- // work.
1267
- // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643
1268
- if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
1269
- return {};
1270
- }
1271
- const side = getSide(placement);
1272
- const initialSideAxis = getSideAxis(initialPlacement);
1273
- const isBasePlacement = getSide(initialPlacement) === initialPlacement;
1274
- const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));
1275
- const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement));
1276
- const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none';
1277
- if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) {
1278
- fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl));
1279
- }
1280
- const placements = [initialPlacement, ...fallbackPlacements];
1281
- const overflow = await platform.detectOverflow(state, detectOverflowOptions);
1282
- const overflows = [];
1283
- let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || [];
1284
- if (checkMainAxis) {
1285
- overflows.push(overflow[side]);
1286
- }
1287
- if (checkCrossAxis) {
1288
- const sides = getAlignmentSides(placement, rects, rtl);
1289
- overflows.push(overflow[sides[0]], overflow[sides[1]]);
1290
- }
1291
- overflowsData = [...overflowsData, {
1292
- placement,
1293
- overflows
1294
- }];
1295
-
1296
- // One or more sides is overflowing.
1297
- if (!overflows.every(side => side <= 0)) {
1298
- var _middlewareData$flip2, _overflowsData$filter;
1299
- const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1;
1300
- const nextPlacement = placements[nextIndex];
1301
- if (nextPlacement) {
1302
- const ignoreCrossAxisOverflow = checkCrossAxis === 'alignment' ? initialSideAxis !== getSideAxis(nextPlacement) : false;
1303
- if (!ignoreCrossAxisOverflow ||
1304
- // We leave the current main axis only if every placement on that axis
1305
- // overflows the main axis.
1306
- overflowsData.every(d => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) {
1307
- // Try next placement and re-run the lifecycle.
1308
- return {
1309
- data: {
1310
- index: nextIndex,
1311
- overflows: overflowsData
1312
- },
1313
- reset: {
1314
- placement: nextPlacement
1315
- }
1316
- };
1317
- }
1318
- }
1319
-
1320
- // First, find the candidates that fit on the mainAxis side of overflow,
1321
- // then find the placement that fits the best on the main crossAxis side.
1322
- let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement;
1323
-
1324
- // Otherwise fallback.
1325
- if (!resetPlacement) {
1326
- switch (fallbackStrategy) {
1327
- case 'bestFit':
1328
- {
1329
- var _overflowsData$filter2;
1330
- const placement = (_overflowsData$filter2 = overflowsData.filter(d => {
1331
- if (hasFallbackAxisSideDirection) {
1332
- const currentSideAxis = getSideAxis(d.placement);
1333
- return currentSideAxis === initialSideAxis ||
1334
- // Create a bias to the `y` side axis due to horizontal
1335
- // reading directions favoring greater width.
1336
- currentSideAxis === 'y';
1337
- }
1338
- return true;
1339
- }).map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0];
1340
- if (placement) {
1341
- resetPlacement = placement;
1342
- }
1343
- break;
1344
- }
1345
- case 'initialPlacement':
1346
- resetPlacement = initialPlacement;
1347
- break;
1348
- }
1349
- }
1350
- if (placement !== resetPlacement) {
1351
- return {
1352
- reset: {
1353
- placement: resetPlacement
1354
- }
1355
- };
1356
- }
1357
- }
1358
- return {};
1359
- }
1360
- };
1361
- };
1362
-
1363
1281
  function getSideOffsets(overflow, rect) {
1364
1282
  return {
1365
1283
  top: overflow.top - rect.height,
@@ -1430,137 +1348,6 @@ const hide$1 = function (options) {
1430
1348
  };
1431
1349
  };
1432
1350
 
1433
- function getBoundingRect(rects) {
1434
- const minX = min(...rects.map(rect => rect.left));
1435
- const minY = min(...rects.map(rect => rect.top));
1436
- const maxX = max(...rects.map(rect => rect.right));
1437
- const maxY = max(...rects.map(rect => rect.bottom));
1438
- return {
1439
- x: minX,
1440
- y: minY,
1441
- width: maxX - minX,
1442
- height: maxY - minY
1443
- };
1444
- }
1445
- function getRectsByLine(rects) {
1446
- const sortedRects = rects.slice().sort((a, b) => a.y - b.y);
1447
- const groups = [];
1448
- let prevRect = null;
1449
- for (let i = 0; i < sortedRects.length; i++) {
1450
- const rect = sortedRects[i];
1451
- if (!prevRect || rect.y - prevRect.y > prevRect.height / 2) {
1452
- groups.push([rect]);
1453
- } else {
1454
- groups[groups.length - 1].push(rect);
1455
- }
1456
- prevRect = rect;
1457
- }
1458
- return groups.map(rect => rectToClientRect(getBoundingRect(rect)));
1459
- }
1460
- /**
1461
- * Provides improved positioning for inline reference elements that can span
1462
- * over multiple lines, such as hyperlinks or range selections.
1463
- * @see https://floating-ui.com/docs/inline
1464
- */
1465
- const inline = function (options) {
1466
- if (options === void 0) {
1467
- options = {};
1468
- }
1469
- return {
1470
- name: 'inline',
1471
- options,
1472
- async fn(state) {
1473
- const {
1474
- placement,
1475
- elements,
1476
- rects,
1477
- platform,
1478
- strategy
1479
- } = state;
1480
- // A MouseEvent's client{X,Y} coords can be up to 2 pixels off a
1481
- // ClientRect's bounds, despite the event listener being triggered. A
1482
- // padding of 2 seems to handle this issue.
1483
- const {
1484
- padding = 2,
1485
- x,
1486
- y
1487
- } = evaluate(options, state);
1488
- const nativeClientRects = Array.from((await (platform.getClientRects == null ? void 0 : platform.getClientRects(elements.reference))) || []);
1489
- const clientRects = getRectsByLine(nativeClientRects);
1490
- const fallback = rectToClientRect(getBoundingRect(nativeClientRects));
1491
- const paddingObject = getPaddingObject(padding);
1492
- function getBoundingClientRect() {
1493
- // There are two rects and they are disjoined.
1494
- if (clientRects.length === 2 && clientRects[0].left > clientRects[1].right && x != null && y != null) {
1495
- // Find the first rect in which the point is fully inside.
1496
- return clientRects.find(rect => x > rect.left - paddingObject.left && x < rect.right + paddingObject.right && y > rect.top - paddingObject.top && y < rect.bottom + paddingObject.bottom) || fallback;
1497
- }
1498
-
1499
- // There are 2 or more connected rects.
1500
- if (clientRects.length >= 2) {
1501
- if (getSideAxis(placement) === 'y') {
1502
- const firstRect = clientRects[0];
1503
- const lastRect = clientRects[clientRects.length - 1];
1504
- const isTop = getSide(placement) === 'top';
1505
- const top = firstRect.top;
1506
- const bottom = lastRect.bottom;
1507
- const left = isTop ? firstRect.left : lastRect.left;
1508
- const right = isTop ? firstRect.right : lastRect.right;
1509
- const width = right - left;
1510
- const height = bottom - top;
1511
- return {
1512
- top,
1513
- bottom,
1514
- left,
1515
- right,
1516
- width,
1517
- height,
1518
- x: left,
1519
- y: top
1520
- };
1521
- }
1522
- const isLeftSide = getSide(placement) === 'left';
1523
- const maxRight = max(...clientRects.map(rect => rect.right));
1524
- const minLeft = min(...clientRects.map(rect => rect.left));
1525
- const measureRects = clientRects.filter(rect => isLeftSide ? rect.left === minLeft : rect.right === maxRight);
1526
- const top = measureRects[0].top;
1527
- const bottom = measureRects[measureRects.length - 1].bottom;
1528
- const left = minLeft;
1529
- const right = maxRight;
1530
- const width = right - left;
1531
- const height = bottom - top;
1532
- return {
1533
- top,
1534
- bottom,
1535
- left,
1536
- right,
1537
- width,
1538
- height,
1539
- x: left,
1540
- y: top
1541
- };
1542
- }
1543
- return fallback;
1544
- }
1545
- const resetRects = await platform.getElementRects({
1546
- reference: {
1547
- getBoundingClientRect
1548
- },
1549
- floating: elements.floating,
1550
- strategy
1551
- });
1552
- if (rects.reference.x !== resetRects.reference.x || rects.reference.y !== resetRects.reference.y || rects.reference.width !== resetRects.reference.width || rects.reference.height !== resetRects.reference.height) {
1553
- return {
1554
- reset: {
1555
- rects: resetRects
1556
- }
1557
- };
1558
- }
1559
- return {};
1560
- }
1561
- };
1562
- };
1563
-
1564
1351
  const originSides = /*#__PURE__*/new Set(['left', 'top']);
1565
1352
 
1566
1353
  // For type backwards-compatibility, the `OffsetOptions` type was also
@@ -1725,158 +1512,6 @@ const shift$1 = function (options) {
1725
1512
  }
1726
1513
  };
1727
1514
  };
1728
- /**
1729
- * Built-in `limiter` that will stop `shift()` at a certain point.
1730
- */
1731
- const limitShift = function (options) {
1732
- if (options === void 0) {
1733
- options = {};
1734
- }
1735
- return {
1736
- options,
1737
- fn(state) {
1738
- const {
1739
- x,
1740
- y,
1741
- placement,
1742
- rects,
1743
- middlewareData
1744
- } = state;
1745
- const {
1746
- offset = 0,
1747
- mainAxis: checkMainAxis = true,
1748
- crossAxis: checkCrossAxis = true
1749
- } = evaluate(options, state);
1750
- const coords = {
1751
- x,
1752
- y
1753
- };
1754
- const crossAxis = getSideAxis(placement);
1755
- const mainAxis = getOppositeAxis(crossAxis);
1756
- let mainAxisCoord = coords[mainAxis];
1757
- let crossAxisCoord = coords[crossAxis];
1758
- const rawOffset = evaluate(offset, state);
1759
- const computedOffset = typeof rawOffset === 'number' ? {
1760
- mainAxis: rawOffset,
1761
- crossAxis: 0
1762
- } : {
1763
- mainAxis: 0,
1764
- crossAxis: 0,
1765
- ...rawOffset
1766
- };
1767
- if (checkMainAxis) {
1768
- const len = mainAxis === 'y' ? 'height' : 'width';
1769
- const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis;
1770
- const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis;
1771
- if (mainAxisCoord < limitMin) {
1772
- mainAxisCoord = limitMin;
1773
- } else if (mainAxisCoord > limitMax) {
1774
- mainAxisCoord = limitMax;
1775
- }
1776
- }
1777
- if (checkCrossAxis) {
1778
- var _middlewareData$offse, _middlewareData$offse2;
1779
- const len = mainAxis === 'y' ? 'width' : 'height';
1780
- const isOriginSide = originSides.has(getSide(placement));
1781
- const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis);
1782
- const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0);
1783
- if (crossAxisCoord < limitMin) {
1784
- crossAxisCoord = limitMin;
1785
- } else if (crossAxisCoord > limitMax) {
1786
- crossAxisCoord = limitMax;
1787
- }
1788
- }
1789
- return {
1790
- [mainAxis]: mainAxisCoord,
1791
- [crossAxis]: crossAxisCoord
1792
- };
1793
- }
1794
- };
1795
- };
1796
-
1797
- /**
1798
- * Provides data that allows you to change the size of the floating element —
1799
- * for instance, prevent it from overflowing the clipping boundary or match the
1800
- * width of the reference element.
1801
- * @see https://floating-ui.com/docs/size
1802
- */
1803
- const size = function (options) {
1804
- if (options === void 0) {
1805
- options = {};
1806
- }
1807
- return {
1808
- name: 'size',
1809
- options,
1810
- async fn(state) {
1811
- var _state$middlewareData, _state$middlewareData2;
1812
- const {
1813
- placement,
1814
- rects,
1815
- platform,
1816
- elements
1817
- } = state;
1818
- const {
1819
- apply = () => {},
1820
- ...detectOverflowOptions
1821
- } = evaluate(options, state);
1822
- const overflow = await platform.detectOverflow(state, detectOverflowOptions);
1823
- const side = getSide(placement);
1824
- const alignment = getAlignment(placement);
1825
- const isYAxis = getSideAxis(placement) === 'y';
1826
- const {
1827
- width,
1828
- height
1829
- } = rects.floating;
1830
- let heightSide;
1831
- let widthSide;
1832
- if (side === 'top' || side === 'bottom') {
1833
- heightSide = side;
1834
- widthSide = alignment === ((await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))) ? 'start' : 'end') ? 'left' : 'right';
1835
- } else {
1836
- widthSide = side;
1837
- heightSide = alignment === 'end' ? 'top' : 'bottom';
1838
- }
1839
- const maximumClippingHeight = height - overflow.top - overflow.bottom;
1840
- const maximumClippingWidth = width - overflow.left - overflow.right;
1841
- const overflowAvailableHeight = min(height - overflow[heightSide], maximumClippingHeight);
1842
- const overflowAvailableWidth = min(width - overflow[widthSide], maximumClippingWidth);
1843
- const noShift = !state.middlewareData.shift;
1844
- let availableHeight = overflowAvailableHeight;
1845
- let availableWidth = overflowAvailableWidth;
1846
- if ((_state$middlewareData = state.middlewareData.shift) != null && _state$middlewareData.enabled.x) {
1847
- availableWidth = maximumClippingWidth;
1848
- }
1849
- if ((_state$middlewareData2 = state.middlewareData.shift) != null && _state$middlewareData2.enabled.y) {
1850
- availableHeight = maximumClippingHeight;
1851
- }
1852
- if (noShift && !alignment) {
1853
- const xMin = max(overflow.left, 0);
1854
- const xMax = max(overflow.right, 0);
1855
- const yMin = max(overflow.top, 0);
1856
- const yMax = max(overflow.bottom, 0);
1857
- if (isYAxis) {
1858
- availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right));
1859
- } else {
1860
- availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom));
1861
- }
1862
- }
1863
- await apply({
1864
- ...state,
1865
- availableWidth,
1866
- availableHeight
1867
- });
1868
- const nextDimensions = await platform.getDimensions(elements.floating);
1869
- if (width !== nextDimensions.width || height !== nextDimensions.height) {
1870
- return {
1871
- reset: {
1872
- rects: true
1873
- }
1874
- };
1875
- }
1876
- return {};
1877
- }
1878
- };
1879
- };
1880
1515
 
1881
1516
  function hasWindow() {
1882
1517
  return typeof window !== 'undefined';
@@ -2742,22 +2377,6 @@ const autoPlacement = autoPlacement$1;
2742
2377
  */
2743
2378
  const shift = shift$1;
2744
2379
 
2745
- /**
2746
- * Optimizes the visibility of the floating element by flipping the `placement`
2747
- * in order to keep it in view when the preferred placement(s) will overflow the
2748
- * clipping boundary. Alternative to `autoPlacement`.
2749
- * @see https://floating-ui.com/docs/flip
2750
- */
2751
- flip;
2752
-
2753
- /**
2754
- * Provides data that allows you to change the size of the floating element —
2755
- * for instance, prevent it from overflowing the clipping boundary or match the
2756
- * width of the reference element.
2757
- * @see https://floating-ui.com/docs/size
2758
- */
2759
- size;
2760
-
2761
2380
  /**
2762
2381
  * Provides data to hide the floating element in applicable situations, such as
2763
2382
  * when it is not in the same clipping context as the reference element.
@@ -2765,25 +2384,6 @@ size;
2765
2384
  */
2766
2385
  const hide = hide$1;
2767
2386
 
2768
- /**
2769
- * Provides data to position an inner element of the floating element so that it
2770
- * appears centered to the reference element.
2771
- * @see https://floating-ui.com/docs/arrow
2772
- */
2773
- arrow;
2774
-
2775
- /**
2776
- * Provides improved positioning for inline reference elements that can span
2777
- * over multiple lines, such as hyperlinks or range selections.
2778
- * @see https://floating-ui.com/docs/inline
2779
- */
2780
- inline;
2781
-
2782
- /**
2783
- * Built-in `limiter` that will stop `shift()` at a certain point.
2784
- */
2785
- limitShift;
2786
-
2787
2387
  /**
2788
2388
  * Computes the `x` and `y` coordinates that will place the floating element
2789
2389
  * next to a given reference element.
@@ -3241,11 +2841,12 @@ const NON_SUBMITTING_INPUT_TYPES = /* @__PURE__ */ new Set([
3241
2841
  ]);
3242
2842
  const FORM_ELEMENT_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
3243
2843
  class Carousel extends Controller {
3244
- static targets = ["track", "prev", "next", "dot"];
2844
+ static targets = ["track", "prev", "next", "dot", "status"];
3245
2845
  static values = {
3246
2846
  wizard: { type: Boolean, default: false },
3247
2847
  autoplay: { type: Number, default: 0 },
3248
- loop: { type: Boolean, default: false }
2848
+ loop: { type: Boolean, default: false },
2849
+ fade: { type: Boolean, default: false }
3249
2850
  };
3250
2851
  observer = null;
3251
2852
  _currentIndex = 0;
@@ -3253,7 +2854,11 @@ class Carousel extends Controller {
3253
2854
  _lastTouchX = 0;
3254
2855
  _autoplayTimer = null;
3255
2856
  connect() {
3256
- this.setupObserver();
2857
+ if (this.fadeValue) {
2858
+ this.initFade();
2859
+ } else {
2860
+ this.setupObserver();
2861
+ }
3257
2862
  this.updateNavigation();
3258
2863
  this.element.addEventListener("keydown", this.handleArrowKeys);
3259
2864
  if (this.wizardValue) {
@@ -3267,6 +2872,8 @@ class Carousel extends Controller {
3267
2872
  this.startAutoplay();
3268
2873
  this.element.addEventListener("mouseenter", this.pauseAutoplay);
3269
2874
  this.element.addEventListener("mouseleave", this.resumeAutoplay);
2875
+ this.element.addEventListener("focusin", this.pauseAutoplay);
2876
+ this.element.addEventListener("focusout", this.resumeAutoplay);
3270
2877
  }
3271
2878
  }
3272
2879
  disconnect() {
@@ -3275,12 +2882,20 @@ class Carousel extends Controller {
3275
2882
  this.element.removeEventListener("keydown", this.handleKeydown);
3276
2883
  this.element.removeEventListener("mouseenter", this.pauseAutoplay);
3277
2884
  this.element.removeEventListener("mouseleave", this.resumeAutoplay);
2885
+ this.element.removeEventListener("focusin", this.pauseAutoplay);
2886
+ this.element.removeEventListener("focusout", this.resumeAutoplay);
3278
2887
  this.trackTarget.removeEventListener("wheel", this.handleWheel);
3279
2888
  this.trackTarget.removeEventListener("touchstart", this.handleTouchStart);
3280
2889
  this.trackTarget.removeEventListener("touchmove", this.handleTouchMove);
3281
2890
  this.stopAutoplay();
3282
2891
  }
3283
2892
  next() {
2893
+ if (this.fadeValue) {
2894
+ const atEnd2 = this._currentIndex >= this.slides.length - 1;
2895
+ const nextIndex = this.loopValue && atEnd2 ? 0 : Math.min(this._currentIndex + 1, this.slides.length - 1);
2896
+ this.activateSlide(nextIndex);
2897
+ return;
2898
+ }
3284
2899
  const atEnd = this._currentIndex >= this.slides.length - 1;
3285
2900
  if (this.loopValue && atEnd) {
3286
2901
  this.slides[0]?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
@@ -3289,6 +2904,12 @@ class Carousel extends Controller {
3289
2904
  }
3290
2905
  }
3291
2906
  previous() {
2907
+ if (this.fadeValue) {
2908
+ const atStart2 = this._currentIndex <= 0;
2909
+ const prevIndex = this.loopValue && atStart2 ? this.slides.length - 1 : Math.max(this._currentIndex - 1, 0);
2910
+ this.activateSlide(prevIndex);
2911
+ return;
2912
+ }
3292
2913
  const atStart = this._currentIndex <= 0;
3293
2914
  if (this.loopValue && atStart) {
3294
2915
  const last = this.slides[this.slides.length - 1];
@@ -3300,6 +2921,10 @@ class Carousel extends Controller {
3300
2921
  goto(event) {
3301
2922
  const button = event.currentTarget;
3302
2923
  const index = parseInt(button.dataset.index ?? "0", 10);
2924
+ if (this.fadeValue) {
2925
+ this.activateSlide(index);
2926
+ return;
2927
+ }
3303
2928
  const slide = this.slides[index];
3304
2929
  slide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
3305
2930
  }
@@ -3319,10 +2944,13 @@ class Carousel extends Controller {
3319
2944
  this.next();
3320
2945
  }
3321
2946
  scrolled() {
2947
+ if (this.fadeValue)
2948
+ return;
3322
2949
  if (this.wizardValue)
3323
2950
  this.clampToFrontier();
3324
2951
  this.updateCurrentIndex();
3325
2952
  this.updateNavigation();
2953
+ this.announceSlide();
3326
2954
  }
3327
2955
  // Arrow function so `this` is stable for add/removeEventListener pairing.
3328
2956
  // Handles ArrowLeft/ArrowRight for keyboard slide navigation. Skips form
@@ -3395,6 +3023,11 @@ class Carousel extends Controller {
3395
3023
  this._autoplayTimer = null;
3396
3024
  }
3397
3025
  autoplayAdvance() {
3026
+ if (this.fadeValue) {
3027
+ const nextIndex = (this._currentIndex + 1) % this.slides.length;
3028
+ this.activateSlide(nextIndex);
3029
+ return;
3030
+ }
3398
3031
  const atEnd = this._currentIndex >= this.slides.length - 1;
3399
3032
  if (atEnd) {
3400
3033
  const firstSlide = this.slides[0];
@@ -3406,6 +3039,53 @@ class Carousel extends Controller {
3406
3039
  get slides() {
3407
3040
  return Array.from(this.trackTarget.children);
3408
3041
  }
3042
+ // Sets up the fade mode initial state. Marks the first slide active, hides
3043
+ // all others from the accessibility tree, and adds .fade-ready so CSS hands
3044
+ // control over to .is-active.
3045
+ initFade() {
3046
+ this.slides.forEach((slide, i) => {
3047
+ const active = i === 0;
3048
+ slide.classList.toggle("is-active", active);
3049
+ slide.toggleAttribute("inert", !active);
3050
+ slide.setAttribute("aria-hidden", String(!active));
3051
+ });
3052
+ this.element.classList.add("fade-ready");
3053
+ this.syncPagerDots();
3054
+ }
3055
+ // Transitions to the slide at `index` in fade mode by swapping .is-active and
3056
+ // hiding non-active slides from the accessibility tree.
3057
+ activateSlide(index) {
3058
+ this.slides.forEach((slide, i) => {
3059
+ const active = i === index;
3060
+ slide.classList.toggle("is-active", active);
3061
+ slide.toggleAttribute("inert", !active);
3062
+ slide.setAttribute("aria-hidden", String(!active));
3063
+ });
3064
+ this._currentIndex = index;
3065
+ this.updateNavigation();
3066
+ this.syncPagerDots();
3067
+ this.announceSlide();
3068
+ }
3069
+ // Syncs pager dot active state from _currentIndex. Used in fade mode where
3070
+ // IntersectionObserver is not set up (the track doesn't scroll).
3071
+ syncPagerDots() {
3072
+ if (!this.hasDotTarget)
3073
+ return;
3074
+ this.dotTargets.forEach((dot, i) => {
3075
+ dot.classList.toggle("active", i === this._currentIndex);
3076
+ if (i === this._currentIndex) {
3077
+ dot.setAttribute("aria-current", "true");
3078
+ } else {
3079
+ dot.removeAttribute("aria-current");
3080
+ }
3081
+ });
3082
+ }
3083
+ // Updates the sr-only live region so screen readers announce the new slide.
3084
+ announceSlide() {
3085
+ if (!this.hasStatusTarget)
3086
+ return;
3087
+ this.statusTarget.textContent = `Slide ${this._currentIndex + 1} of ${this.slides.length}`;
3088
+ }
3409
3089
  // Snaps scroll position back to the frontier if the user somehow scrolled
3410
3090
  // past it (e.g. momentum after a partially-blocked gesture).
3411
3091
  clampToFrontier() {
@@ -3458,15 +3138,35 @@ class Carousel extends Controller {
3458
3138
  this.slides.forEach((slide) => this.observer?.observe(slide));
3459
3139
  }
3460
3140
  updateNavigation() {
3141
+ if (this.fadeValue) {
3142
+ const atStart2 = this._currentIndex <= 0;
3143
+ const atEnd2 = this._currentIndex >= this.slides.length - 1;
3144
+ if (this.hasPrevTarget) {
3145
+ const hide = atStart2 && !this.loopValue;
3146
+ this.prevTarget.classList.toggle("hide", hide);
3147
+ this.prevTarget.disabled = hide;
3148
+ }
3149
+ if (this.hasNextTarget) {
3150
+ const hide = atEnd2 && !this.loopValue;
3151
+ this.nextTarget.classList.toggle("hide", hide);
3152
+ this.nextTarget.disabled = hide;
3153
+ }
3154
+ return;
3155
+ }
3461
3156
  const track = this.trackTarget;
3462
3157
  const atStart = track.scrollLeft <= 0;
3463
3158
  const atEnd = track.scrollLeft >= track.scrollWidth - track.clientWidth - 1;
3464
- if (this.hasPrevTarget)
3465
- this.prevTarget.classList.toggle("hide", atStart && !this.loopValue);
3159
+ if (this.hasPrevTarget) {
3160
+ const hide = atStart && !this.loopValue;
3161
+ this.prevTarget.classList.toggle("hide", hide);
3162
+ this.prevTarget.disabled = hide;
3163
+ }
3466
3164
  if (this.hasNextTarget) {
3467
3165
  const atLastSlide = this._currentIndex >= this.slides.length - 1;
3468
3166
  const frontierReached = this.wizardValue && this._currentIndex >= this._maxUnlocked;
3469
- this.nextTarget.classList.toggle("hide", (atEnd || atLastSlide || frontierReached) && !this.loopValue);
3167
+ const hide = (atEnd || atLastSlide || frontierReached) && !this.loopValue;
3168
+ this.nextTarget.classList.toggle("hide", hide);
3169
+ this.nextTarget.disabled = hide;
3470
3170
  }
3471
3171
  if (this.wizardValue && this.hasDotTarget) {
3472
3172
  this.dotTargets.forEach((dot, i) => {
@@ -3492,11 +3192,13 @@ class FormLiveSummary extends Controller {
3492
3192
  this.form?.addEventListener("input", this.render);
3493
3193
  this.form?.addEventListener("change", this.render);
3494
3194
  this.form?.addEventListener("midwest-autocomplete:change", this.render);
3195
+ this.form?.addEventListener("midwest-color-picker:update", this.render);
3495
3196
  }
3496
3197
  disconnect() {
3497
3198
  this.form?.removeEventListener("input", this.render);
3498
3199
  this.form?.removeEventListener("change", this.render);
3499
3200
  this.form?.removeEventListener("midwest-autocomplete:change", this.render);
3201
+ this.form?.removeEventListener("midwest-color-picker:update", this.render);
3500
3202
  }
3501
3203
  get form() {
3502
3204
  return this.element.closest("form");
@@ -3514,15 +3216,18 @@ class FormLiveSummary extends Controller {
3514
3216
  }
3515
3217
  const dl = document.createElement("dl");
3516
3218
  dl.className = "midwest-form-live-summary-list";
3517
- fields.forEach(({ label, value }) => {
3219
+ fields.forEach(({ label, value, swatchColor }) => {
3518
3220
  const dt = Object.assign(document.createElement("dt"), {
3519
3221
  className: "midwest-form-live-summary-term",
3520
3222
  textContent: label
3521
3223
  });
3522
- const dd = Object.assign(document.createElement("dd"), {
3523
- className: "midwest-form-live-summary-definition",
3524
- textContent: value || "\u2014"
3525
- });
3224
+ const dd = document.createElement("dd");
3225
+ dd.className = "midwest-form-live-summary-definition";
3226
+ if (swatchColor !== void 0) {
3227
+ dd.append(this.buildSwatch(swatchColor), value || "\u2014");
3228
+ } else {
3229
+ dd.textContent = value || "\u2014";
3230
+ }
3526
3231
  dl.append(dt, dd);
3527
3232
  });
3528
3233
  this.listTarget.replaceChildren(dl);
@@ -3579,6 +3284,18 @@ class FormLiveSummary extends Controller {
3579
3284
  value: element.value
3580
3285
  });
3581
3286
  });
3287
+ form.querySelectorAll('[data-midwest-color-picker-target="input"]').forEach((input) => {
3288
+ if (this.element.contains(input))
3289
+ return;
3290
+ if (!input.name)
3291
+ return;
3292
+ const picker = input.closest(".midwest-color-picker");
3293
+ const labelEl = picker?.querySelector(".midwest-form-group__top label") ?? this.form?.querySelector(`label[for="${CSS.escape(input.id)}"]`);
3294
+ const label = labelEl?.textContent?.trim() || this.prettifyName(input.name.replace(/\[.*\]$/, ""));
3295
+ const value = input.value;
3296
+ const display = value.startsWith("#") ? value : value ? value.charAt(0).toUpperCase() + value.slice(1) : "\u2014";
3297
+ fields.push({ label, value: display, swatchColor: value || void 0 });
3298
+ });
3582
3299
  form.querySelectorAll(".midwest-autocomplete").forEach((ac) => {
3583
3300
  if (this.element.contains(ac))
3584
3301
  return;
@@ -3591,6 +3308,22 @@ class FormLiveSummary extends Controller {
3591
3308
  });
3592
3309
  return fields;
3593
3310
  }
3311
+ // Builds a small inline colour swatch to display alongside a colour value.
3312
+ // Hex values are applied as an inline background-color; named theme colours
3313
+ // use a scoped theme-{color} class so the CSS variable cascade does the work.
3314
+ // An empty/transparent value renders a diagonal-stripe placeholder.
3315
+ buildSwatch(color) {
3316
+ const swatch = document.createElement("span");
3317
+ swatch.className = "midwest-form-live-summary-swatch";
3318
+ if (color.startsWith("#")) {
3319
+ swatch.style.backgroundColor = color;
3320
+ } else if (color && color !== "transparent") {
3321
+ swatch.classList.add(`theme-${color}`, "bg-theme-6");
3322
+ } else {
3323
+ swatch.classList.add("is-transparent");
3324
+ }
3325
+ return swatch;
3326
+ }
3594
3327
  // Finds the label associated with a specific element via `for` attribute,
3595
3328
  // wrapping label, or falls back to a prettified version of the element name.
3596
3329
  labelFor(el) {
@@ -3656,16 +3389,37 @@ class CountdownTimer extends Controller {
3656
3389
  seconds: { type: Number, default: 5 }
3657
3390
  };
3658
3391
  timer = null;
3392
+ remaining = 0;
3393
+ startTime = 0;
3659
3394
  connect() {
3660
- this.timer = setTimeout(() => {
3661
- this.dispatch("complete", { bubbles: true });
3662
- }, this.secondsValue * 1e3);
3395
+ this.remaining = this.secondsValue * 1e3;
3396
+ this.resume();
3663
3397
  }
3664
3398
  disconnect() {
3665
- if (this.timer !== null) {
3666
- clearTimeout(this.timer);
3399
+ this.pause();
3400
+ }
3401
+ pause() {
3402
+ if (this.timer === null)
3403
+ return;
3404
+ clearTimeout(this.timer);
3405
+ this.timer = null;
3406
+ this.remaining = Math.max(0, this.remaining - (Date.now() - this.startTime));
3407
+ this.setPlayState("paused");
3408
+ }
3409
+ resume() {
3410
+ if (this.timer !== null)
3411
+ return;
3412
+ this.startTime = Date.now();
3413
+ this.timer = setTimeout(() => {
3667
3414
  this.timer = null;
3668
- }
3415
+ this.dispatch("complete", { bubbles: true });
3416
+ }, this.remaining);
3417
+ this.setPlayState("running");
3418
+ }
3419
+ setPlayState(state) {
3420
+ const progress = this.element.querySelector(".midwest-countdown-timer__progress");
3421
+ if (progress)
3422
+ progress.style.animationPlayState = state;
3669
3423
  }
3670
3424
  }
3671
3425
 
@@ -3858,19 +3612,30 @@ class Rating extends Controller {
3858
3612
  }
3859
3613
 
3860
3614
  class Autocomplete extends Controller {
3861
- static targets = ["input", "dropdown", "list", "tokens", "multipleTokenTemplate", "singleTokenTemplate", "status", "turboList", "loader"];
3615
+ static targets = [
3616
+ "input",
3617
+ "dropdown",
3618
+ "list",
3619
+ "tokens",
3620
+ "multipleTokenTemplate",
3621
+ "singleTokenTemplate",
3622
+ "status",
3623
+ "turboList",
3624
+ "loader",
3625
+ "freeFormHintList",
3626
+ "freeFormHintValue"
3627
+ ];
3862
3628
  static values = {
3863
3629
  options: { type: Array, default: [] },
3864
3630
  url: { type: String, default: "" },
3865
3631
  multiple: { type: Boolean, default: true },
3866
- turboFrameUrl: { type: String, default: "" }
3632
+ turboFrameUrl: { type: String, default: "" },
3633
+ freeForm: { type: Boolean, default: false }
3867
3634
  };
3868
3635
  highlightedIndex = -1;
3869
3636
  fetchAbort = null;
3870
3637
  searchTimer;
3871
3638
  // ── Named handlers ─────────────────────────────────────────────────────────
3872
- // Arrow-function class properties preserve `this` and produce a stable
3873
- // reference so addEventListener/removeEventListener pairs always match.
3874
3639
  onFrameLoad = () => {
3875
3640
  this.syncSelectedState();
3876
3641
  this.loading = false;
@@ -3905,9 +3670,6 @@ class Autocomplete extends Controller {
3905
3670
  clearTimeout(this.searchTimer);
3906
3671
  this.element.removeEventListener("focusout", this.onFocusOut);
3907
3672
  }
3908
- // Target callbacks — Stimulus calls these automatically when targets connect
3909
- // or disconnect, including after the initial connect(). This replaces manual
3910
- // hasTurboListTarget guards and addEventListener calls inside connect().
3911
3673
  dropdownTargetConnected(el) {
3912
3674
  el.addEventListener("toggle", this.onToggle);
3913
3675
  }
@@ -3932,13 +3694,27 @@ class Autocomplete extends Controller {
3932
3694
  this.open();
3933
3695
  }
3934
3696
  open() {
3697
+ if (!this.hasDropdownTarget)
3698
+ return;
3935
3699
  if (this.dropdownTarget.matches(":popover-open"))
3936
3700
  return;
3701
+ const query = this.inputTarget.value.trim();
3702
+ if (this.freeFormValue && !this.hasListTarget && !this.hasTurboListTarget) {
3703
+ if (query) {
3704
+ this.updateFreeFormHint(query);
3705
+ this.showDropdown();
3706
+ }
3707
+ return;
3708
+ }
3937
3709
  clearTimeout(this.searchTimer);
3938
- this.resolve(this.inputTarget.value.trim());
3710
+ this.resolve(query);
3939
3711
  }
3940
3712
  search() {
3941
3713
  const query = this.inputTarget.value.trim();
3714
+ if (this.freeFormValue)
3715
+ this.updateFreeFormHint(query);
3716
+ if (!this.hasListTarget && !this.hasTurboListTarget)
3717
+ return;
3942
3718
  if (!this.hasTurboListTarget && !this.urlValue) {
3943
3719
  this.renderOptions(this.filteredOptions(query));
3944
3720
  return;
@@ -3953,6 +3729,8 @@ class Autocomplete extends Controller {
3953
3729
  switch (event.key) {
3954
3730
  case "ArrowDown": {
3955
3731
  event.preventDefault();
3732
+ if (!this.hasDropdownTarget)
3733
+ break;
3956
3734
  if (!this.dropdownTarget.matches(":popover-open"))
3957
3735
  this.open();
3958
3736
  const items = this.visibleOptions();
@@ -3962,7 +3740,7 @@ class Autocomplete extends Controller {
3962
3740
  }
3963
3741
  case "ArrowUp": {
3964
3742
  event.preventDefault();
3965
- if (!this.dropdownTarget.matches(":popover-open"))
3743
+ if (!this.hasDropdownTarget || !this.dropdownTarget.matches(":popover-open"))
3966
3744
  break;
3967
3745
  const items = this.visibleOptions();
3968
3746
  this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
@@ -3971,11 +3749,22 @@ class Autocomplete extends Controller {
3971
3749
  }
3972
3750
  case "Enter": {
3973
3751
  event.preventDefault();
3974
- if (!this.dropdownTarget.matches(":popover-open"))
3975
- break;
3976
- const items = this.visibleOptions();
3977
- if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
3978
- this.pickItem(items[this.highlightedIndex]);
3752
+ if (this.hasDropdownTarget && this.dropdownTarget.matches(":popover-open") && (this.hasListTarget || this.hasTurboListTarget)) {
3753
+ const items = this.visibleOptions();
3754
+ if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
3755
+ this.pickItem(items[this.highlightedIndex]);
3756
+ break;
3757
+ }
3758
+ }
3759
+ if (this.freeFormValue) {
3760
+ this.commitFreeFormTag();
3761
+ }
3762
+ break;
3763
+ }
3764
+ case ",": {
3765
+ if (this.freeFormValue && this.inputTarget.value.trim()) {
3766
+ event.preventDefault();
3767
+ this.commitFreeFormTag();
3979
3768
  }
3980
3769
  break;
3981
3770
  }
@@ -3992,6 +3781,22 @@ class Autocomplete extends Controller {
3992
3781
  break;
3993
3782
  }
3994
3783
  }
3784
+ // Splits pasted text on commas and newlines into individual tags (free-form mode only).
3785
+ pasteTag(event) {
3786
+ if (!this.freeFormValue)
3787
+ return;
3788
+ const text = event.clipboardData?.getData("text") ?? "";
3789
+ const tags = text.split(/[\n,]+/).map((t) => t.trim()).filter(Boolean);
3790
+ if (tags.length === 0)
3791
+ return;
3792
+ event.preventDefault();
3793
+ tags.forEach((tag) => this.addFreeFormTag(tag));
3794
+ }
3795
+ // Mousedown on the "Hit Enter to add 'X'" hint row.
3796
+ addFreeFormTagFromHint(event) {
3797
+ event.preventDefault();
3798
+ this.commitFreeFormTag();
3799
+ }
3995
3800
  removeToken(event) {
3996
3801
  event.stopPropagation();
3997
3802
  const btn = event.currentTarget;
@@ -3999,9 +3804,6 @@ class Autocomplete extends Controller {
3999
3804
  if (value)
4000
3805
  this.deselect(value);
4001
3806
  }
4002
- // Public action used by turbo-frame option items via data-action.
4003
- // Each server-rendered <li> should have:
4004
- // data-action="mousedown->midwest-autocomplete#selectOption"
4005
3807
  selectOption(event) {
4006
3808
  event.preventDefault();
4007
3809
  this.pickItem(event.currentTarget);
@@ -4013,7 +3815,6 @@ class Autocomplete extends Controller {
4013
3815
  isSelected(value) {
4014
3816
  return !!this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
4015
3817
  }
4016
- // Routes to the correct data source. Shared by open() and the debounced search path.
4017
3818
  resolve(query) {
4018
3819
  if (this.hasTurboListTarget) {
4019
3820
  this.navigateFrame(query);
@@ -4042,9 +3843,6 @@ class Autocomplete extends Controller {
4042
3843
  this.turboListTarget.setAttribute("src", url.toString());
4043
3844
  this.showDropdown();
4044
3845
  }
4045
- // After a turbo frame loads, mark already-selected items and update the live region.
4046
- // The empty state element is always present in the frame and CSS shows/hides it
4047
- // based on whether any non-selected options exist.
4048
3846
  syncSelectedState() {
4049
3847
  const options = this.turboListTarget.querySelectorAll(".midwest-autocomplete-option");
4050
3848
  options.forEach((option) => {
@@ -4080,6 +3878,7 @@ class Autocomplete extends Controller {
4080
3878
  select(option) {
4081
3879
  this.appendToken(option);
4082
3880
  this.hideDropdown();
3881
+ this.hideFreeFormHint();
4083
3882
  this.inputTarget.value = "";
4084
3883
  this.inputTarget.focus();
4085
3884
  this.announce(`${option.label} selected`);
@@ -4151,11 +3950,19 @@ class Autocomplete extends Controller {
4151
3950
  this.renderEmpty("No more options");
4152
3951
  return;
4153
3952
  }
3953
+ this.hideFreeFormHint();
4154
3954
  this.loading = false;
4155
3955
  this.announce(`${availableCount} option${availableCount === 1 ? "" : "s"} available`);
4156
3956
  this.showDropdown();
4157
3957
  }
4158
3958
  renderEmpty(message = "No results") {
3959
+ if (this.freeFormValue && this.inputTarget.value.trim()) {
3960
+ if (this.hasListTarget)
3961
+ this.listTarget.replaceChildren();
3962
+ this.loading = false;
3963
+ this.updateFreeFormHint(this.inputTarget.value.trim());
3964
+ return;
3965
+ }
4159
3966
  const li = document.createElement("li");
4160
3967
  li.className = "midwest-autocomplete-empty";
4161
3968
  li.setAttribute("aria-hidden", "true");
@@ -4165,15 +3972,39 @@ class Autocomplete extends Controller {
4165
3972
  this.announce(message);
4166
3973
  this.showDropdown();
4167
3974
  }
3975
+ // Shows or hides the "Hit Enter to add 'X'" hint based on the current query.
3976
+ updateFreeFormHint(query) {
3977
+ if (!this.hasFreeFormHintListTarget)
3978
+ return;
3979
+ if (query && !this.isSelected(query)) {
3980
+ if (this.hasFreeFormHintValueTarget) {
3981
+ this.freeFormHintValueTarget.textContent = `"${query}"`;
3982
+ }
3983
+ this.freeFormHintListTarget.hidden = false;
3984
+ this.showDropdown();
3985
+ } else {
3986
+ this.hideFreeFormHint();
3987
+ if (!this.hasListTarget && !this.hasTurboListTarget)
3988
+ this.hideDropdown();
3989
+ }
3990
+ }
3991
+ hideFreeFormHint() {
3992
+ if (this.hasFreeFormHintListTarget)
3993
+ this.freeFormHintListTarget.hidden = true;
3994
+ }
4168
3995
  set loading(on) {
4169
3996
  if (this.hasLoaderTarget)
4170
3997
  this.loaderTarget.hidden = !on;
4171
3998
  }
4172
3999
  showDropdown() {
4000
+ if (!this.hasDropdownTarget)
4001
+ return;
4173
4002
  if (!this.dropdownTarget.matches(":popover-open"))
4174
4003
  this.dropdownTarget.showPopover();
4175
4004
  }
4176
4005
  hideDropdown() {
4006
+ if (!this.hasDropdownTarget)
4007
+ return;
4177
4008
  if (this.dropdownTarget.matches(":popover-open"))
4178
4009
  this.dropdownTarget.hidePopover();
4179
4010
  }
@@ -4197,6 +4028,17 @@ class Autocomplete extends Controller {
4197
4028
  const support = item.querySelector(".midwest-autocomplete-option-support")?.textContent?.trim();
4198
4029
  this.select({ value, label, support });
4199
4030
  }
4031
+ commitFreeFormTag() {
4032
+ const value = this.inputTarget.value.trim();
4033
+ this.addFreeFormTag(value);
4034
+ }
4035
+ addFreeFormTag(value) {
4036
+ if (!value)
4037
+ return;
4038
+ if (this.isSelected(value))
4039
+ return;
4040
+ this.select({ value, label: value });
4041
+ }
4200
4042
  announce(message) {
4201
4043
  if (this.hasStatusTarget)
4202
4044
  this.statusTarget.textContent = message;
@@ -4733,6 +4575,289 @@ class Image extends Controller {
4733
4575
  }
4734
4576
  }
4735
4577
 
4578
+ class ColorPicker extends Controller {
4579
+ static targets = ["input", "swatch", "customSwatch", "colorInput"];
4580
+ onCustomColorChange = null;
4581
+ get scaleTarget() {
4582
+ return this.element.dataset.scaleTarget ?? "theme";
4583
+ }
4584
+ connect() {
4585
+ if (this.hasColorInputTarget) {
4586
+ this.onCustomColorChange = (e) => this.handleCustomColor(e);
4587
+ this.colorInputTarget.addEventListener("input", this.onCustomColorChange);
4588
+ }
4589
+ if (this.hasInputTarget && this.inputTarget.value.startsWith("#")) {
4590
+ this.restoreCustom(this.inputTarget.value);
4591
+ }
4592
+ }
4593
+ disconnect() {
4594
+ if (this.hasColorInputTarget && this.onCustomColorChange) {
4595
+ this.colorInputTarget.removeEventListener("input", this.onCustomColorChange);
4596
+ this.onCustomColorChange = null;
4597
+ }
4598
+ }
4599
+ select(e) {
4600
+ const button = e.currentTarget;
4601
+ const color = button.dataset.color ?? "";
4602
+ this.applySelection(color);
4603
+ this.dispatch("update", { detail: { color, scaleTarget: this.scaleTarget } });
4604
+ }
4605
+ openCustom(e) {
4606
+ e.preventDefault();
4607
+ this.colorInputTarget.click();
4608
+ }
4609
+ handleCustomColor(e) {
4610
+ const hex = e.target.value;
4611
+ if (this.hasCustomSwatchTarget) {
4612
+ this.customSwatchTarget.dataset.color = hex;
4613
+ this.customSwatchTarget.style.background = hex;
4614
+ }
4615
+ const scale = this.generateScale(hex);
4616
+ this.applyScale(scale);
4617
+ this.applySelection(hex);
4618
+ this.dispatch("update", { detail: { color: hex, scale, scaleTarget: this.scaleTarget } });
4619
+ }
4620
+ restoreCustom(hex) {
4621
+ if (this.hasColorInputTarget) {
4622
+ this.colorInputTarget.value = hex;
4623
+ }
4624
+ if (this.hasCustomSwatchTarget) {
4625
+ this.customSwatchTarget.style.background = hex;
4626
+ }
4627
+ const scale = this.generateScale(hex);
4628
+ this.applyScale(scale);
4629
+ }
4630
+ applySelection(color) {
4631
+ if (this.hasInputTarget) {
4632
+ this.inputTarget.value = color;
4633
+ }
4634
+ this.swatchTargets.forEach((swatch) => {
4635
+ const isSelected = swatch.dataset.color === color;
4636
+ swatch.classList.toggle("is-selected", isSelected);
4637
+ swatch.setAttribute("aria-pressed", String(isSelected));
4638
+ });
4639
+ }
4640
+ // Generates 13 RGB channel strings (space-separated, matching --{color}-{n}-channels format)
4641
+ // from a hex color by fixing the hue/saturation and distributing lightness across the scale.
4642
+ generateScale(hex) {
4643
+ const [h, s] = this.hexToHsl(hex);
4644
+ const lightnessSteps = [97, 90, 82, 74, 65, 56, 48, 40, 33, 26, 18, 10, 3];
4645
+ return Object.fromEntries(
4646
+ lightnessSteps.map((l, i) => {
4647
+ const [r, g, b] = this.hslToRgb(h, s, l);
4648
+ return [String(i), `${r} ${g} ${b}`];
4649
+ })
4650
+ );
4651
+ }
4652
+ applyScale(scale) {
4653
+ const el = this.element;
4654
+ Object.entries(scale).forEach(([step, channels]) => {
4655
+ el.style.setProperty(`--${this.scaleTarget}-${step}-channels`, channels);
4656
+ });
4657
+ }
4658
+ hexToHsl(hex) {
4659
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
4660
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
4661
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
4662
+ const max = Math.max(r, g, b);
4663
+ const min = Math.min(r, g, b);
4664
+ const l = (max + min) / 2;
4665
+ if (max === min)
4666
+ return [0, 0, l * 100];
4667
+ const d = max - min;
4668
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
4669
+ const h = max === r ? ((g - b) / d + (g < b ? 6 : 0)) / 6 : max === g ? ((b - r) / d + 2) / 6 : ((r - g) / d + 4) / 6;
4670
+ return [h * 360, s * 100, l * 100];
4671
+ }
4672
+ hslToRgb(h, s, l) {
4673
+ h /= 360;
4674
+ s /= 100;
4675
+ l /= 100;
4676
+ if (s === 0) {
4677
+ const v = Math.round(l * 255);
4678
+ return [v, v, v];
4679
+ }
4680
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
4681
+ const p = 2 * l - q;
4682
+ return [
4683
+ Math.round(this.hue2rgb(p, q, h + 1 / 3) * 255),
4684
+ Math.round(this.hue2rgb(p, q, h) * 255),
4685
+ Math.round(this.hue2rgb(p, q, h - 1 / 3) * 255)
4686
+ ];
4687
+ }
4688
+ hue2rgb(p, q, t) {
4689
+ if (t < 0)
4690
+ t += 1;
4691
+ if (t > 1)
4692
+ t -= 1;
4693
+ if (t < 1 / 6)
4694
+ return p + (q - p) * 6 * t;
4695
+ if (t < 1 / 2)
4696
+ return q;
4697
+ if (t < 2 / 3)
4698
+ return p + (q - p) * (2 / 3 - t) * 6;
4699
+ return p;
4700
+ }
4701
+ }
4702
+
4703
+ class Notification extends Controller {
4704
+ container = null;
4705
+ observer = null;
4706
+ connect() {
4707
+ this.container = this.element.closest("[popover]");
4708
+ this.container?.showPopover?.();
4709
+ requestAnimationFrame(() => {
4710
+ this.element.classList.add("visible");
4711
+ });
4712
+ const frame = this.element.parentElement;
4713
+ if (frame) {
4714
+ this.observer = new MutationObserver(() => this.syncCountdown());
4715
+ this.observer.observe(frame, { childList: true });
4716
+ setTimeout(() => this.syncCountdown());
4717
+ }
4718
+ }
4719
+ disconnect() {
4720
+ this.observer?.disconnect();
4721
+ this.observer = null;
4722
+ if (this.container) {
4723
+ const frame = this.container.querySelector("turbo-frame");
4724
+ if (frame?.children.length === 0 && this.container.matches(":popover-open")) {
4725
+ this.container.hidePopover?.();
4726
+ }
4727
+ this.container = null;
4728
+ }
4729
+ }
4730
+ close() {
4731
+ if (!this.element.classList.contains("animated")) {
4732
+ this.element.remove();
4733
+ return;
4734
+ }
4735
+ this.element.classList.add("closing");
4736
+ setTimeout(() => {
4737
+ this.element.remove();
4738
+ }, 350);
4739
+ }
4740
+ syncCountdown() {
4741
+ const timerEl = this.element.querySelector('[data-controller~="midwest-countdown-timer"]');
4742
+ if (!timerEl)
4743
+ return;
4744
+ const timer = this.application.getControllerForElementAndIdentifier(
4745
+ timerEl,
4746
+ "midwest-countdown-timer"
4747
+ );
4748
+ if (!timer)
4749
+ return;
4750
+ const isTop = this.element === this.element.parentElement?.firstElementChild;
4751
+ if (isTop) {
4752
+ timer.resume();
4753
+ } else {
4754
+ timer.pause();
4755
+ }
4756
+ }
4757
+ }
4758
+
4759
+ class HorizontalScroll extends Controller {
4760
+ resizeObserver = null;
4761
+ connect() {
4762
+ this.updateOverflow();
4763
+ this.element.addEventListener("scroll", this.updateOverflow);
4764
+ this.resizeObserver = new ResizeObserver(this.updateOverflow);
4765
+ this.resizeObserver.observe(this.element);
4766
+ }
4767
+ disconnect() {
4768
+ this.element.removeEventListener("scroll", this.updateOverflow);
4769
+ this.resizeObserver?.disconnect();
4770
+ this.resizeObserver = null;
4771
+ }
4772
+ updateOverflow = () => {
4773
+ const el = this.element;
4774
+ el.classList.toggle("can-scroll-left", el.scrollLeft > 0);
4775
+ el.classList.toggle("can-scroll-right", el.scrollLeft < el.scrollWidth - el.clientWidth - 1);
4776
+ };
4777
+ }
4778
+
4779
+ class Table extends Controller {
4780
+ static targets = ["selectAll", "rowCheckbox"];
4781
+ toggleAll() {
4782
+ const checked = this.selectAllTarget.checked;
4783
+ this.rowCheckboxTargets.forEach((cb) => {
4784
+ cb.checked = checked;
4785
+ });
4786
+ }
4787
+ updateSelectAll() {
4788
+ if (!this.hasSelectAllTarget)
4789
+ return;
4790
+ const total = this.rowCheckboxTargets.length;
4791
+ const checkedCount = this.rowCheckboxTargets.filter((cb) => cb.checked).length;
4792
+ if (checkedCount === 0) {
4793
+ this.selectAllTarget.checked = false;
4794
+ this.selectAllTarget.indeterminate = false;
4795
+ } else if (checkedCount === total) {
4796
+ this.selectAllTarget.checked = true;
4797
+ this.selectAllTarget.indeterminate = false;
4798
+ } else {
4799
+ this.selectAllTarget.checked = false;
4800
+ this.selectAllTarget.indeterminate = true;
4801
+ }
4802
+ }
4803
+ }
4804
+
4805
+ class LoadMore extends Controller {
4806
+ static values = {
4807
+ url: String,
4808
+ tbodyId: String
4809
+ };
4810
+ observer = null;
4811
+ loading = false;
4812
+ connect() {
4813
+ if (!this.urlValue)
4814
+ return;
4815
+ this.observer = new IntersectionObserver(
4816
+ (entries) => {
4817
+ if (entries[0].isIntersecting)
4818
+ this.load();
4819
+ },
4820
+ // Start fetching 300px before the sentinel enters the viewport so rows
4821
+ // appear before the user actually reaches the bottom.
4822
+ { rootMargin: "0px 0px 300px 0px" }
4823
+ );
4824
+ this.observer.observe(this.element);
4825
+ }
4826
+ disconnect() {
4827
+ this.observer?.disconnect();
4828
+ this.observer = null;
4829
+ }
4830
+ async load() {
4831
+ if (this.loading)
4832
+ return;
4833
+ this.loading = true;
4834
+ this.observer?.disconnect();
4835
+ try {
4836
+ const response = await fetch(this.urlValue, {
4837
+ headers: {
4838
+ Accept: "text/html",
4839
+ "X-Requested-With": "XMLHttpRequest"
4840
+ },
4841
+ credentials: "same-origin"
4842
+ });
4843
+ if (!response.ok)
4844
+ return;
4845
+ const html = await response.text();
4846
+ const doc = new DOMParser().parseFromString(html, "text/html");
4847
+ const tbody = document.getElementById(this.tbodyIdValue);
4848
+ const newRows = Array.from(
4849
+ doc.querySelectorAll(".midwest-table-body tr")
4850
+ );
4851
+ if (!tbody || newRows.length === 0)
4852
+ return;
4853
+ this.element.remove();
4854
+ newRows.forEach((row) => tbody.appendChild(document.importNode(row, true)));
4855
+ } catch {
4856
+ this.loading = false;
4857
+ }
4858
+ }
4859
+ }
4860
+
4736
4861
  function registerMidwestControllers(application) {
4737
4862
  application.register("midwest-card", Card);
4738
4863
  application.register("midwest-banner", Banner);
@@ -4755,6 +4880,11 @@ function registerMidwestControllers(application) {
4755
4880
  application.register("midwest-rating", Rating);
4756
4881
  application.register("midwest-autocomplete", Autocomplete);
4757
4882
  application.register("midwest-image", Image);
4883
+ application.register("midwest-color-picker", ColorPicker);
4884
+ application.register("midwest-notification", Notification);
4885
+ application.register("midwest-horizontal-scroll", HorizontalScroll);
4886
+ application.register("midwest-table", Table);
4887
+ application.register("midwest-load-more", LoadMore);
4758
4888
  }
4759
4889
 
4760
4890
  export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };