@unabridged/midwest 0.17.0 → 0.18.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.
- package/app/assets/javascript/midwest/index.ts +4 -0
- package/app/assets/javascript/midwest.js +407 -33
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +4 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +4 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/color_picker_component/color_picker_component_controller.js +135 -0
- package/dist/javascript/collection/app/components/midwest/color_picker_component/color_picker_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/countdown_timer_component/countdown_timer_component_controller.js +27 -6
- package/dist/javascript/collection/app/components/midwest/countdown_timer_component/countdown_timer_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js +109 -21
- package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/form/color_picker_component/color_picker_component_controller.js +129 -0
- package/dist/javascript/collection/app/components/midwest/form/color_picker_component/color_picker_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js +50 -1
- package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +38 -5
- package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js +60 -0
- package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js.map +1 -0
- package/dist/javascript/midwest.d.ts +5 -0
- package/dist/javascript/midwest.js +407 -33
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,8 @@ import Range from '../../../components/midwest/form/range_component/range_compon
|
|
|
19
19
|
import Rating from '../../../components/midwest/rating_component/rating_component_controller'
|
|
20
20
|
import Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller'
|
|
21
21
|
import Image from '../../../components/midwest/image_component/image_component_controller'
|
|
22
|
+
import ColorPicker from '../../../components/midwest/form/color_picker_component/color_picker_component_controller'
|
|
23
|
+
import Notification from '../../../components/midwest/notification_component/notification_component_controller'
|
|
22
24
|
/* IMPORTS */
|
|
23
25
|
|
|
24
26
|
export { Card, Banner, CountdownTimer, Chart }
|
|
@@ -45,5 +47,7 @@ export function registerMidwestControllers (application: any) {
|
|
|
45
47
|
application.register('midwest-rating', Rating)
|
|
46
48
|
application.register('midwest-autocomplete', Autocomplete)
|
|
47
49
|
application.register('midwest-image', Image)
|
|
50
|
+
application.register('midwest-color-picker', ColorPicker)
|
|
51
|
+
application.register('midwest-notification', Notification)
|
|
48
52
|
/* EXPORTS */
|
|
49
53
|
}
|
|
@@ -549,13 +549,16 @@ class Banner extends Controller {
|
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
class Input extends Controller {
|
|
552
|
-
static targets = ["input", "counter"];
|
|
552
|
+
static targets = ["input", "counter", "capsLockWarning"];
|
|
553
553
|
updateClearButton = null;
|
|
554
554
|
updateCounter = null;
|
|
555
|
+
updateCapsLock = null;
|
|
556
|
+
hideCapsLock = null;
|
|
555
557
|
connect() {
|
|
556
558
|
this.applyColor();
|
|
557
559
|
this.applySearchClear();
|
|
558
560
|
this.applyCounter();
|
|
561
|
+
this.applyPassword();
|
|
559
562
|
}
|
|
560
563
|
disconnect() {
|
|
561
564
|
if (this.updateClearButton) {
|
|
@@ -566,6 +569,38 @@ class Input extends Controller {
|
|
|
566
569
|
this.inputTarget.removeEventListener("input", this.updateCounter);
|
|
567
570
|
this.updateCounter = null;
|
|
568
571
|
}
|
|
572
|
+
if (this.updateCapsLock) {
|
|
573
|
+
this.inputTarget.removeEventListener("keydown", this.updateCapsLock);
|
|
574
|
+
this.inputTarget.removeEventListener("keyup", this.updateCapsLock);
|
|
575
|
+
this.inputTarget.removeEventListener("mousedown", this.updateCapsLock);
|
|
576
|
+
this.updateCapsLock = null;
|
|
577
|
+
}
|
|
578
|
+
if (this.hideCapsLock) {
|
|
579
|
+
this.inputTarget.removeEventListener("blur", this.hideCapsLock);
|
|
580
|
+
this.hideCapsLock = null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
increment() {
|
|
584
|
+
const input = this.inputTarget;
|
|
585
|
+
const step = parseFloat(input.step) || 1;
|
|
586
|
+
const max = input.max !== "" ? parseFloat(input.max) : Infinity;
|
|
587
|
+
const newValue = (parseFloat(input.value) || 0) + step;
|
|
588
|
+
if (newValue <= max) {
|
|
589
|
+
input.value = String(newValue);
|
|
590
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
591
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
decrement() {
|
|
595
|
+
const input = this.inputTarget;
|
|
596
|
+
const step = parseFloat(input.step) || 1;
|
|
597
|
+
const min = input.min !== "" ? parseFloat(input.min) : -Infinity;
|
|
598
|
+
const newValue = (parseFloat(input.value) || 0) - step;
|
|
599
|
+
if (newValue >= min) {
|
|
600
|
+
input.value = String(newValue);
|
|
601
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
602
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
603
|
+
}
|
|
569
604
|
}
|
|
570
605
|
applyColor() {
|
|
571
606
|
if (this.inputTarget.type !== "color") {
|
|
@@ -584,6 +619,20 @@ class Input extends Controller {
|
|
|
584
619
|
};
|
|
585
620
|
this.inputTarget.addEventListener("input", this.updateCounter);
|
|
586
621
|
}
|
|
622
|
+
applyPassword() {
|
|
623
|
+
if (!this.hasCapsLockWarningTarget)
|
|
624
|
+
return;
|
|
625
|
+
this.updateCapsLock = (e) => {
|
|
626
|
+
this.capsLockWarningTarget.hidden = !e.getModifierState("CapsLock");
|
|
627
|
+
};
|
|
628
|
+
this.hideCapsLock = () => {
|
|
629
|
+
this.capsLockWarningTarget.hidden = true;
|
|
630
|
+
};
|
|
631
|
+
this.inputTarget.addEventListener("keydown", this.updateCapsLock);
|
|
632
|
+
this.inputTarget.addEventListener("keyup", this.updateCapsLock);
|
|
633
|
+
this.inputTarget.addEventListener("mousedown", this.updateCapsLock);
|
|
634
|
+
this.inputTarget.addEventListener("blur", this.hideCapsLock);
|
|
635
|
+
}
|
|
587
636
|
applySearchClear() {
|
|
588
637
|
if (this.inputTarget.type !== "search")
|
|
589
638
|
return;
|
|
@@ -3492,11 +3541,13 @@ class FormLiveSummary extends Controller {
|
|
|
3492
3541
|
this.form?.addEventListener("input", this.render);
|
|
3493
3542
|
this.form?.addEventListener("change", this.render);
|
|
3494
3543
|
this.form?.addEventListener("midwest-autocomplete:change", this.render);
|
|
3544
|
+
this.form?.addEventListener("midwest-color-picker:update", this.render);
|
|
3495
3545
|
}
|
|
3496
3546
|
disconnect() {
|
|
3497
3547
|
this.form?.removeEventListener("input", this.render);
|
|
3498
3548
|
this.form?.removeEventListener("change", this.render);
|
|
3499
3549
|
this.form?.removeEventListener("midwest-autocomplete:change", this.render);
|
|
3550
|
+
this.form?.removeEventListener("midwest-color-picker:update", this.render);
|
|
3500
3551
|
}
|
|
3501
3552
|
get form() {
|
|
3502
3553
|
return this.element.closest("form");
|
|
@@ -3514,15 +3565,18 @@ class FormLiveSummary extends Controller {
|
|
|
3514
3565
|
}
|
|
3515
3566
|
const dl = document.createElement("dl");
|
|
3516
3567
|
dl.className = "midwest-form-live-summary-list";
|
|
3517
|
-
fields.forEach(({ label, value }) => {
|
|
3568
|
+
fields.forEach(({ label, value, swatchColor }) => {
|
|
3518
3569
|
const dt = Object.assign(document.createElement("dt"), {
|
|
3519
3570
|
className: "midwest-form-live-summary-term",
|
|
3520
3571
|
textContent: label
|
|
3521
3572
|
});
|
|
3522
|
-
const dd =
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3573
|
+
const dd = document.createElement("dd");
|
|
3574
|
+
dd.className = "midwest-form-live-summary-definition";
|
|
3575
|
+
if (swatchColor !== void 0) {
|
|
3576
|
+
dd.append(this.buildSwatch(swatchColor), value || "\u2014");
|
|
3577
|
+
} else {
|
|
3578
|
+
dd.textContent = value || "\u2014";
|
|
3579
|
+
}
|
|
3526
3580
|
dl.append(dt, dd);
|
|
3527
3581
|
});
|
|
3528
3582
|
this.listTarget.replaceChildren(dl);
|
|
@@ -3579,6 +3633,18 @@ class FormLiveSummary extends Controller {
|
|
|
3579
3633
|
value: element.value
|
|
3580
3634
|
});
|
|
3581
3635
|
});
|
|
3636
|
+
form.querySelectorAll('[data-midwest-color-picker-target="input"]').forEach((input) => {
|
|
3637
|
+
if (this.element.contains(input))
|
|
3638
|
+
return;
|
|
3639
|
+
if (!input.name)
|
|
3640
|
+
return;
|
|
3641
|
+
const picker = input.closest(".midwest-color-picker");
|
|
3642
|
+
const labelEl = picker?.querySelector(".midwest-form-group__top label") ?? this.form?.querySelector(`label[for="${CSS.escape(input.id)}"]`);
|
|
3643
|
+
const label = labelEl?.textContent?.trim() || this.prettifyName(input.name.replace(/\[.*\]$/, ""));
|
|
3644
|
+
const value = input.value;
|
|
3645
|
+
const display = value.startsWith("#") ? value : value ? value.charAt(0).toUpperCase() + value.slice(1) : "\u2014";
|
|
3646
|
+
fields.push({ label, value: display, swatchColor: value || void 0 });
|
|
3647
|
+
});
|
|
3582
3648
|
form.querySelectorAll(".midwest-autocomplete").forEach((ac) => {
|
|
3583
3649
|
if (this.element.contains(ac))
|
|
3584
3650
|
return;
|
|
@@ -3591,6 +3657,22 @@ class FormLiveSummary extends Controller {
|
|
|
3591
3657
|
});
|
|
3592
3658
|
return fields;
|
|
3593
3659
|
}
|
|
3660
|
+
// Builds a small inline colour swatch to display alongside a colour value.
|
|
3661
|
+
// Hex values are applied as an inline background-color; named theme colours
|
|
3662
|
+
// use a scoped theme-{color} class so the CSS variable cascade does the work.
|
|
3663
|
+
// An empty/transparent value renders a diagonal-stripe placeholder.
|
|
3664
|
+
buildSwatch(color) {
|
|
3665
|
+
const swatch = document.createElement("span");
|
|
3666
|
+
swatch.className = "midwest-form-live-summary-swatch";
|
|
3667
|
+
if (color.startsWith("#")) {
|
|
3668
|
+
swatch.style.backgroundColor = color;
|
|
3669
|
+
} else if (color && color !== "transparent") {
|
|
3670
|
+
swatch.classList.add(`theme-${color}`, "bg-theme-6");
|
|
3671
|
+
} else {
|
|
3672
|
+
swatch.classList.add("is-transparent");
|
|
3673
|
+
}
|
|
3674
|
+
return swatch;
|
|
3675
|
+
}
|
|
3594
3676
|
// Finds the label associated with a specific element via `for` attribute,
|
|
3595
3677
|
// wrapping label, or falls back to a prettified version of the element name.
|
|
3596
3678
|
labelFor(el) {
|
|
@@ -3656,16 +3738,37 @@ class CountdownTimer extends Controller {
|
|
|
3656
3738
|
seconds: { type: Number, default: 5 }
|
|
3657
3739
|
};
|
|
3658
3740
|
timer = null;
|
|
3741
|
+
remaining = 0;
|
|
3742
|
+
startTime = 0;
|
|
3659
3743
|
connect() {
|
|
3660
|
-
this.
|
|
3661
|
-
|
|
3662
|
-
}, this.secondsValue * 1e3);
|
|
3744
|
+
this.remaining = this.secondsValue * 1e3;
|
|
3745
|
+
this.resume();
|
|
3663
3746
|
}
|
|
3664
3747
|
disconnect() {
|
|
3665
|
-
|
|
3666
|
-
|
|
3748
|
+
this.pause();
|
|
3749
|
+
}
|
|
3750
|
+
pause() {
|
|
3751
|
+
if (this.timer === null)
|
|
3752
|
+
return;
|
|
3753
|
+
clearTimeout(this.timer);
|
|
3754
|
+
this.timer = null;
|
|
3755
|
+
this.remaining = Math.max(0, this.remaining - (Date.now() - this.startTime));
|
|
3756
|
+
this.setPlayState("paused");
|
|
3757
|
+
}
|
|
3758
|
+
resume() {
|
|
3759
|
+
if (this.timer !== null)
|
|
3760
|
+
return;
|
|
3761
|
+
this.startTime = Date.now();
|
|
3762
|
+
this.timer = setTimeout(() => {
|
|
3667
3763
|
this.timer = null;
|
|
3668
|
-
|
|
3764
|
+
this.dispatch("complete", { bubbles: true });
|
|
3765
|
+
}, this.remaining);
|
|
3766
|
+
this.setPlayState("running");
|
|
3767
|
+
}
|
|
3768
|
+
setPlayState(state) {
|
|
3769
|
+
const progress = this.element.querySelector(".midwest-countdown-timer__progress");
|
|
3770
|
+
if (progress)
|
|
3771
|
+
progress.style.animationPlayState = state;
|
|
3669
3772
|
}
|
|
3670
3773
|
}
|
|
3671
3774
|
|
|
@@ -3858,19 +3961,30 @@ class Rating extends Controller {
|
|
|
3858
3961
|
}
|
|
3859
3962
|
|
|
3860
3963
|
class Autocomplete extends Controller {
|
|
3861
|
-
static targets = [
|
|
3964
|
+
static targets = [
|
|
3965
|
+
"input",
|
|
3966
|
+
"dropdown",
|
|
3967
|
+
"list",
|
|
3968
|
+
"tokens",
|
|
3969
|
+
"multipleTokenTemplate",
|
|
3970
|
+
"singleTokenTemplate",
|
|
3971
|
+
"status",
|
|
3972
|
+
"turboList",
|
|
3973
|
+
"loader",
|
|
3974
|
+
"freeFormHintList",
|
|
3975
|
+
"freeFormHintValue"
|
|
3976
|
+
];
|
|
3862
3977
|
static values = {
|
|
3863
3978
|
options: { type: Array, default: [] },
|
|
3864
3979
|
url: { type: String, default: "" },
|
|
3865
3980
|
multiple: { type: Boolean, default: true },
|
|
3866
|
-
turboFrameUrl: { type: String, default: "" }
|
|
3981
|
+
turboFrameUrl: { type: String, default: "" },
|
|
3982
|
+
freeForm: { type: Boolean, default: false }
|
|
3867
3983
|
};
|
|
3868
3984
|
highlightedIndex = -1;
|
|
3869
3985
|
fetchAbort = null;
|
|
3870
3986
|
searchTimer;
|
|
3871
3987
|
// ── Named handlers ─────────────────────────────────────────────────────────
|
|
3872
|
-
// Arrow-function class properties preserve `this` and produce a stable
|
|
3873
|
-
// reference so addEventListener/removeEventListener pairs always match.
|
|
3874
3988
|
onFrameLoad = () => {
|
|
3875
3989
|
this.syncSelectedState();
|
|
3876
3990
|
this.loading = false;
|
|
@@ -3905,9 +4019,6 @@ class Autocomplete extends Controller {
|
|
|
3905
4019
|
clearTimeout(this.searchTimer);
|
|
3906
4020
|
this.element.removeEventListener("focusout", this.onFocusOut);
|
|
3907
4021
|
}
|
|
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
4022
|
dropdownTargetConnected(el) {
|
|
3912
4023
|
el.addEventListener("toggle", this.onToggle);
|
|
3913
4024
|
}
|
|
@@ -3932,13 +4043,27 @@ class Autocomplete extends Controller {
|
|
|
3932
4043
|
this.open();
|
|
3933
4044
|
}
|
|
3934
4045
|
open() {
|
|
4046
|
+
if (!this.hasDropdownTarget)
|
|
4047
|
+
return;
|
|
3935
4048
|
if (this.dropdownTarget.matches(":popover-open"))
|
|
3936
4049
|
return;
|
|
4050
|
+
const query = this.inputTarget.value.trim();
|
|
4051
|
+
if (this.freeFormValue && !this.hasListTarget && !this.hasTurboListTarget) {
|
|
4052
|
+
if (query) {
|
|
4053
|
+
this.updateFreeFormHint(query);
|
|
4054
|
+
this.showDropdown();
|
|
4055
|
+
}
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
3937
4058
|
clearTimeout(this.searchTimer);
|
|
3938
|
-
this.resolve(
|
|
4059
|
+
this.resolve(query);
|
|
3939
4060
|
}
|
|
3940
4061
|
search() {
|
|
3941
4062
|
const query = this.inputTarget.value.trim();
|
|
4063
|
+
if (this.freeFormValue)
|
|
4064
|
+
this.updateFreeFormHint(query);
|
|
4065
|
+
if (!this.hasListTarget && !this.hasTurboListTarget)
|
|
4066
|
+
return;
|
|
3942
4067
|
if (!this.hasTurboListTarget && !this.urlValue) {
|
|
3943
4068
|
this.renderOptions(this.filteredOptions(query));
|
|
3944
4069
|
return;
|
|
@@ -3953,6 +4078,8 @@ class Autocomplete extends Controller {
|
|
|
3953
4078
|
switch (event.key) {
|
|
3954
4079
|
case "ArrowDown": {
|
|
3955
4080
|
event.preventDefault();
|
|
4081
|
+
if (!this.hasDropdownTarget)
|
|
4082
|
+
break;
|
|
3956
4083
|
if (!this.dropdownTarget.matches(":popover-open"))
|
|
3957
4084
|
this.open();
|
|
3958
4085
|
const items = this.visibleOptions();
|
|
@@ -3962,7 +4089,7 @@ class Autocomplete extends Controller {
|
|
|
3962
4089
|
}
|
|
3963
4090
|
case "ArrowUp": {
|
|
3964
4091
|
event.preventDefault();
|
|
3965
|
-
if (!this.dropdownTarget.matches(":popover-open"))
|
|
4092
|
+
if (!this.hasDropdownTarget || !this.dropdownTarget.matches(":popover-open"))
|
|
3966
4093
|
break;
|
|
3967
4094
|
const items = this.visibleOptions();
|
|
3968
4095
|
this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
|
|
@@ -3971,11 +4098,22 @@ class Autocomplete extends Controller {
|
|
|
3971
4098
|
}
|
|
3972
4099
|
case "Enter": {
|
|
3973
4100
|
event.preventDefault();
|
|
3974
|
-
if (
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
4101
|
+
if (this.hasDropdownTarget && this.dropdownTarget.matches(":popover-open") && (this.hasListTarget || this.hasTurboListTarget)) {
|
|
4102
|
+
const items = this.visibleOptions();
|
|
4103
|
+
if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
|
|
4104
|
+
this.pickItem(items[this.highlightedIndex]);
|
|
4105
|
+
break;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
if (this.freeFormValue) {
|
|
4109
|
+
this.commitFreeFormTag();
|
|
4110
|
+
}
|
|
4111
|
+
break;
|
|
4112
|
+
}
|
|
4113
|
+
case ",": {
|
|
4114
|
+
if (this.freeFormValue && this.inputTarget.value.trim()) {
|
|
4115
|
+
event.preventDefault();
|
|
4116
|
+
this.commitFreeFormTag();
|
|
3979
4117
|
}
|
|
3980
4118
|
break;
|
|
3981
4119
|
}
|
|
@@ -3992,6 +4130,22 @@ class Autocomplete extends Controller {
|
|
|
3992
4130
|
break;
|
|
3993
4131
|
}
|
|
3994
4132
|
}
|
|
4133
|
+
// Splits pasted text on commas and newlines into individual tags (free-form mode only).
|
|
4134
|
+
pasteTag(event) {
|
|
4135
|
+
if (!this.freeFormValue)
|
|
4136
|
+
return;
|
|
4137
|
+
const text = event.clipboardData?.getData("text") ?? "";
|
|
4138
|
+
const tags = text.split(/[\n,]+/).map((t) => t.trim()).filter(Boolean);
|
|
4139
|
+
if (tags.length === 0)
|
|
4140
|
+
return;
|
|
4141
|
+
event.preventDefault();
|
|
4142
|
+
tags.forEach((tag) => this.addFreeFormTag(tag));
|
|
4143
|
+
}
|
|
4144
|
+
// Mousedown on the "Hit Enter to add 'X'" hint row.
|
|
4145
|
+
addFreeFormTagFromHint(event) {
|
|
4146
|
+
event.preventDefault();
|
|
4147
|
+
this.commitFreeFormTag();
|
|
4148
|
+
}
|
|
3995
4149
|
removeToken(event) {
|
|
3996
4150
|
event.stopPropagation();
|
|
3997
4151
|
const btn = event.currentTarget;
|
|
@@ -3999,9 +4153,6 @@ class Autocomplete extends Controller {
|
|
|
3999
4153
|
if (value)
|
|
4000
4154
|
this.deselect(value);
|
|
4001
4155
|
}
|
|
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
4156
|
selectOption(event) {
|
|
4006
4157
|
event.preventDefault();
|
|
4007
4158
|
this.pickItem(event.currentTarget);
|
|
@@ -4013,7 +4164,6 @@ class Autocomplete extends Controller {
|
|
|
4013
4164
|
isSelected(value) {
|
|
4014
4165
|
return !!this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
|
|
4015
4166
|
}
|
|
4016
|
-
// Routes to the correct data source. Shared by open() and the debounced search path.
|
|
4017
4167
|
resolve(query) {
|
|
4018
4168
|
if (this.hasTurboListTarget) {
|
|
4019
4169
|
this.navigateFrame(query);
|
|
@@ -4042,9 +4192,6 @@ class Autocomplete extends Controller {
|
|
|
4042
4192
|
this.turboListTarget.setAttribute("src", url.toString());
|
|
4043
4193
|
this.showDropdown();
|
|
4044
4194
|
}
|
|
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
4195
|
syncSelectedState() {
|
|
4049
4196
|
const options = this.turboListTarget.querySelectorAll(".midwest-autocomplete-option");
|
|
4050
4197
|
options.forEach((option) => {
|
|
@@ -4080,6 +4227,7 @@ class Autocomplete extends Controller {
|
|
|
4080
4227
|
select(option) {
|
|
4081
4228
|
this.appendToken(option);
|
|
4082
4229
|
this.hideDropdown();
|
|
4230
|
+
this.hideFreeFormHint();
|
|
4083
4231
|
this.inputTarget.value = "";
|
|
4084
4232
|
this.inputTarget.focus();
|
|
4085
4233
|
this.announce(`${option.label} selected`);
|
|
@@ -4151,11 +4299,19 @@ class Autocomplete extends Controller {
|
|
|
4151
4299
|
this.renderEmpty("No more options");
|
|
4152
4300
|
return;
|
|
4153
4301
|
}
|
|
4302
|
+
this.hideFreeFormHint();
|
|
4154
4303
|
this.loading = false;
|
|
4155
4304
|
this.announce(`${availableCount} option${availableCount === 1 ? "" : "s"} available`);
|
|
4156
4305
|
this.showDropdown();
|
|
4157
4306
|
}
|
|
4158
4307
|
renderEmpty(message = "No results") {
|
|
4308
|
+
if (this.freeFormValue && this.inputTarget.value.trim()) {
|
|
4309
|
+
if (this.hasListTarget)
|
|
4310
|
+
this.listTarget.replaceChildren();
|
|
4311
|
+
this.loading = false;
|
|
4312
|
+
this.updateFreeFormHint(this.inputTarget.value.trim());
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4159
4315
|
const li = document.createElement("li");
|
|
4160
4316
|
li.className = "midwest-autocomplete-empty";
|
|
4161
4317
|
li.setAttribute("aria-hidden", "true");
|
|
@@ -4165,15 +4321,39 @@ class Autocomplete extends Controller {
|
|
|
4165
4321
|
this.announce(message);
|
|
4166
4322
|
this.showDropdown();
|
|
4167
4323
|
}
|
|
4324
|
+
// Shows or hides the "Hit Enter to add 'X'" hint based on the current query.
|
|
4325
|
+
updateFreeFormHint(query) {
|
|
4326
|
+
if (!this.hasFreeFormHintListTarget)
|
|
4327
|
+
return;
|
|
4328
|
+
if (query && !this.isSelected(query)) {
|
|
4329
|
+
if (this.hasFreeFormHintValueTarget) {
|
|
4330
|
+
this.freeFormHintValueTarget.textContent = `"${query}"`;
|
|
4331
|
+
}
|
|
4332
|
+
this.freeFormHintListTarget.hidden = false;
|
|
4333
|
+
this.showDropdown();
|
|
4334
|
+
} else {
|
|
4335
|
+
this.hideFreeFormHint();
|
|
4336
|
+
if (!this.hasListTarget && !this.hasTurboListTarget)
|
|
4337
|
+
this.hideDropdown();
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
hideFreeFormHint() {
|
|
4341
|
+
if (this.hasFreeFormHintListTarget)
|
|
4342
|
+
this.freeFormHintListTarget.hidden = true;
|
|
4343
|
+
}
|
|
4168
4344
|
set loading(on) {
|
|
4169
4345
|
if (this.hasLoaderTarget)
|
|
4170
4346
|
this.loaderTarget.hidden = !on;
|
|
4171
4347
|
}
|
|
4172
4348
|
showDropdown() {
|
|
4349
|
+
if (!this.hasDropdownTarget)
|
|
4350
|
+
return;
|
|
4173
4351
|
if (!this.dropdownTarget.matches(":popover-open"))
|
|
4174
4352
|
this.dropdownTarget.showPopover();
|
|
4175
4353
|
}
|
|
4176
4354
|
hideDropdown() {
|
|
4355
|
+
if (!this.hasDropdownTarget)
|
|
4356
|
+
return;
|
|
4177
4357
|
if (this.dropdownTarget.matches(":popover-open"))
|
|
4178
4358
|
this.dropdownTarget.hidePopover();
|
|
4179
4359
|
}
|
|
@@ -4197,6 +4377,17 @@ class Autocomplete extends Controller {
|
|
|
4197
4377
|
const support = item.querySelector(".midwest-autocomplete-option-support")?.textContent?.trim();
|
|
4198
4378
|
this.select({ value, label, support });
|
|
4199
4379
|
}
|
|
4380
|
+
commitFreeFormTag() {
|
|
4381
|
+
const value = this.inputTarget.value.trim();
|
|
4382
|
+
this.addFreeFormTag(value);
|
|
4383
|
+
}
|
|
4384
|
+
addFreeFormTag(value) {
|
|
4385
|
+
if (!value)
|
|
4386
|
+
return;
|
|
4387
|
+
if (this.isSelected(value))
|
|
4388
|
+
return;
|
|
4389
|
+
this.select({ value, label: value });
|
|
4390
|
+
}
|
|
4200
4391
|
announce(message) {
|
|
4201
4392
|
if (this.hasStatusTarget)
|
|
4202
4393
|
this.statusTarget.textContent = message;
|
|
@@ -4733,6 +4924,187 @@ class Image extends Controller {
|
|
|
4733
4924
|
}
|
|
4734
4925
|
}
|
|
4735
4926
|
|
|
4927
|
+
class ColorPicker extends Controller {
|
|
4928
|
+
static targets = ["input", "swatch", "customSwatch", "colorInput"];
|
|
4929
|
+
onCustomColorChange = null;
|
|
4930
|
+
get scaleTarget() {
|
|
4931
|
+
return this.element.dataset.scaleTarget ?? "theme";
|
|
4932
|
+
}
|
|
4933
|
+
connect() {
|
|
4934
|
+
if (this.hasColorInputTarget) {
|
|
4935
|
+
this.onCustomColorChange = (e) => this.handleCustomColor(e);
|
|
4936
|
+
this.colorInputTarget.addEventListener("input", this.onCustomColorChange);
|
|
4937
|
+
}
|
|
4938
|
+
if (this.hasInputTarget && this.inputTarget.value.startsWith("#")) {
|
|
4939
|
+
this.restoreCustom(this.inputTarget.value);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
disconnect() {
|
|
4943
|
+
if (this.hasColorInputTarget && this.onCustomColorChange) {
|
|
4944
|
+
this.colorInputTarget.removeEventListener("input", this.onCustomColorChange);
|
|
4945
|
+
this.onCustomColorChange = null;
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
select(e) {
|
|
4949
|
+
const button = e.currentTarget;
|
|
4950
|
+
const color = button.dataset.color ?? "";
|
|
4951
|
+
this.applySelection(color);
|
|
4952
|
+
this.dispatch("update", { detail: { color, scaleTarget: this.scaleTarget } });
|
|
4953
|
+
}
|
|
4954
|
+
openCustom(e) {
|
|
4955
|
+
e.preventDefault();
|
|
4956
|
+
this.colorInputTarget.click();
|
|
4957
|
+
}
|
|
4958
|
+
handleCustomColor(e) {
|
|
4959
|
+
const hex = e.target.value;
|
|
4960
|
+
if (this.hasCustomSwatchTarget) {
|
|
4961
|
+
this.customSwatchTarget.dataset.color = hex;
|
|
4962
|
+
this.customSwatchTarget.style.background = hex;
|
|
4963
|
+
}
|
|
4964
|
+
const scale = this.generateScale(hex);
|
|
4965
|
+
this.applyScale(scale);
|
|
4966
|
+
this.applySelection(hex);
|
|
4967
|
+
this.dispatch("update", { detail: { color: hex, scale, scaleTarget: this.scaleTarget } });
|
|
4968
|
+
}
|
|
4969
|
+
restoreCustom(hex) {
|
|
4970
|
+
if (this.hasColorInputTarget) {
|
|
4971
|
+
this.colorInputTarget.value = hex;
|
|
4972
|
+
}
|
|
4973
|
+
if (this.hasCustomSwatchTarget) {
|
|
4974
|
+
this.customSwatchTarget.style.background = hex;
|
|
4975
|
+
}
|
|
4976
|
+
const scale = this.generateScale(hex);
|
|
4977
|
+
this.applyScale(scale);
|
|
4978
|
+
}
|
|
4979
|
+
applySelection(color) {
|
|
4980
|
+
if (this.hasInputTarget) {
|
|
4981
|
+
this.inputTarget.value = color;
|
|
4982
|
+
}
|
|
4983
|
+
this.swatchTargets.forEach((swatch) => {
|
|
4984
|
+
const isSelected = swatch.dataset.color === color;
|
|
4985
|
+
swatch.classList.toggle("is-selected", isSelected);
|
|
4986
|
+
swatch.setAttribute("aria-pressed", String(isSelected));
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
// Generates 13 RGB channel strings (space-separated, matching --{color}-{n}-channels format)
|
|
4990
|
+
// from a hex color by fixing the hue/saturation and distributing lightness across the scale.
|
|
4991
|
+
generateScale(hex) {
|
|
4992
|
+
const [h, s] = this.hexToHsl(hex);
|
|
4993
|
+
const lightnessSteps = [97, 90, 82, 74, 65, 56, 48, 40, 33, 26, 18, 10, 3];
|
|
4994
|
+
return Object.fromEntries(
|
|
4995
|
+
lightnessSteps.map((l, i) => {
|
|
4996
|
+
const [r, g, b] = this.hslToRgb(h, s, l);
|
|
4997
|
+
return [String(i), `${r} ${g} ${b}`];
|
|
4998
|
+
})
|
|
4999
|
+
);
|
|
5000
|
+
}
|
|
5001
|
+
applyScale(scale) {
|
|
5002
|
+
const el = this.element;
|
|
5003
|
+
Object.entries(scale).forEach(([step, channels]) => {
|
|
5004
|
+
el.style.setProperty(`--${this.scaleTarget}-${step}-channels`, channels);
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
5007
|
+
hexToHsl(hex) {
|
|
5008
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
5009
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
5010
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
5011
|
+
const max = Math.max(r, g, b);
|
|
5012
|
+
const min = Math.min(r, g, b);
|
|
5013
|
+
const l = (max + min) / 2;
|
|
5014
|
+
if (max === min)
|
|
5015
|
+
return [0, 0, l * 100];
|
|
5016
|
+
const d = max - min;
|
|
5017
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
5018
|
+
const h = max === r ? ((g - b) / d + (g < b ? 6 : 0)) / 6 : max === g ? ((b - r) / d + 2) / 6 : ((r - g) / d + 4) / 6;
|
|
5019
|
+
return [h * 360, s * 100, l * 100];
|
|
5020
|
+
}
|
|
5021
|
+
hslToRgb(h, s, l) {
|
|
5022
|
+
h /= 360;
|
|
5023
|
+
s /= 100;
|
|
5024
|
+
l /= 100;
|
|
5025
|
+
if (s === 0) {
|
|
5026
|
+
const v = Math.round(l * 255);
|
|
5027
|
+
return [v, v, v];
|
|
5028
|
+
}
|
|
5029
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
5030
|
+
const p = 2 * l - q;
|
|
5031
|
+
return [
|
|
5032
|
+
Math.round(this.hue2rgb(p, q, h + 1 / 3) * 255),
|
|
5033
|
+
Math.round(this.hue2rgb(p, q, h) * 255),
|
|
5034
|
+
Math.round(this.hue2rgb(p, q, h - 1 / 3) * 255)
|
|
5035
|
+
];
|
|
5036
|
+
}
|
|
5037
|
+
hue2rgb(p, q, t) {
|
|
5038
|
+
if (t < 0)
|
|
5039
|
+
t += 1;
|
|
5040
|
+
if (t > 1)
|
|
5041
|
+
t -= 1;
|
|
5042
|
+
if (t < 1 / 6)
|
|
5043
|
+
return p + (q - p) * 6 * t;
|
|
5044
|
+
if (t < 1 / 2)
|
|
5045
|
+
return q;
|
|
5046
|
+
if (t < 2 / 3)
|
|
5047
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
5048
|
+
return p;
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
class Notification extends Controller {
|
|
5053
|
+
container = null;
|
|
5054
|
+
observer = null;
|
|
5055
|
+
connect() {
|
|
5056
|
+
this.container = this.element.closest("[popover]");
|
|
5057
|
+
this.container?.showPopover?.();
|
|
5058
|
+
requestAnimationFrame(() => {
|
|
5059
|
+
this.element.classList.add("visible");
|
|
5060
|
+
});
|
|
5061
|
+
const frame = this.element.parentElement;
|
|
5062
|
+
if (frame) {
|
|
5063
|
+
this.observer = new MutationObserver(() => this.syncCountdown());
|
|
5064
|
+
this.observer.observe(frame, { childList: true });
|
|
5065
|
+
setTimeout(() => this.syncCountdown());
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
disconnect() {
|
|
5069
|
+
this.observer?.disconnect();
|
|
5070
|
+
this.observer = null;
|
|
5071
|
+
if (this.container) {
|
|
5072
|
+
const frame = this.container.querySelector("turbo-frame");
|
|
5073
|
+
if (frame?.children.length === 0 && this.container.matches(":popover-open")) {
|
|
5074
|
+
this.container.hidePopover?.();
|
|
5075
|
+
}
|
|
5076
|
+
this.container = null;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
close() {
|
|
5080
|
+
if (!this.element.classList.contains("animated")) {
|
|
5081
|
+
this.element.remove();
|
|
5082
|
+
return;
|
|
5083
|
+
}
|
|
5084
|
+
this.element.classList.add("closing");
|
|
5085
|
+
setTimeout(() => {
|
|
5086
|
+
this.element.remove();
|
|
5087
|
+
}, 350);
|
|
5088
|
+
}
|
|
5089
|
+
syncCountdown() {
|
|
5090
|
+
const timerEl = this.element.querySelector('[data-controller~="midwest-countdown-timer"]');
|
|
5091
|
+
if (!timerEl)
|
|
5092
|
+
return;
|
|
5093
|
+
const timer = this.application.getControllerForElementAndIdentifier(
|
|
5094
|
+
timerEl,
|
|
5095
|
+
"midwest-countdown-timer"
|
|
5096
|
+
);
|
|
5097
|
+
if (!timer)
|
|
5098
|
+
return;
|
|
5099
|
+
const isTop = this.element === this.element.parentElement?.firstElementChild;
|
|
5100
|
+
if (isTop) {
|
|
5101
|
+
timer.resume();
|
|
5102
|
+
} else {
|
|
5103
|
+
timer.pause();
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
|
|
4736
5108
|
function registerMidwestControllers(application) {
|
|
4737
5109
|
application.register("midwest-card", Card);
|
|
4738
5110
|
application.register("midwest-banner", Banner);
|
|
@@ -4755,6 +5127,8 @@ function registerMidwestControllers(application) {
|
|
|
4755
5127
|
application.register("midwest-rating", Rating);
|
|
4756
5128
|
application.register("midwest-autocomplete", Autocomplete);
|
|
4757
5129
|
application.register("midwest-image", Image);
|
|
5130
|
+
application.register("midwest-color-picker", ColorPicker);
|
|
5131
|
+
application.register("midwest-notification", Notification);
|
|
4758
5132
|
}
|
|
4759
5133
|
|
|
4760
5134
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|