@unabridged/midwest 0.15.0 → 0.16.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 +275 -1
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +1 -0
- 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/form/autocomplete_component/autocomplete_component_controller.js +262 -0
- package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js +16 -1
- package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js.map +1 -1
- package/dist/javascript/midwest.js +275 -1
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import AvatarGroup from '../../../components/midwest/avatar_group_component/avat
|
|
|
17
17
|
import Chart from '../../../components/midwest/chart_component/chart_component_controller'
|
|
18
18
|
import Range from '../../../components/midwest/form/range_component/range_component_controller'
|
|
19
19
|
import Rating from '../../../components/midwest/rating_component/rating_component_controller'
|
|
20
|
+
import Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller'
|
|
20
21
|
/* IMPORTS */
|
|
21
22
|
|
|
22
23
|
export { Card, Banner, CountdownTimer, Chart }
|
|
@@ -41,5 +42,6 @@ export function registerMidwestControllers (application: any) {
|
|
|
41
42
|
application.register('midwest-chart', Chart)
|
|
42
43
|
application.register('midwest-range', Range)
|
|
43
44
|
application.register('midwest-rating', Rating)
|
|
45
|
+
application.register('midwest-autocomplete', Autocomplete)
|
|
44
46
|
/* EXPORTS */
|
|
45
47
|
}
|
|
@@ -549,17 +549,23 @@ class Banner extends Controller {
|
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
class Input extends Controller {
|
|
552
|
-
static targets = ["input"];
|
|
552
|
+
static targets = ["input", "counter"];
|
|
553
553
|
updateClearButton = null;
|
|
554
|
+
updateCounter = null;
|
|
554
555
|
connect() {
|
|
555
556
|
this.applyColor();
|
|
556
557
|
this.applySearchClear();
|
|
558
|
+
this.applyCounter();
|
|
557
559
|
}
|
|
558
560
|
disconnect() {
|
|
559
561
|
if (this.updateClearButton) {
|
|
560
562
|
this.inputTarget.removeEventListener("input", this.updateClearButton);
|
|
561
563
|
this.updateClearButton = null;
|
|
562
564
|
}
|
|
565
|
+
if (this.updateCounter) {
|
|
566
|
+
this.inputTarget.removeEventListener("input", this.updateCounter);
|
|
567
|
+
this.updateCounter = null;
|
|
568
|
+
}
|
|
563
569
|
}
|
|
564
570
|
applyColor() {
|
|
565
571
|
if (this.inputTarget.type !== "color") {
|
|
@@ -569,6 +575,15 @@ class Input extends Controller {
|
|
|
569
575
|
this.inputTarget.setAttribute("value", this.inputTarget.value);
|
|
570
576
|
});
|
|
571
577
|
}
|
|
578
|
+
applyCounter() {
|
|
579
|
+
if (!this.hasCounterTarget)
|
|
580
|
+
return;
|
|
581
|
+
this.counterTarget.textContent = String(this.inputTarget.value.length);
|
|
582
|
+
this.updateCounter = () => {
|
|
583
|
+
this.counterTarget.textContent = String(this.inputTarget.value.length);
|
|
584
|
+
};
|
|
585
|
+
this.inputTarget.addEventListener("input", this.updateCounter);
|
|
586
|
+
}
|
|
572
587
|
applySearchClear() {
|
|
573
588
|
if (this.inputTarget.type !== "search")
|
|
574
589
|
return;
|
|
@@ -3804,6 +3819,264 @@ class Rating extends Controller {
|
|
|
3804
3819
|
}
|
|
3805
3820
|
}
|
|
3806
3821
|
|
|
3822
|
+
class Autocomplete extends Controller {
|
|
3823
|
+
static targets = ["input", "dropdown", "list", "tokens", "token", "multipleTokenTemplate", "singleTokenTemplate", "status"];
|
|
3824
|
+
static values = {
|
|
3825
|
+
options: { type: Array, default: [] },
|
|
3826
|
+
url: { type: String, default: "" },
|
|
3827
|
+
multiple: { type: Boolean, default: true }
|
|
3828
|
+
};
|
|
3829
|
+
selectedValues = /* @__PURE__ */ new Map();
|
|
3830
|
+
highlightedIndex = -1;
|
|
3831
|
+
fetchAbort = null;
|
|
3832
|
+
connect() {
|
|
3833
|
+
this.loadInitialTokens();
|
|
3834
|
+
}
|
|
3835
|
+
disconnect() {
|
|
3836
|
+
this.fetchAbort?.abort();
|
|
3837
|
+
}
|
|
3838
|
+
// ── Public actions ─────────────────────────────────────────────────────────
|
|
3839
|
+
focusInput() {
|
|
3840
|
+
this.inputTarget.focus();
|
|
3841
|
+
this.open();
|
|
3842
|
+
}
|
|
3843
|
+
open() {
|
|
3844
|
+
if (this.dropdownTarget.matches(":popover-open"))
|
|
3845
|
+
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
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
async search() {
|
|
3854
|
+
const query = this.inputTarget.value.trim();
|
|
3855
|
+
if (this.urlValue) {
|
|
3856
|
+
await this.fetch(query);
|
|
3857
|
+
} else {
|
|
3858
|
+
this.renderOptions(this.filteredOptions(query));
|
|
3859
|
+
}
|
|
3860
|
+
this.showDropdown();
|
|
3861
|
+
}
|
|
3862
|
+
navigate(event) {
|
|
3863
|
+
switch (event.key) {
|
|
3864
|
+
case "ArrowDown": {
|
|
3865
|
+
event.preventDefault();
|
|
3866
|
+
if (!this.dropdownTarget.matches(":popover-open"))
|
|
3867
|
+
this.open();
|
|
3868
|
+
const items = this.visibleOptions();
|
|
3869
|
+
this.highlightedIndex = Math.min(this.highlightedIndex + 1, items.length - 1);
|
|
3870
|
+
this.syncHighlight(items);
|
|
3871
|
+
break;
|
|
3872
|
+
}
|
|
3873
|
+
case "ArrowUp": {
|
|
3874
|
+
event.preventDefault();
|
|
3875
|
+
const items = this.visibleOptions();
|
|
3876
|
+
this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
|
|
3877
|
+
this.syncHighlight(items);
|
|
3878
|
+
break;
|
|
3879
|
+
}
|
|
3880
|
+
case "Enter": {
|
|
3881
|
+
event.preventDefault();
|
|
3882
|
+
const items = this.visibleOptions();
|
|
3883
|
+
if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
|
|
3884
|
+
this.pickItem(items[this.highlightedIndex]);
|
|
3885
|
+
}
|
|
3886
|
+
break;
|
|
3887
|
+
}
|
|
3888
|
+
case "Escape":
|
|
3889
|
+
this.hideDropdown();
|
|
3890
|
+
break;
|
|
3891
|
+
case "Backspace":
|
|
3892
|
+
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]);
|
|
3896
|
+
}
|
|
3897
|
+
break;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
visibleOptions() {
|
|
3901
|
+
return this.listTarget.querySelectorAll('.midwest-autocomplete-option:not([aria-selected="true"])');
|
|
3902
|
+
}
|
|
3903
|
+
handleBlur() {
|
|
3904
|
+
setTimeout(() => this.hideDropdown(), 150);
|
|
3905
|
+
}
|
|
3906
|
+
removeToken(event) {
|
|
3907
|
+
event.stopPropagation();
|
|
3908
|
+
const btn = event.currentTarget;
|
|
3909
|
+
const value = btn.dataset.value;
|
|
3910
|
+
if (value)
|
|
3911
|
+
this.deselect(value);
|
|
3912
|
+
}
|
|
3913
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
3914
|
+
get optionIdPrefix() {
|
|
3915
|
+
return `ac-opt-${(this.element.dataset.name ?? "field").replace(/\W/g, "-")}`;
|
|
3916
|
+
}
|
|
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
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
filteredOptions(query) {
|
|
3925
|
+
const q = query.toLowerCase();
|
|
3926
|
+
return this.optionsValue.filter(
|
|
3927
|
+
(o) => q === "" || o.label.toLowerCase().includes(q) || (o.support?.toLowerCase().includes(q) ?? false)
|
|
3928
|
+
);
|
|
3929
|
+
}
|
|
3930
|
+
async fetch(query) {
|
|
3931
|
+
this.fetchAbort?.abort();
|
|
3932
|
+
this.fetchAbort = new AbortController();
|
|
3933
|
+
this.showLoading();
|
|
3934
|
+
try {
|
|
3935
|
+
const url = new URL(this.urlValue, window.location.href);
|
|
3936
|
+
if (query)
|
|
3937
|
+
url.searchParams.set("q", query);
|
|
3938
|
+
const res = await globalThis.fetch(url.toString(), {
|
|
3939
|
+
signal: this.fetchAbort.signal,
|
|
3940
|
+
headers: { Accept: "application/json" }
|
|
3941
|
+
});
|
|
3942
|
+
const data = await res.json();
|
|
3943
|
+
this.renderOptions(data);
|
|
3944
|
+
} catch (err) {
|
|
3945
|
+
if (err.name !== "AbortError") {
|
|
3946
|
+
this.renderEmpty();
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
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
|
+
this.appendToken(option);
|
|
3957
|
+
this.hideDropdown();
|
|
3958
|
+
this.inputTarget.value = "";
|
|
3959
|
+
this.inputTarget.focus();
|
|
3960
|
+
this.announce(`${option.label} selected`);
|
|
3961
|
+
this.dispatch("change", { detail: { value: option.value, label: option.label } });
|
|
3962
|
+
}
|
|
3963
|
+
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`);
|
|
3969
|
+
this.dispatch("change", { detail: { value: null } });
|
|
3970
|
+
}
|
|
3971
|
+
appendToken(option) {
|
|
3972
|
+
const name = this.element.dataset.name ?? "";
|
|
3973
|
+
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
|
+
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;
|
|
3993
|
+
input.value = option.value;
|
|
3994
|
+
this.tokensTarget.appendChild(fragment);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
removeTokenElement(value) {
|
|
3998
|
+
const token = this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
|
|
3999
|
+
token?.remove();
|
|
4000
|
+
}
|
|
4001
|
+
renderOptions(options) {
|
|
4002
|
+
this.listTarget.innerHTML = "";
|
|
4003
|
+
this.highlightedIndex = -1;
|
|
4004
|
+
if (options.length === 0) {
|
|
4005
|
+
this.renderEmpty();
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
options.forEach((option, index) => {
|
|
4009
|
+
const li = document.createElement("li");
|
|
4010
|
+
li.id = `${this.optionIdPrefix}-${index}`;
|
|
4011
|
+
li.className = "midwest-autocomplete-option";
|
|
4012
|
+
li.setAttribute("role", "option");
|
|
4013
|
+
li.setAttribute("aria-selected", this.selectedValues.has(option.value) ? "true" : "false");
|
|
4014
|
+
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
|
+
});
|
|
4023
|
+
this.listTarget.appendChild(li);
|
|
4024
|
+
});
|
|
4025
|
+
const availableCount = options.filter((o) => !this.selectedValues.has(o.value)).length;
|
|
4026
|
+
if (availableCount === 0) {
|
|
4027
|
+
this.renderEmpty("No more options");
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
this.announce(`${availableCount} option${availableCount === 1 ? "" : "s"} available`);
|
|
4031
|
+
this.showDropdown();
|
|
4032
|
+
}
|
|
4033
|
+
renderEmpty(message = "No results") {
|
|
4034
|
+
this.listTarget.innerHTML = `<li class="midwest-autocomplete-empty" aria-hidden="true">${this.escapeHtml(message)}</li>`;
|
|
4035
|
+
this.announce(message);
|
|
4036
|
+
this.showDropdown();
|
|
4037
|
+
}
|
|
4038
|
+
showLoading() {
|
|
4039
|
+
this.listTarget.innerHTML = '<li class="midwest-autocomplete-loading" aria-hidden="true">Loading\u2026</li>';
|
|
4040
|
+
this.announce("Loading");
|
|
4041
|
+
this.showDropdown();
|
|
4042
|
+
}
|
|
4043
|
+
showDropdown() {
|
|
4044
|
+
if (!this.dropdownTarget.matches(":popover-open"))
|
|
4045
|
+
this.dropdownTarget.showPopover();
|
|
4046
|
+
this.inputTarget.setAttribute("aria-expanded", "true");
|
|
4047
|
+
}
|
|
4048
|
+
hideDropdown() {
|
|
4049
|
+
if (this.dropdownTarget.matches(":popover-open"))
|
|
4050
|
+
this.dropdownTarget.hidePopover();
|
|
4051
|
+
this.inputTarget.setAttribute("aria-expanded", "false");
|
|
4052
|
+
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
4053
|
+
this.highlightedIndex = -1;
|
|
4054
|
+
}
|
|
4055
|
+
syncHighlight(items) {
|
|
4056
|
+
items.forEach((item, i) => item.classList.toggle("is-highlighted", i === this.highlightedIndex));
|
|
4057
|
+
const active = items[this.highlightedIndex];
|
|
4058
|
+
active?.scrollIntoView({ block: "nearest" });
|
|
4059
|
+
if (active) {
|
|
4060
|
+
this.inputTarget.setAttribute("aria-activedescendant", active.id);
|
|
4061
|
+
} else {
|
|
4062
|
+
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
pickItem(item) {
|
|
4066
|
+
const value = item.dataset.value ?? "";
|
|
4067
|
+
const label = item.querySelector(".midwest-autocomplete-option-label")?.textContent?.trim() ?? value;
|
|
4068
|
+
const support = item.querySelector(".midwest-autocomplete-option-support")?.textContent?.trim();
|
|
4069
|
+
this.select({ value, label, support });
|
|
4070
|
+
}
|
|
4071
|
+
announce(message) {
|
|
4072
|
+
if (this.hasStatusTarget)
|
|
4073
|
+
this.statusTarget.textContent = message;
|
|
4074
|
+
}
|
|
4075
|
+
escapeHtml(str) {
|
|
4076
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
|
|
3807
4080
|
function registerMidwestControllers(application) {
|
|
3808
4081
|
application.register("midwest-card", Card);
|
|
3809
4082
|
application.register("midwest-banner", Banner);
|
|
@@ -3824,6 +4097,7 @@ function registerMidwestControllers(application) {
|
|
|
3824
4097
|
application.register("midwest-chart", Chart);
|
|
3825
4098
|
application.register("midwest-range", Range);
|
|
3826
4099
|
application.register("midwest-rating", Rating);
|
|
4100
|
+
application.register("midwest-autocomplete", Autocomplete);
|
|
3827
4101
|
}
|
|
3828
4102
|
|
|
3829
4103
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|