@unabridged/midwest 0.16.0 → 0.17.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 +2 -0
- package/app/assets/javascript/midwest.js +754 -97
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +3 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +2 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +17 -5
- package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js +180 -92
- 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/live_summary_component/live_summary_component_controller.js +12 -0
- 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/image_component/color_thief.js +490 -0
- package/dist/javascript/collection/app/components/midwest/image_component/color_thief.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/image_component/image_component_controller.js +47 -0
- package/dist/javascript/collection/app/components/midwest/image_component/image_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/tabs_component/tabs_component_controller.js +15 -1
- package/dist/javascript/collection/app/components/midwest/tabs_component/tabs_component_controller.js.map +1 -1
- package/dist/javascript/midwest.js +754 -97
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -3111,16 +3111,23 @@ class Dialog extends Controller {
|
|
|
3111
3111
|
}
|
|
3112
3112
|
|
|
3113
3113
|
class Tabs extends Controller {
|
|
3114
|
-
static targets = ["tab", "panel"];
|
|
3114
|
+
static targets = ["tab", "panel", "list"];
|
|
3115
3115
|
connect() {
|
|
3116
3116
|
const activeIndex = this.tabTargets.findIndex(
|
|
3117
3117
|
(tab) => tab.getAttribute("aria-selected") === "true"
|
|
3118
3118
|
);
|
|
3119
3119
|
this.activateTab(activeIndex >= 0 ? activeIndex : 0);
|
|
3120
3120
|
this.element.addEventListener("keydown", this.handleKeydown);
|
|
3121
|
+
if (this.hasListTarget) {
|
|
3122
|
+
this.updateOverflow();
|
|
3123
|
+
this.listTarget.addEventListener("scroll", this.updateOverflow);
|
|
3124
|
+
}
|
|
3121
3125
|
}
|
|
3122
3126
|
disconnect() {
|
|
3123
3127
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
|
3128
|
+
if (this.hasListTarget) {
|
|
3129
|
+
this.listTarget.removeEventListener("scroll", this.updateOverflow);
|
|
3130
|
+
}
|
|
3124
3131
|
}
|
|
3125
3132
|
select(event) {
|
|
3126
3133
|
const tab = event.currentTarget;
|
|
@@ -3128,6 +3135,13 @@ class Tabs extends Controller {
|
|
|
3128
3135
|
if (index >= 0)
|
|
3129
3136
|
this.activateTab(index);
|
|
3130
3137
|
}
|
|
3138
|
+
updateOverflow = () => {
|
|
3139
|
+
if (!this.hasListTarget)
|
|
3140
|
+
return;
|
|
3141
|
+
const list = this.listTarget;
|
|
3142
|
+
list.classList.toggle("can-scroll-left", list.scrollLeft > 0);
|
|
3143
|
+
list.classList.toggle("can-scroll-right", list.scrollLeft < list.scrollWidth - list.clientWidth - 1);
|
|
3144
|
+
};
|
|
3131
3145
|
handleKeydown = (event) => {
|
|
3132
3146
|
const tabs = this.tabTargets;
|
|
3133
3147
|
if (tabs.length === 0)
|
|
@@ -3230,7 +3244,8 @@ class Carousel extends Controller {
|
|
|
3230
3244
|
static targets = ["track", "prev", "next", "dot"];
|
|
3231
3245
|
static values = {
|
|
3232
3246
|
wizard: { type: Boolean, default: false },
|
|
3233
|
-
autoplay: { type: Number, default: 0 }
|
|
3247
|
+
autoplay: { type: Number, default: 0 },
|
|
3248
|
+
loop: { type: Boolean, default: false }
|
|
3234
3249
|
};
|
|
3235
3250
|
observer = null;
|
|
3236
3251
|
_currentIndex = 0;
|
|
@@ -3266,10 +3281,21 @@ class Carousel extends Controller {
|
|
|
3266
3281
|
this.stopAutoplay();
|
|
3267
3282
|
}
|
|
3268
3283
|
next() {
|
|
3269
|
-
this.
|
|
3284
|
+
const atEnd = this._currentIndex >= this.slides.length - 1;
|
|
3285
|
+
if (this.loopValue && atEnd) {
|
|
3286
|
+
this.slides[0]?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
3287
|
+
} else {
|
|
3288
|
+
this.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
|
|
3289
|
+
}
|
|
3270
3290
|
}
|
|
3271
3291
|
previous() {
|
|
3272
|
-
|
|
3292
|
+
const atStart = this._currentIndex <= 0;
|
|
3293
|
+
if (this.loopValue && atStart) {
|
|
3294
|
+
const last = this.slides[this.slides.length - 1];
|
|
3295
|
+
last?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
3296
|
+
} else {
|
|
3297
|
+
this.trackTarget.scrollBy({ left: -this.trackTarget.clientWidth, behavior: "smooth" });
|
|
3298
|
+
}
|
|
3273
3299
|
}
|
|
3274
3300
|
goto(event) {
|
|
3275
3301
|
const button = event.currentTarget;
|
|
@@ -3436,11 +3462,11 @@ class Carousel extends Controller {
|
|
|
3436
3462
|
const atStart = track.scrollLeft <= 0;
|
|
3437
3463
|
const atEnd = track.scrollLeft >= track.scrollWidth - track.clientWidth - 1;
|
|
3438
3464
|
if (this.hasPrevTarget)
|
|
3439
|
-
this.prevTarget.classList.toggle("hide", atStart);
|
|
3465
|
+
this.prevTarget.classList.toggle("hide", atStart && !this.loopValue);
|
|
3440
3466
|
if (this.hasNextTarget) {
|
|
3441
3467
|
const atLastSlide = this._currentIndex >= this.slides.length - 1;
|
|
3442
3468
|
const frontierReached = this.wizardValue && this._currentIndex >= this._maxUnlocked;
|
|
3443
|
-
this.nextTarget.classList.toggle("hide", atEnd || atLastSlide || frontierReached);
|
|
3469
|
+
this.nextTarget.classList.toggle("hide", (atEnd || atLastSlide || frontierReached) && !this.loopValue);
|
|
3444
3470
|
}
|
|
3445
3471
|
if (this.wizardValue && this.hasDotTarget) {
|
|
3446
3472
|
this.dotTargets.forEach((dot, i) => {
|
|
@@ -3465,10 +3491,12 @@ class FormLiveSummary extends Controller {
|
|
|
3465
3491
|
this.render();
|
|
3466
3492
|
this.form?.addEventListener("input", this.render);
|
|
3467
3493
|
this.form?.addEventListener("change", this.render);
|
|
3494
|
+
this.form?.addEventListener("midwest-autocomplete:change", this.render);
|
|
3468
3495
|
}
|
|
3469
3496
|
disconnect() {
|
|
3470
3497
|
this.form?.removeEventListener("input", this.render);
|
|
3471
3498
|
this.form?.removeEventListener("change", this.render);
|
|
3499
|
+
this.form?.removeEventListener("midwest-autocomplete:change", this.render);
|
|
3472
3500
|
}
|
|
3473
3501
|
get form() {
|
|
3474
3502
|
return this.element.closest("form");
|
|
@@ -3551,6 +3579,16 @@ class FormLiveSummary extends Controller {
|
|
|
3551
3579
|
value: element.value
|
|
3552
3580
|
});
|
|
3553
3581
|
});
|
|
3582
|
+
form.querySelectorAll(".midwest-autocomplete").forEach((ac) => {
|
|
3583
|
+
if (this.element.contains(ac))
|
|
3584
|
+
return;
|
|
3585
|
+
if (!ac.querySelector('[role="combobox"]'))
|
|
3586
|
+
return;
|
|
3587
|
+
const labelEl = ac.querySelector("label");
|
|
3588
|
+
const label = labelEl?.textContent?.trim() || this.prettifyName((ac.dataset.name ?? "").replace(/\[.*\]$/, ""));
|
|
3589
|
+
const selected = Array.from(ac.querySelectorAll(".midwest-autocomplete-tokens .value")).map((el) => el.textContent?.trim()).filter(Boolean).join(", ");
|
|
3590
|
+
fields.push({ label, value: selected || "\u2014" });
|
|
3591
|
+
});
|
|
3554
3592
|
return fields;
|
|
3555
3593
|
}
|
|
3556
3594
|
// Finds the label associated with a specific element via `for` attribute,
|
|
@@ -3820,22 +3858,75 @@ class Rating extends Controller {
|
|
|
3820
3858
|
}
|
|
3821
3859
|
|
|
3822
3860
|
class Autocomplete extends Controller {
|
|
3823
|
-
static targets = ["input", "dropdown", "list", "tokens", "
|
|
3861
|
+
static targets = ["input", "dropdown", "list", "tokens", "multipleTokenTemplate", "singleTokenTemplate", "status", "turboList", "loader"];
|
|
3824
3862
|
static values = {
|
|
3825
3863
|
options: { type: Array, default: [] },
|
|
3826
3864
|
url: { type: String, default: "" },
|
|
3827
|
-
multiple: { type: Boolean, default: true }
|
|
3865
|
+
multiple: { type: Boolean, default: true },
|
|
3866
|
+
turboFrameUrl: { type: String, default: "" }
|
|
3828
3867
|
};
|
|
3829
|
-
selectedValues = /* @__PURE__ */ new Map();
|
|
3830
3868
|
highlightedIndex = -1;
|
|
3831
3869
|
fetchAbort = null;
|
|
3870
|
+
searchTimer;
|
|
3871
|
+
// ── Named handlers ─────────────────────────────────────────────────────────
|
|
3872
|
+
// Arrow-function class properties preserve `this` and produce a stable
|
|
3873
|
+
// reference so addEventListener/removeEventListener pairs always match.
|
|
3874
|
+
onFrameLoad = () => {
|
|
3875
|
+
this.syncSelectedState();
|
|
3876
|
+
this.loading = false;
|
|
3877
|
+
this.showDropdown();
|
|
3878
|
+
};
|
|
3879
|
+
onToggle = (e) => {
|
|
3880
|
+
const opened = e.newState === "open";
|
|
3881
|
+
this.inputTarget.setAttribute("aria-expanded", String(opened));
|
|
3882
|
+
if (!opened) {
|
|
3883
|
+
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
3884
|
+
this.highlightedIndex = -1;
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
onFocusOut = (e) => {
|
|
3888
|
+
if (!this.element.contains(e.relatedTarget)) {
|
|
3889
|
+
this.hideDropdown();
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
onListMouseDown = (e) => {
|
|
3893
|
+
const option = e.target.closest(".midwest-autocomplete-option");
|
|
3894
|
+
if (option) {
|
|
3895
|
+
e.preventDefault();
|
|
3896
|
+
this.pickItem(option);
|
|
3897
|
+
}
|
|
3898
|
+
};
|
|
3899
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
3832
3900
|
connect() {
|
|
3833
|
-
this.
|
|
3901
|
+
this.element.addEventListener("focusout", this.onFocusOut);
|
|
3834
3902
|
}
|
|
3835
3903
|
disconnect() {
|
|
3836
3904
|
this.fetchAbort?.abort();
|
|
3905
|
+
clearTimeout(this.searchTimer);
|
|
3906
|
+
this.element.removeEventListener("focusout", this.onFocusOut);
|
|
3907
|
+
}
|
|
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
|
+
dropdownTargetConnected(el) {
|
|
3912
|
+
el.addEventListener("toggle", this.onToggle);
|
|
3913
|
+
}
|
|
3914
|
+
dropdownTargetDisconnected(el) {
|
|
3915
|
+
el.removeEventListener("toggle", this.onToggle);
|
|
3916
|
+
}
|
|
3917
|
+
listTargetConnected(el) {
|
|
3918
|
+
el.addEventListener("mousedown", this.onListMouseDown);
|
|
3919
|
+
}
|
|
3920
|
+
listTargetDisconnected(el) {
|
|
3921
|
+
el.removeEventListener("mousedown", this.onListMouseDown);
|
|
3922
|
+
}
|
|
3923
|
+
turboListTargetConnected(el) {
|
|
3924
|
+
el.addEventListener("turbo:frame-load", this.onFrameLoad);
|
|
3925
|
+
}
|
|
3926
|
+
turboListTargetDisconnected(el) {
|
|
3927
|
+
el.removeEventListener("turbo:frame-load", this.onFrameLoad);
|
|
3837
3928
|
}
|
|
3838
|
-
// ── Public actions
|
|
3929
|
+
// ── Public actions ──────────────────────────────────────────────────────────
|
|
3839
3930
|
focusInput() {
|
|
3840
3931
|
this.inputTarget.focus();
|
|
3841
3932
|
this.open();
|
|
@@ -3843,21 +3934,20 @@ class Autocomplete extends Controller {
|
|
|
3843
3934
|
open() {
|
|
3844
3935
|
if (this.dropdownTarget.matches(":popover-open"))
|
|
3845
3936
|
return;
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
this.fetch(query);
|
|
3849
|
-
} else {
|
|
3850
|
-
this.renderOptions(this.filteredOptions(query));
|
|
3851
|
-
}
|
|
3937
|
+
clearTimeout(this.searchTimer);
|
|
3938
|
+
this.resolve(this.inputTarget.value.trim());
|
|
3852
3939
|
}
|
|
3853
|
-
|
|
3940
|
+
search() {
|
|
3854
3941
|
const query = this.inputTarget.value.trim();
|
|
3855
|
-
if (this.urlValue) {
|
|
3856
|
-
await this.fetch(query);
|
|
3857
|
-
} else {
|
|
3942
|
+
if (!this.hasTurboListTarget && !this.urlValue) {
|
|
3858
3943
|
this.renderOptions(this.filteredOptions(query));
|
|
3944
|
+
return;
|
|
3859
3945
|
}
|
|
3860
|
-
this.
|
|
3946
|
+
clearTimeout(this.searchTimer);
|
|
3947
|
+
this.searchTimer = window.setTimeout(() => {
|
|
3948
|
+
this.searchTimer = void 0;
|
|
3949
|
+
this.resolve(query);
|
|
3950
|
+
}, 200);
|
|
3861
3951
|
}
|
|
3862
3952
|
navigate(event) {
|
|
3863
3953
|
switch (event.key) {
|
|
@@ -3872,6 +3962,8 @@ class Autocomplete extends Controller {
|
|
|
3872
3962
|
}
|
|
3873
3963
|
case "ArrowUp": {
|
|
3874
3964
|
event.preventDefault();
|
|
3965
|
+
if (!this.dropdownTarget.matches(":popover-open"))
|
|
3966
|
+
break;
|
|
3875
3967
|
const items = this.visibleOptions();
|
|
3876
3968
|
this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
|
|
3877
3969
|
this.syncHighlight(items);
|
|
@@ -3879,6 +3971,8 @@ class Autocomplete extends Controller {
|
|
|
3879
3971
|
}
|
|
3880
3972
|
case "Enter": {
|
|
3881
3973
|
event.preventDefault();
|
|
3974
|
+
if (!this.dropdownTarget.matches(":popover-open"))
|
|
3975
|
+
break;
|
|
3882
3976
|
const items = this.visibleOptions();
|
|
3883
3977
|
if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
|
|
3884
3978
|
this.pickItem(items[this.highlightedIndex]);
|
|
@@ -3890,19 +3984,14 @@ class Autocomplete extends Controller {
|
|
|
3890
3984
|
break;
|
|
3891
3985
|
case "Backspace":
|
|
3892
3986
|
if (this.inputTarget.value === "" && this.multipleValue) {
|
|
3893
|
-
const
|
|
3894
|
-
|
|
3895
|
-
|
|
3987
|
+
const tokens = this.tokensTarget.querySelectorAll("[data-value]");
|
|
3988
|
+
const last = tokens[tokens.length - 1];
|
|
3989
|
+
if (last?.dataset.value)
|
|
3990
|
+
this.deselect(last.dataset.value);
|
|
3896
3991
|
}
|
|
3897
3992
|
break;
|
|
3898
3993
|
}
|
|
3899
3994
|
}
|
|
3900
|
-
visibleOptions() {
|
|
3901
|
-
return this.listTarget.querySelectorAll('.midwest-autocomplete-option:not([aria-selected="true"])');
|
|
3902
|
-
}
|
|
3903
|
-
handleBlur() {
|
|
3904
|
-
setTimeout(() => this.hideDropdown(), 150);
|
|
3905
|
-
}
|
|
3906
3995
|
removeToken(event) {
|
|
3907
3996
|
event.stopPropagation();
|
|
3908
3997
|
const btn = event.currentTarget;
|
|
@@ -3910,16 +3999,31 @@ class Autocomplete extends Controller {
|
|
|
3910
3999
|
if (value)
|
|
3911
4000
|
this.deselect(value);
|
|
3912
4001
|
}
|
|
3913
|
-
//
|
|
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
|
+
selectOption(event) {
|
|
4006
|
+
event.preventDefault();
|
|
4007
|
+
this.pickItem(event.currentTarget);
|
|
4008
|
+
}
|
|
4009
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
3914
4010
|
get optionIdPrefix() {
|
|
3915
4011
|
return `ac-opt-${(this.element.dataset.name ?? "field").replace(/\W/g, "-")}`;
|
|
3916
4012
|
}
|
|
3917
|
-
|
|
3918
|
-
this.
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
4013
|
+
isSelected(value) {
|
|
4014
|
+
return !!this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
|
|
4015
|
+
}
|
|
4016
|
+
// Routes to the correct data source. Shared by open() and the debounced search path.
|
|
4017
|
+
resolve(query) {
|
|
4018
|
+
if (this.hasTurboListTarget) {
|
|
4019
|
+
this.navigateFrame(query);
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
if (this.urlValue) {
|
|
4023
|
+
this.fetch(query);
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
this.renderOptions(this.filteredOptions(query));
|
|
3923
4027
|
}
|
|
3924
4028
|
filteredOptions(query) {
|
|
3925
4029
|
const q = query.toLowerCase();
|
|
@@ -3927,10 +4031,36 @@ class Autocomplete extends Controller {
|
|
|
3927
4031
|
(o) => q === "" || o.label.toLowerCase().includes(q) || (o.support?.toLowerCase().includes(q) ?? false)
|
|
3928
4032
|
);
|
|
3929
4033
|
}
|
|
4034
|
+
navigateFrame(query) {
|
|
4035
|
+
const url = new URL(this.turboFrameUrlValue, window.location.href);
|
|
4036
|
+
if (query)
|
|
4037
|
+
url.searchParams.set("q", query);
|
|
4038
|
+
this.tokensTarget.querySelectorAll("input[type=hidden]").forEach((input) => {
|
|
4039
|
+
url.searchParams.append("selected[]", input.value);
|
|
4040
|
+
});
|
|
4041
|
+
this.loading = true;
|
|
4042
|
+
this.turboListTarget.setAttribute("src", url.toString());
|
|
4043
|
+
this.showDropdown();
|
|
4044
|
+
}
|
|
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
|
+
syncSelectedState() {
|
|
4049
|
+
const options = this.turboListTarget.querySelectorAll(".midwest-autocomplete-option");
|
|
4050
|
+
options.forEach((option) => {
|
|
4051
|
+
const val = option.dataset.value ?? "";
|
|
4052
|
+
option.setAttribute("aria-selected", this.isSelected(val) ? "true" : "false");
|
|
4053
|
+
});
|
|
4054
|
+
const visibleCount = this.visibleOptions().length;
|
|
4055
|
+
this.announce(visibleCount > 0 ? `${visibleCount} option${visibleCount === 1 ? "" : "s"} available` : "No more options");
|
|
4056
|
+
}
|
|
3930
4057
|
async fetch(query) {
|
|
3931
4058
|
this.fetchAbort?.abort();
|
|
3932
4059
|
this.fetchAbort = new AbortController();
|
|
3933
|
-
this.
|
|
4060
|
+
this.listTarget.replaceChildren();
|
|
4061
|
+
this.loading = true;
|
|
4062
|
+
this.announce("Loading");
|
|
4063
|
+
this.showDropdown();
|
|
3934
4064
|
try {
|
|
3935
4065
|
const url = new URL(this.urlValue, window.location.href);
|
|
3936
4066
|
if (query)
|
|
@@ -3948,11 +4078,6 @@ class Autocomplete extends Controller {
|
|
|
3948
4078
|
}
|
|
3949
4079
|
}
|
|
3950
4080
|
select(option) {
|
|
3951
|
-
if (!this.multipleValue) {
|
|
3952
|
-
this.selectedValues.forEach((_, val) => this.removeTokenElement(val));
|
|
3953
|
-
this.selectedValues.clear();
|
|
3954
|
-
}
|
|
3955
|
-
this.selectedValues.set(option.value, option);
|
|
3956
4081
|
this.appendToken(option);
|
|
3957
4082
|
this.hideDropdown();
|
|
3958
4083
|
this.inputTarget.value = "";
|
|
@@ -3961,102 +4086,106 @@ class Autocomplete extends Controller {
|
|
|
3961
4086
|
this.dispatch("change", { detail: { value: option.value, label: option.label } });
|
|
3962
4087
|
}
|
|
3963
4088
|
deselect(value) {
|
|
3964
|
-
const
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
if (
|
|
3968
|
-
this.announce(`${
|
|
4089
|
+
const token = this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
|
|
4090
|
+
const label = token?.querySelector(".value")?.textContent?.trim();
|
|
4091
|
+
token?.remove();
|
|
4092
|
+
if (label)
|
|
4093
|
+
this.announce(`${label} removed`);
|
|
3969
4094
|
this.dispatch("change", { detail: { value: null } });
|
|
3970
4095
|
}
|
|
3971
4096
|
appendToken(option) {
|
|
3972
|
-
|
|
4097
|
+
if (!this.multipleValue) {
|
|
4098
|
+
this.tokensTarget.querySelectorAll("[data-value]").forEach((el) => el.remove());
|
|
4099
|
+
}
|
|
4100
|
+
const template = this.multipleValue ? this.multipleTokenTemplateTarget : this.singleTokenTemplateTarget;
|
|
4101
|
+
const fragment = template.content.cloneNode(true);
|
|
4102
|
+
const badge = fragment.querySelector(".midwest-badge");
|
|
4103
|
+
if (!badge)
|
|
4104
|
+
return;
|
|
4105
|
+
badge.dataset.value = option.value;
|
|
4106
|
+
const valueEl = badge.querySelector(".value");
|
|
4107
|
+
if (valueEl)
|
|
4108
|
+
valueEl.textContent = option.label;
|
|
3973
4109
|
if (this.multipleValue) {
|
|
3974
|
-
const fragment = this.multipleTokenTemplateTarget.content.cloneNode(true);
|
|
3975
|
-
const badge = fragment.querySelector(".midwest-badge");
|
|
3976
|
-
badge.dataset.value = option.value;
|
|
3977
|
-
badge.querySelector(".value").textContent = option.label;
|
|
3978
4110
|
const removeBtn = badge.querySelector(".remove");
|
|
3979
|
-
removeBtn
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
this.
|
|
3987
|
-
const fragment = this.singleTokenTemplateTarget.content.cloneNode(true);
|
|
3988
|
-
const badge = fragment.querySelector(".midwest-badge");
|
|
3989
|
-
badge.dataset.value = option.value;
|
|
3990
|
-
badge.querySelector(".value").textContent = option.label;
|
|
3991
|
-
const input = badge.querySelector("input[type=hidden]");
|
|
3992
|
-
input.name = name;
|
|
4111
|
+
if (removeBtn) {
|
|
4112
|
+
removeBtn.dataset.value = option.value;
|
|
4113
|
+
removeBtn.setAttribute("aria-label", `Remove ${option.label}`);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
const input = badge.querySelector("input[type=hidden]");
|
|
4117
|
+
if (input) {
|
|
4118
|
+
input.name = this.element.dataset.name ?? "";
|
|
3993
4119
|
input.value = option.value;
|
|
3994
|
-
this.tokensTarget.appendChild(fragment);
|
|
3995
4120
|
}
|
|
3996
|
-
|
|
3997
|
-
removeTokenElement(value) {
|
|
3998
|
-
const token = this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
|
|
3999
|
-
token?.remove();
|
|
4121
|
+
this.tokensTarget.appendChild(fragment);
|
|
4000
4122
|
}
|
|
4001
4123
|
renderOptions(options) {
|
|
4002
|
-
this.listTarget.innerHTML = "";
|
|
4003
4124
|
this.highlightedIndex = -1;
|
|
4004
4125
|
if (options.length === 0) {
|
|
4005
4126
|
this.renderEmpty();
|
|
4006
4127
|
return;
|
|
4007
4128
|
}
|
|
4129
|
+
this.listTarget.replaceChildren();
|
|
4008
4130
|
options.forEach((option, index) => {
|
|
4009
4131
|
const li = document.createElement("li");
|
|
4010
4132
|
li.id = `${this.optionIdPrefix}-${index}`;
|
|
4011
4133
|
li.className = "midwest-autocomplete-option";
|
|
4012
4134
|
li.setAttribute("role", "option");
|
|
4013
|
-
li.setAttribute("aria-selected", this.
|
|
4135
|
+
li.setAttribute("aria-selected", this.isSelected(option.value) ? "true" : "false");
|
|
4014
4136
|
li.dataset.value = option.value;
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4137
|
+
const label = document.createElement("span");
|
|
4138
|
+
label.className = "midwest-autocomplete-option-label";
|
|
4139
|
+
label.textContent = option.label;
|
|
4140
|
+
li.appendChild(label);
|
|
4141
|
+
if (option.support) {
|
|
4142
|
+
const support = document.createElement("span");
|
|
4143
|
+
support.className = "midwest-autocomplete-option-support";
|
|
4144
|
+
support.textContent = option.support;
|
|
4145
|
+
li.appendChild(support);
|
|
4146
|
+
}
|
|
4023
4147
|
this.listTarget.appendChild(li);
|
|
4024
4148
|
});
|
|
4025
|
-
const availableCount = options.filter((o) => !this.
|
|
4149
|
+
const availableCount = options.filter((o) => !this.isSelected(o.value)).length;
|
|
4026
4150
|
if (availableCount === 0) {
|
|
4027
4151
|
this.renderEmpty("No more options");
|
|
4028
4152
|
return;
|
|
4029
4153
|
}
|
|
4154
|
+
this.loading = false;
|
|
4030
4155
|
this.announce(`${availableCount} option${availableCount === 1 ? "" : "s"} available`);
|
|
4031
4156
|
this.showDropdown();
|
|
4032
4157
|
}
|
|
4033
4158
|
renderEmpty(message = "No results") {
|
|
4034
|
-
|
|
4159
|
+
const li = document.createElement("li");
|
|
4160
|
+
li.className = "midwest-autocomplete-empty";
|
|
4161
|
+
li.setAttribute("aria-hidden", "true");
|
|
4162
|
+
li.textContent = message;
|
|
4163
|
+
this.listTarget.replaceChildren(li);
|
|
4164
|
+
this.loading = false;
|
|
4035
4165
|
this.announce(message);
|
|
4036
4166
|
this.showDropdown();
|
|
4037
4167
|
}
|
|
4038
|
-
|
|
4039
|
-
this.
|
|
4040
|
-
|
|
4041
|
-
this.showDropdown();
|
|
4168
|
+
set loading(on) {
|
|
4169
|
+
if (this.hasLoaderTarget)
|
|
4170
|
+
this.loaderTarget.hidden = !on;
|
|
4042
4171
|
}
|
|
4043
4172
|
showDropdown() {
|
|
4044
4173
|
if (!this.dropdownTarget.matches(":popover-open"))
|
|
4045
4174
|
this.dropdownTarget.showPopover();
|
|
4046
|
-
this.inputTarget.setAttribute("aria-expanded", "true");
|
|
4047
4175
|
}
|
|
4048
4176
|
hideDropdown() {
|
|
4049
4177
|
if (this.dropdownTarget.matches(":popover-open"))
|
|
4050
4178
|
this.dropdownTarget.hidePopover();
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
this.
|
|
4179
|
+
}
|
|
4180
|
+
visibleOptions() {
|
|
4181
|
+
const container = this.hasTurboListTarget ? this.turboListTarget : this.listTarget;
|
|
4182
|
+
return container.querySelectorAll('.midwest-autocomplete-option:not([aria-selected="true"])');
|
|
4054
4183
|
}
|
|
4055
4184
|
syncHighlight(items) {
|
|
4056
4185
|
items.forEach((item, i) => item.classList.toggle("is-highlighted", i === this.highlightedIndex));
|
|
4057
4186
|
const active = items[this.highlightedIndex];
|
|
4058
4187
|
active?.scrollIntoView({ block: "nearest" });
|
|
4059
|
-
if (active) {
|
|
4188
|
+
if (active?.id) {
|
|
4060
4189
|
this.inputTarget.setAttribute("aria-activedescendant", active.id);
|
|
4061
4190
|
} else {
|
|
4062
4191
|
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
@@ -4064,7 +4193,7 @@ class Autocomplete extends Controller {
|
|
|
4064
4193
|
}
|
|
4065
4194
|
pickItem(item) {
|
|
4066
4195
|
const value = item.dataset.value ?? "";
|
|
4067
|
-
const label = item.querySelector(".midwest-autocomplete-option-label")?.textContent?.trim()
|
|
4196
|
+
const label = item.dataset.label?.trim() || item.querySelector(".midwest-autocomplete-option-label")?.textContent?.trim() || value;
|
|
4068
4197
|
const support = item.querySelector(".midwest-autocomplete-option-support")?.textContent?.trim();
|
|
4069
4198
|
this.select({ value, label, support });
|
|
4070
4199
|
}
|
|
@@ -4072,8 +4201,535 @@ class Autocomplete extends Controller {
|
|
|
4072
4201
|
if (this.hasStatusTarget)
|
|
4073
4202
|
this.statusTarget.textContent = message;
|
|
4074
4203
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
/*
|
|
4207
|
+
* Color Thief v2.0
|
|
4208
|
+
* by Lokesh Dhakar - http://www.lokeshdhakar.com
|
|
4209
|
+
*
|
|
4210
|
+
* Thanks
|
|
4211
|
+
* ------
|
|
4212
|
+
* Nick Rabinowitz - For creating quantize.js.
|
|
4213
|
+
* John Schulz - For clean up and optimization. @JFSIII
|
|
4214
|
+
* Nathan Spady - For adding drag and drop support to the demo page.
|
|
4215
|
+
*
|
|
4216
|
+
* License
|
|
4217
|
+
* -------
|
|
4218
|
+
* Copyright 2011, 2015 Lokesh Dhakar
|
|
4219
|
+
* Released under the MIT license
|
|
4220
|
+
* https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE
|
|
4221
|
+
*
|
|
4222
|
+
* @license
|
|
4223
|
+
*/
|
|
4224
|
+
const CanvasImage = function(image) {
|
|
4225
|
+
this.canvas = document.createElement("canvas");
|
|
4226
|
+
this.context = this.canvas.getContext("2d");
|
|
4227
|
+
document.body.appendChild(this.canvas);
|
|
4228
|
+
this.width = this.canvas.width = image.width;
|
|
4229
|
+
this.height = this.canvas.height = image.height;
|
|
4230
|
+
this.context.drawImage(image, 0, 0, this.width, this.height);
|
|
4231
|
+
};
|
|
4232
|
+
CanvasImage.prototype.clear = function() {
|
|
4233
|
+
this.context.clearRect(0, 0, this.width, this.height);
|
|
4234
|
+
};
|
|
4235
|
+
CanvasImage.prototype.update = function(imageData) {
|
|
4236
|
+
this.context.putImageData(imageData, 0, 0);
|
|
4237
|
+
};
|
|
4238
|
+
CanvasImage.prototype.getPixelCount = function() {
|
|
4239
|
+
return this.width * this.height;
|
|
4240
|
+
};
|
|
4241
|
+
CanvasImage.prototype.getImageData = function() {
|
|
4242
|
+
return this.context.getImageData(0, 0, this.width, this.height);
|
|
4243
|
+
};
|
|
4244
|
+
CanvasImage.prototype.removeCanvas = function() {
|
|
4245
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
4246
|
+
};
|
|
4247
|
+
const ColorThief = function() {
|
|
4248
|
+
};
|
|
4249
|
+
ColorThief.prototype.getColor = function(sourceImage2, quality) {
|
|
4250
|
+
const palette = this.getPalette(sourceImage2, 5, quality);
|
|
4251
|
+
const dominantColor = palette[0];
|
|
4252
|
+
return dominantColor;
|
|
4253
|
+
};
|
|
4254
|
+
ColorThief.prototype.getPalette = function(sourceImage2, colorCount, quality) {
|
|
4255
|
+
if (typeof colorCount === "undefined" || colorCount < 2 || colorCount > 256) {
|
|
4256
|
+
colorCount = 10;
|
|
4257
|
+
}
|
|
4258
|
+
if (typeof quality === "undefined" || quality < 1) {
|
|
4259
|
+
quality = 10;
|
|
4260
|
+
}
|
|
4261
|
+
const image = new CanvasImage(sourceImage2);
|
|
4262
|
+
const imageData = image.getImageData();
|
|
4263
|
+
const pixels = imageData.data;
|
|
4264
|
+
const pixelCount = image.getPixelCount();
|
|
4265
|
+
const pixelArray = [];
|
|
4266
|
+
for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
|
|
4267
|
+
offset = i * 4;
|
|
4268
|
+
r = pixels[offset + 0];
|
|
4269
|
+
g = pixels[offset + 1];
|
|
4270
|
+
b = pixels[offset + 2];
|
|
4271
|
+
a = pixels[offset + 3];
|
|
4272
|
+
if (a >= 125) {
|
|
4273
|
+
if (!(r > 250 && g > 250 && b > 250)) {
|
|
4274
|
+
pixelArray.push([r, g, b]);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
const cmap = MMCQ.quantize(pixelArray, colorCount);
|
|
4279
|
+
const palette = cmap ? cmap.palette() : null;
|
|
4280
|
+
image.removeCanvas();
|
|
4281
|
+
return palette;
|
|
4282
|
+
};
|
|
4283
|
+
ColorThief.prototype.getColorFromUrl = function(imageUrl, callback, quality) {
|
|
4284
|
+
sourceImage = document.createElement("img");
|
|
4285
|
+
const thief = this;
|
|
4286
|
+
sourceImage.addEventListener("load", function() {
|
|
4287
|
+
const palette = thief.getPalette(sourceImage, 5, quality);
|
|
4288
|
+
const dominantColor = palette[0];
|
|
4289
|
+
callback(dominantColor, imageUrl);
|
|
4290
|
+
});
|
|
4291
|
+
sourceImage.src = imageUrl;
|
|
4292
|
+
};
|
|
4293
|
+
ColorThief.prototype.getImageData = function(imageUrl, callback) {
|
|
4294
|
+
xhr = new XMLHttpRequest();
|
|
4295
|
+
xhr.open("GET", imageUrl, true);
|
|
4296
|
+
xhr.responseType = "arraybuffer";
|
|
4297
|
+
xhr.onload = function(e) {
|
|
4298
|
+
if (this.status == 200) {
|
|
4299
|
+
uInt8Array = new Uint8Array(this.response);
|
|
4300
|
+
i = uInt8Array.length;
|
|
4301
|
+
binaryString = new Array(i);
|
|
4302
|
+
for (var i = 0; i < uInt8Array.length; i++) {
|
|
4303
|
+
binaryString[i] = String.fromCharCode(uInt8Array[i]);
|
|
4304
|
+
}
|
|
4305
|
+
data = binaryString.join("");
|
|
4306
|
+
base64 = window.btoa(data);
|
|
4307
|
+
callback("data:image/png;base64," + base64);
|
|
4308
|
+
}
|
|
4309
|
+
};
|
|
4310
|
+
xhr.send();
|
|
4311
|
+
};
|
|
4312
|
+
ColorThief.prototype.getColorAsync = function(imageUrl, callback, quality) {
|
|
4313
|
+
const thief = this;
|
|
4314
|
+
this.getImageData(imageUrl, function(imageData) {
|
|
4315
|
+
sourceImage = document.createElement("img");
|
|
4316
|
+
sourceImage.addEventListener("load", function() {
|
|
4317
|
+
const palette = thief.getPalette(sourceImage, 5, quality);
|
|
4318
|
+
const dominantColor = palette[0];
|
|
4319
|
+
callback(dominantColor, this);
|
|
4320
|
+
});
|
|
4321
|
+
sourceImage.src = imageData;
|
|
4322
|
+
});
|
|
4323
|
+
};
|
|
4324
|
+
/*!
|
|
4325
|
+
* quantize.js Copyright 2008 Nick Rabinowitz.
|
|
4326
|
+
* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
4327
|
+
* @license
|
|
4328
|
+
*/
|
|
4329
|
+
/*!
|
|
4330
|
+
* Block below copied from Protovis: http://mbostock.github.com/protovis/
|
|
4331
|
+
* Copyright 2010 Stanford Visualization Group
|
|
4332
|
+
* Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
|
|
4333
|
+
* @license
|
|
4334
|
+
*/
|
|
4335
|
+
if (!pv) {
|
|
4336
|
+
var pv = {
|
|
4337
|
+
map: function(array, f) {
|
|
4338
|
+
const o = {};
|
|
4339
|
+
return f ? array.map(function(d, i) {
|
|
4340
|
+
o.index = i;
|
|
4341
|
+
return f.call(o, d);
|
|
4342
|
+
}) : array.slice();
|
|
4343
|
+
},
|
|
4344
|
+
naturalOrder: function(a, b) {
|
|
4345
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
4346
|
+
},
|
|
4347
|
+
sum: function(array, f) {
|
|
4348
|
+
const o = {};
|
|
4349
|
+
return array.reduce(f ? function(p, d, i) {
|
|
4350
|
+
o.index = i;
|
|
4351
|
+
return p + f.call(o, d);
|
|
4352
|
+
} : function(p, d) {
|
|
4353
|
+
return p + d;
|
|
4354
|
+
}, 0);
|
|
4355
|
+
},
|
|
4356
|
+
max: function(array, f) {
|
|
4357
|
+
return Math.max.apply(null, f ? pv.map(array, f) : array);
|
|
4358
|
+
}
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
var MMCQ = function() {
|
|
4362
|
+
const sigbits = 5, rshift = 8 - sigbits, maxIterations = 1e3, fractByPopulations = 0.75;
|
|
4363
|
+
function getColorIndex(r, g, b) {
|
|
4364
|
+
return (r << 2 * sigbits) + (g << sigbits) + b;
|
|
4365
|
+
}
|
|
4366
|
+
function PQueue(comparator) {
|
|
4367
|
+
let contents = [], sorted = false;
|
|
4368
|
+
function sort() {
|
|
4369
|
+
contents.sort(comparator);
|
|
4370
|
+
sorted = true;
|
|
4371
|
+
}
|
|
4372
|
+
return {
|
|
4373
|
+
push: function(o) {
|
|
4374
|
+
contents.push(o);
|
|
4375
|
+
sorted = false;
|
|
4376
|
+
},
|
|
4377
|
+
peek: function(index) {
|
|
4378
|
+
if (!sorted)
|
|
4379
|
+
sort();
|
|
4380
|
+
if (index === void 0)
|
|
4381
|
+
index = contents.length - 1;
|
|
4382
|
+
return contents[index];
|
|
4383
|
+
},
|
|
4384
|
+
pop: function() {
|
|
4385
|
+
if (!sorted)
|
|
4386
|
+
sort();
|
|
4387
|
+
return contents.pop();
|
|
4388
|
+
},
|
|
4389
|
+
size: function() {
|
|
4390
|
+
return contents.length;
|
|
4391
|
+
},
|
|
4392
|
+
map: function(f) {
|
|
4393
|
+
return contents.map(f);
|
|
4394
|
+
},
|
|
4395
|
+
debug: function() {
|
|
4396
|
+
if (!sorted)
|
|
4397
|
+
sort();
|
|
4398
|
+
return contents;
|
|
4399
|
+
}
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
function VBox(r1, r2, g1, g2, b1, b2, histo) {
|
|
4403
|
+
const vbox = this;
|
|
4404
|
+
vbox.r1 = r1;
|
|
4405
|
+
vbox.r2 = r2;
|
|
4406
|
+
vbox.g1 = g1;
|
|
4407
|
+
vbox.g2 = g2;
|
|
4408
|
+
vbox.b1 = b1;
|
|
4409
|
+
vbox.b2 = b2;
|
|
4410
|
+
vbox.histo = histo;
|
|
4411
|
+
}
|
|
4412
|
+
VBox.prototype = {
|
|
4413
|
+
volume: function(force) {
|
|
4414
|
+
const vbox = this;
|
|
4415
|
+
if (!vbox._volume || force) {
|
|
4416
|
+
vbox._volume = (vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1);
|
|
4417
|
+
}
|
|
4418
|
+
return vbox._volume;
|
|
4419
|
+
},
|
|
4420
|
+
count: function(force) {
|
|
4421
|
+
const vbox = this, histo = vbox.histo;
|
|
4422
|
+
if (!vbox._count_set || force) {
|
|
4423
|
+
let npix = 0, index, i, j, k;
|
|
4424
|
+
for (i = vbox.r1; i <= vbox.r2; i++) {
|
|
4425
|
+
for (j = vbox.g1; j <= vbox.g2; j++) {
|
|
4426
|
+
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
4427
|
+
index = getColorIndex(i, j, k);
|
|
4428
|
+
npix += histo[index] || 0;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
vbox._count = npix;
|
|
4433
|
+
vbox._count_set = true;
|
|
4434
|
+
}
|
|
4435
|
+
return vbox._count;
|
|
4436
|
+
},
|
|
4437
|
+
copy: function() {
|
|
4438
|
+
const vbox = this;
|
|
4439
|
+
return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
|
|
4440
|
+
},
|
|
4441
|
+
avg: function(force) {
|
|
4442
|
+
const vbox = this, histo = vbox.histo;
|
|
4443
|
+
if (!vbox._avg || force) {
|
|
4444
|
+
let ntot = 0, mult = 1 << 8 - sigbits, rsum = 0, gsum = 0, bsum = 0, hval, i, j, k, histoindex;
|
|
4445
|
+
for (i = vbox.r1; i <= vbox.r2; i++) {
|
|
4446
|
+
for (j = vbox.g1; j <= vbox.g2; j++) {
|
|
4447
|
+
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
4448
|
+
histoindex = getColorIndex(i, j, k);
|
|
4449
|
+
hval = histo[histoindex] || 0;
|
|
4450
|
+
ntot += hval;
|
|
4451
|
+
rsum += hval * (i + 0.5) * mult;
|
|
4452
|
+
gsum += hval * (j + 0.5) * mult;
|
|
4453
|
+
bsum += hval * (k + 0.5) * mult;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
if (ntot) {
|
|
4458
|
+
vbox._avg = [~~(rsum / ntot), ~~(gsum / ntot), ~~(bsum / ntot)];
|
|
4459
|
+
} else {
|
|
4460
|
+
vbox._avg = [
|
|
4461
|
+
~~(mult * (vbox.r1 + vbox.r2 + 1) / 2),
|
|
4462
|
+
~~(mult * (vbox.g1 + vbox.g2 + 1) / 2),
|
|
4463
|
+
~~(mult * (vbox.b1 + vbox.b2 + 1) / 2)
|
|
4464
|
+
];
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
return vbox._avg;
|
|
4468
|
+
},
|
|
4469
|
+
contains: function(pixel) {
|
|
4470
|
+
const vbox = this, rval = pixel[0] >> rshift;
|
|
4471
|
+
gval = pixel[1] >> rshift;
|
|
4472
|
+
bval = pixel[2] >> rshift;
|
|
4473
|
+
return rval >= vbox.r1 && rval <= vbox.r2 && gval >= vbox.g1 && gval <= vbox.g2 && bval >= vbox.b1 && bval <= vbox.b2;
|
|
4474
|
+
}
|
|
4475
|
+
};
|
|
4476
|
+
function CMap() {
|
|
4477
|
+
this.vboxes = new PQueue(function(a, b) {
|
|
4478
|
+
return pv.naturalOrder(
|
|
4479
|
+
a.vbox.count() * a.vbox.volume(),
|
|
4480
|
+
b.vbox.count() * b.vbox.volume()
|
|
4481
|
+
);
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
CMap.prototype = {
|
|
4485
|
+
push: function(vbox) {
|
|
4486
|
+
this.vboxes.push({
|
|
4487
|
+
vbox,
|
|
4488
|
+
color: vbox.avg()
|
|
4489
|
+
});
|
|
4490
|
+
},
|
|
4491
|
+
palette: function() {
|
|
4492
|
+
return this.vboxes.map(function(vb) {
|
|
4493
|
+
return vb.color;
|
|
4494
|
+
});
|
|
4495
|
+
},
|
|
4496
|
+
size: function() {
|
|
4497
|
+
return this.vboxes.size();
|
|
4498
|
+
},
|
|
4499
|
+
map: function(color) {
|
|
4500
|
+
const vboxes = this.vboxes;
|
|
4501
|
+
for (let i = 0; i < vboxes.size(); i++) {
|
|
4502
|
+
if (vboxes.peek(i).vbox.contains(color)) {
|
|
4503
|
+
return vboxes.peek(i).color;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
return this.nearest(color);
|
|
4507
|
+
},
|
|
4508
|
+
nearest: function(color) {
|
|
4509
|
+
let vboxes = this.vboxes, d1, d2, pColor;
|
|
4510
|
+
for (let i = 0; i < vboxes.size(); i++) {
|
|
4511
|
+
d2 = Math.sqrt(
|
|
4512
|
+
Math.pow(color[0] - vboxes.peek(i).color[0], 2) + Math.pow(color[1] - vboxes.peek(i).color[1], 2) + Math.pow(color[2] - vboxes.peek(i).color[2], 2)
|
|
4513
|
+
);
|
|
4514
|
+
if (d2 < d1 || d1 === void 0) {
|
|
4515
|
+
d1 = d2;
|
|
4516
|
+
pColor = vboxes.peek(i).color;
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
return pColor;
|
|
4520
|
+
},
|
|
4521
|
+
forcebw: function() {
|
|
4522
|
+
const vboxes = this.vboxes;
|
|
4523
|
+
vboxes.sort(function(a, b) {
|
|
4524
|
+
return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color));
|
|
4525
|
+
});
|
|
4526
|
+
const lowest = vboxes[0].color;
|
|
4527
|
+
if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
|
|
4528
|
+
vboxes[0].color = [0, 0, 0];
|
|
4529
|
+
const idx = vboxes.length - 1, highest = vboxes[idx].color;
|
|
4530
|
+
if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
|
|
4531
|
+
vboxes[idx].color = [255, 255, 255];
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
function getHisto(pixels) {
|
|
4535
|
+
let histosize = 1 << 3 * sigbits, histo = new Array(histosize), index, rval, gval2, bval2;
|
|
4536
|
+
pixels.forEach(function(pixel) {
|
|
4537
|
+
rval = pixel[0] >> rshift;
|
|
4538
|
+
gval2 = pixel[1] >> rshift;
|
|
4539
|
+
bval2 = pixel[2] >> rshift;
|
|
4540
|
+
index = getColorIndex(rval, gval2, bval2);
|
|
4541
|
+
histo[index] = (histo[index] || 0) + 1;
|
|
4542
|
+
});
|
|
4543
|
+
return histo;
|
|
4544
|
+
}
|
|
4545
|
+
function vboxFromPixels(pixels, histo) {
|
|
4546
|
+
let rmin = 1e6, rmax = 0, gmin = 1e6, gmax = 0, bmin = 1e6, bmax = 0, rval, gval2, bval2;
|
|
4547
|
+
pixels.forEach(function(pixel) {
|
|
4548
|
+
rval = pixel[0] >> rshift;
|
|
4549
|
+
gval2 = pixel[1] >> rshift;
|
|
4550
|
+
bval2 = pixel[2] >> rshift;
|
|
4551
|
+
if (rval < rmin)
|
|
4552
|
+
rmin = rval;
|
|
4553
|
+
else if (rval > rmax)
|
|
4554
|
+
rmax = rval;
|
|
4555
|
+
if (gval2 < gmin)
|
|
4556
|
+
gmin = gval2;
|
|
4557
|
+
else if (gval2 > gmax)
|
|
4558
|
+
gmax = gval2;
|
|
4559
|
+
if (bval2 < bmin)
|
|
4560
|
+
bmin = bval2;
|
|
4561
|
+
else if (bval2 > bmax)
|
|
4562
|
+
bmax = bval2;
|
|
4563
|
+
});
|
|
4564
|
+
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
|
|
4565
|
+
}
|
|
4566
|
+
function medianCutApply(histo, vbox) {
|
|
4567
|
+
if (!vbox.count())
|
|
4568
|
+
return;
|
|
4569
|
+
const rw = vbox.r2 - vbox.r1 + 1, gw = vbox.g2 - vbox.g1 + 1, bw = vbox.b2 - vbox.b1 + 1, maxw = pv.max([rw, gw, bw]);
|
|
4570
|
+
if (vbox.count() == 1) {
|
|
4571
|
+
return [vbox.copy()];
|
|
4572
|
+
}
|
|
4573
|
+
let total = 0, partialsum = [], lookaheadsum = [], i, j, k, sum, index;
|
|
4574
|
+
if (maxw == rw) {
|
|
4575
|
+
for (i = vbox.r1; i <= vbox.r2; i++) {
|
|
4576
|
+
sum = 0;
|
|
4577
|
+
for (j = vbox.g1; j <= vbox.g2; j++) {
|
|
4578
|
+
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
4579
|
+
index = getColorIndex(i, j, k);
|
|
4580
|
+
sum += histo[index] || 0;
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
total += sum;
|
|
4584
|
+
partialsum[i] = total;
|
|
4585
|
+
}
|
|
4586
|
+
} else if (maxw == gw) {
|
|
4587
|
+
for (i = vbox.g1; i <= vbox.g2; i++) {
|
|
4588
|
+
sum = 0;
|
|
4589
|
+
for (j = vbox.r1; j <= vbox.r2; j++) {
|
|
4590
|
+
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
4591
|
+
index = getColorIndex(j, i, k);
|
|
4592
|
+
sum += histo[index] || 0;
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
total += sum;
|
|
4596
|
+
partialsum[i] = total;
|
|
4597
|
+
}
|
|
4598
|
+
} else {
|
|
4599
|
+
for (i = vbox.b1; i <= vbox.b2; i++) {
|
|
4600
|
+
sum = 0;
|
|
4601
|
+
for (j = vbox.r1; j <= vbox.r2; j++) {
|
|
4602
|
+
for (k = vbox.g1; k <= vbox.g2; k++) {
|
|
4603
|
+
index = getColorIndex(j, k, i);
|
|
4604
|
+
sum += histo[index] || 0;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
total += sum;
|
|
4608
|
+
partialsum[i] = total;
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
partialsum.forEach(function(d, i2) {
|
|
4612
|
+
lookaheadsum[i2] = total - d;
|
|
4613
|
+
});
|
|
4614
|
+
function doCut(color) {
|
|
4615
|
+
let dim1 = color + "1", dim2 = color + "2", left, right, vbox1, vbox2, d2, count2 = 0;
|
|
4616
|
+
for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
|
|
4617
|
+
if (partialsum[i] > total / 2) {
|
|
4618
|
+
vbox1 = vbox.copy();
|
|
4619
|
+
vbox2 = vbox.copy();
|
|
4620
|
+
left = i - vbox[dim1];
|
|
4621
|
+
right = vbox[dim2] - i;
|
|
4622
|
+
if (left <= right)
|
|
4623
|
+
d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
|
|
4624
|
+
else
|
|
4625
|
+
d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
|
|
4626
|
+
while (!partialsum[d2])
|
|
4627
|
+
d2++;
|
|
4628
|
+
count2 = lookaheadsum[d2];
|
|
4629
|
+
while (!count2 && partialsum[d2 - 1])
|
|
4630
|
+
count2 = lookaheadsum[--d2];
|
|
4631
|
+
vbox1[dim2] = d2;
|
|
4632
|
+
vbox2[dim1] = vbox1[dim2] + 1;
|
|
4633
|
+
return [vbox1, vbox2];
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
return maxw == rw ? doCut("r") : maxw == gw ? doCut("g") : doCut("b");
|
|
4638
|
+
}
|
|
4639
|
+
function quantize(pixels, maxcolors) {
|
|
4640
|
+
if (!pixels.length || maxcolors < 2 || maxcolors > 256) {
|
|
4641
|
+
return false;
|
|
4642
|
+
}
|
|
4643
|
+
const histo = getHisto(pixels);
|
|
4644
|
+
histo.forEach(function() {
|
|
4645
|
+
});
|
|
4646
|
+
const vbox = vboxFromPixels(pixels, histo), pq = new PQueue(function(a, b) {
|
|
4647
|
+
return pv.naturalOrder(a.count(), b.count());
|
|
4648
|
+
});
|
|
4649
|
+
pq.push(vbox);
|
|
4650
|
+
function iter(lh, target) {
|
|
4651
|
+
let ncolors = 1, niters = 0, vbox2;
|
|
4652
|
+
while (niters < maxIterations) {
|
|
4653
|
+
vbox2 = lh.pop();
|
|
4654
|
+
if (!vbox2.count()) {
|
|
4655
|
+
lh.push(vbox2);
|
|
4656
|
+
niters++;
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
const vboxes = medianCutApply(histo, vbox2), vbox1 = vboxes[0], vbox22 = vboxes[1];
|
|
4660
|
+
if (!vbox1) {
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4663
|
+
lh.push(vbox1);
|
|
4664
|
+
if (vbox22) {
|
|
4665
|
+
lh.push(vbox22);
|
|
4666
|
+
ncolors++;
|
|
4667
|
+
}
|
|
4668
|
+
if (ncolors >= target)
|
|
4669
|
+
return;
|
|
4670
|
+
if (niters++ > maxIterations) {
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
iter(pq, fractByPopulations * maxcolors);
|
|
4676
|
+
const pq2 = new PQueue(function(a, b) {
|
|
4677
|
+
return pv.naturalOrder(a.count() * a.volume(), b.count() * b.volume());
|
|
4678
|
+
});
|
|
4679
|
+
while (pq.size()) {
|
|
4680
|
+
pq2.push(pq.pop());
|
|
4681
|
+
}
|
|
4682
|
+
iter(pq2, maxcolors - pq2.size());
|
|
4683
|
+
const cmap = new CMap();
|
|
4684
|
+
while (pq2.size()) {
|
|
4685
|
+
cmap.push(pq2.pop());
|
|
4686
|
+
}
|
|
4687
|
+
return cmap;
|
|
4688
|
+
}
|
|
4689
|
+
return {
|
|
4690
|
+
quantize
|
|
4691
|
+
};
|
|
4692
|
+
}();
|
|
4693
|
+
|
|
4694
|
+
class Image extends Controller {
|
|
4695
|
+
static targets = ["dialog"];
|
|
4696
|
+
static values = { poster: String };
|
|
4697
|
+
connect() {
|
|
4698
|
+
this.extractColor();
|
|
4699
|
+
}
|
|
4700
|
+
extractColor() {
|
|
4701
|
+
if (!this.hasDialogTarget)
|
|
4702
|
+
return;
|
|
4703
|
+
const mainImg = this.element.querySelector("img.midwest-image-main");
|
|
4704
|
+
const colorSource = this.posterValue || mainImg?.src;
|
|
4705
|
+
if (!colorSource)
|
|
4706
|
+
return;
|
|
4707
|
+
const sample = new window.Image(80, 80);
|
|
4708
|
+
sample.crossOrigin = "Anonymous";
|
|
4709
|
+
sample.addEventListener("load", () => {
|
|
4710
|
+
try {
|
|
4711
|
+
const [r, g, b] = new ColorThief().getColor(sample);
|
|
4712
|
+
this.dialogTarget.style.setProperty("--image-dominant-color", `${r} ${g} ${b}`);
|
|
4713
|
+
} catch {
|
|
4714
|
+
}
|
|
4715
|
+
});
|
|
4716
|
+
sample.src = colorSource;
|
|
4717
|
+
}
|
|
4718
|
+
zoom() {
|
|
4719
|
+
if (this.hasDialogTarget) {
|
|
4720
|
+
this.dialogTarget.showModal();
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
close() {
|
|
4724
|
+
if (this.hasDialogTarget) {
|
|
4725
|
+
this.dialogTarget.close();
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
// Close when clicking the backdrop (outside the dialog content).
|
|
4729
|
+
backdropClose(event) {
|
|
4730
|
+
if (event.target === this.dialogTarget) {
|
|
4731
|
+
this.dialogTarget.close();
|
|
4732
|
+
}
|
|
4077
4733
|
}
|
|
4078
4734
|
}
|
|
4079
4735
|
|
|
@@ -4098,6 +4754,7 @@ function registerMidwestControllers(application) {
|
|
|
4098
4754
|
application.register("midwest-range", Range);
|
|
4099
4755
|
application.register("midwest-rating", Rating);
|
|
4100
4756
|
application.register("midwest-autocomplete", Autocomplete);
|
|
4757
|
+
application.register("midwest-image", Image);
|
|
4101
4758
|
}
|
|
4102
4759
|
|
|
4103
4760
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|