@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.
Files changed (23) hide show
  1. package/app/assets/javascript/midwest/index.ts +2 -0
  2. package/app/assets/javascript/midwest.js +754 -97
  3. package/app/assets/javascript/midwest.js.map +1 -1
  4. package/app/assets/stylesheets/midwest.css +1 -1
  5. package/app/assets/stylesheets/midwest.tailwind.css +3 -1
  6. package/dist/css/midwest.css +1 -1
  7. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +2 -0
  8. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  9. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +17 -5
  10. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
  11. package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js +180 -92
  12. package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js.map +1 -1
  13. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +12 -0
  14. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  15. package/dist/javascript/collection/app/components/midwest/image_component/color_thief.js +490 -0
  16. package/dist/javascript/collection/app/components/midwest/image_component/color_thief.js.map +1 -0
  17. package/dist/javascript/collection/app/components/midwest/image_component/image_component_controller.js +47 -0
  18. package/dist/javascript/collection/app/components/midwest/image_component/image_component_controller.js.map +1 -0
  19. package/dist/javascript/collection/app/components/midwest/tabs_component/tabs_component_controller.js +15 -1
  20. package/dist/javascript/collection/app/components/midwest/tabs_component/tabs_component_controller.js.map +1 -1
  21. package/dist/javascript/midwest.js +754 -97
  22. package/dist/javascript/midwest.js.map +1 -1
  23. 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.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
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
- this.trackTarget.scrollBy({ left: -this.trackTarget.clientWidth, behavior: "smooth" });
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", "token", "multipleTokenTemplate", "singleTokenTemplate", "status"];
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.loadInitialTokens();
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
- const query = this.inputTarget.value.trim();
3847
- if (this.urlValue) {
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
- async search() {
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.showDropdown();
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 entries = Array.from(this.selectedValues.keys());
3894
- if (entries.length > 0)
3895
- this.deselect(entries[entries.length - 1]);
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
- // ── Private ────────────────────────────────────────────────────────────────
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
- loadInitialTokens() {
3918
- this.tokenTargets.forEach((token) => {
3919
- const value = token.dataset.value ?? "";
3920
- const label = token.querySelector(".value")?.textContent?.trim() ?? value;
3921
- this.selectedValues.set(value, { value, label });
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.showLoading();
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 option = this.selectedValues.get(value);
3965
- this.selectedValues.delete(value);
3966
- this.removeTokenElement(value);
3967
- if (option)
3968
- this.announce(`${option.label} removed`);
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
- const name = this.element.dataset.name ?? "";
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.dataset.value = option.value;
3980
- removeBtn.setAttribute("aria-label", `Remove ${option.label}`);
3981
- const input = badge.querySelector("input[type=hidden]");
3982
- input.name = name;
3983
- input.value = option.value;
3984
- this.tokensTarget.appendChild(fragment);
3985
- } else {
3986
- this.tokensTarget.querySelectorAll("[data-value]").forEach((el) => el.remove());
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.selectedValues.has(option.value) ? "true" : "false");
4135
+ li.setAttribute("aria-selected", this.isSelected(option.value) ? "true" : "false");
4014
4136
  li.dataset.value = option.value;
4015
- li.innerHTML = `
4016
- <span class="midwest-autocomplete-option-label">${this.escapeHtml(option.label)}</span>
4017
- ${option.support ? `<span class="midwest-autocomplete-option-support">${this.escapeHtml(option.support)}</span>` : ""}
4018
- `;
4019
- li.addEventListener("mousedown", (e) => {
4020
- e.preventDefault();
4021
- this.select(option);
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.selectedValues.has(o.value)).length;
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
- this.listTarget.innerHTML = `<li class="midwest-autocomplete-empty" aria-hidden="true">${this.escapeHtml(message)}</li>`;
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
- showLoading() {
4039
- this.listTarget.innerHTML = '<li class="midwest-autocomplete-loading" aria-hidden="true">Loading\u2026</li>';
4040
- this.announce("Loading");
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
- this.inputTarget.setAttribute("aria-expanded", "false");
4052
- this.inputTarget.removeAttribute("aria-activedescendant");
4053
- this.highlightedIndex = -1;
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() ?? value;
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
- escapeHtml(str) {
4076
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 };