@unabridged/midwest 0.24.0 → 0.24.4
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 +10 -0
- package/app/assets/javascript/midwest/map_providers.ts +61 -0
- package/app/assets/javascript/midwest/viewport_observer.ts +49 -0
- package/app/assets/javascript/midwest.js +1056 -70
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +7 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +10 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/map_providers.js +56 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/map_providers.js.map +1 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/viewport_observer.js +22 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/viewport_observer.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +4 -7
- package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +0 -6
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/form/address_component/address_component_controller.js +441 -0
- package/dist/javascript/collection/app/components/midwest/form/address_component/address_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +55 -1
- 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/intersection_observer_component/intersection_observer_component_controller.js +20 -25
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/map_component/map_component_controller.js +297 -0
- package/dist/javascript/collection/app/components/midwest/map_component/map_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/motion_component/motion_component_controller.js +32 -0
- package/dist/javascript/collection/app/components/midwest/motion_component/motion_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js +4 -12
- package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +34 -5
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/page_transition_component/page_transition_component_controller.js +38 -0
- package/dist/javascript/collection/app/components/midwest/page_transition_component/page_transition_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/popover_component/popover_component_controller.js +67 -0
- package/dist/javascript/collection/app/components/midwest/popover_component/popover_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js +10 -14
- package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js.map +1 -1
- package/dist/javascript/midwest.js +1056 -70
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -2557,7 +2557,6 @@ class Dropdown extends Controller {
|
|
|
2557
2557
|
supported: { type: Boolean, default: true },
|
|
2558
2558
|
align: { type: String, default: "right" }
|
|
2559
2559
|
};
|
|
2560
|
-
observer = new IntersectionObserver(this.computedCB.bind(this), { threshold: 1 });
|
|
2561
2560
|
cleanup;
|
|
2562
2561
|
connect() {
|
|
2563
2562
|
this.supportedValue = CSS.supports("anchor-name: --myanchor");
|
|
@@ -2623,11 +2622,6 @@ class Dropdown extends Controller {
|
|
|
2623
2622
|
(item) => item.closest('[data-controller~="midwest-dropdown"]') === this.element && getComputedStyle(item).display !== "none"
|
|
2624
2623
|
);
|
|
2625
2624
|
}
|
|
2626
|
-
computedCB(entries) {
|
|
2627
|
-
entries.forEach(async () => {
|
|
2628
|
-
this.computation();
|
|
2629
|
-
});
|
|
2630
|
-
}
|
|
2631
2625
|
closeIfOpen = () => {
|
|
2632
2626
|
this.element.hidePopover();
|
|
2633
2627
|
};
|
|
@@ -3012,7 +3006,7 @@ class Carousel extends Controller {
|
|
|
3012
3006
|
}
|
|
3013
3007
|
const atEnd = this._currentIndex >= this.slides.length - 1;
|
|
3014
3008
|
if (this.loopValue && atEnd) {
|
|
3015
|
-
this.
|
|
3009
|
+
this.trackTarget.scrollTo({ left: 0, behavior: "smooth" });
|
|
3016
3010
|
} else {
|
|
3017
3011
|
this.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
|
|
3018
3012
|
}
|
|
@@ -3026,8 +3020,7 @@ class Carousel extends Controller {
|
|
|
3026
3020
|
}
|
|
3027
3021
|
const atStart = this._currentIndex <= 0;
|
|
3028
3022
|
if (this.loopValue && atStart) {
|
|
3029
|
-
|
|
3030
|
-
last?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
3023
|
+
this.trackTarget.scrollTo({ left: (this.slides.length - 1) * this.trackTarget.clientWidth, behavior: "smooth" });
|
|
3031
3024
|
} else {
|
|
3032
3025
|
this.trackTarget.scrollBy({ left: -this.trackTarget.clientWidth, behavior: "smooth" });
|
|
3033
3026
|
}
|
|
@@ -3039,8 +3032,7 @@ class Carousel extends Controller {
|
|
|
3039
3032
|
this.activateSlide(index);
|
|
3040
3033
|
return;
|
|
3041
3034
|
}
|
|
3042
|
-
|
|
3043
|
-
slide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
3035
|
+
this.trackTarget.scrollTo({ left: index * this.trackTarget.clientWidth, behavior: "smooth" });
|
|
3044
3036
|
}
|
|
3045
3037
|
// Unlocks the next slide and advances to it. Intended for wizard mode: wire
|
|
3046
3038
|
// a "Continue" button inside a slide to this action so users progress
|
|
@@ -3144,8 +3136,7 @@ class Carousel extends Controller {
|
|
|
3144
3136
|
}
|
|
3145
3137
|
const atEnd = this._currentIndex >= this.slides.length - 1;
|
|
3146
3138
|
if (atEnd) {
|
|
3147
|
-
|
|
3148
|
-
firstSlide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
3139
|
+
this.trackTarget.scrollTo({ left: 0, behavior: "smooth" });
|
|
3149
3140
|
} else {
|
|
3150
3141
|
this.next();
|
|
3151
3142
|
}
|
|
@@ -3308,6 +3299,7 @@ class FormLiveSummary extends Controller {
|
|
|
3308
3299
|
this.form?.addEventListener("midwest-autocomplete:change", this.render);
|
|
3309
3300
|
this.form?.addEventListener("midwest-color-picker:update", this.render);
|
|
3310
3301
|
this.form?.addEventListener("midwest-repeatable:change", this.render);
|
|
3302
|
+
this.form?.addEventListener("midwest-address:geocode", this.render);
|
|
3311
3303
|
}
|
|
3312
3304
|
disconnect() {
|
|
3313
3305
|
this.form?.removeEventListener("input", this.render);
|
|
@@ -3315,6 +3307,7 @@ class FormLiveSummary extends Controller {
|
|
|
3315
3307
|
this.form?.removeEventListener("midwest-autocomplete:change", this.render);
|
|
3316
3308
|
this.form?.removeEventListener("midwest-color-picker:update", this.render);
|
|
3317
3309
|
this.form?.removeEventListener("midwest-repeatable:change", this.render);
|
|
3310
|
+
this.form?.removeEventListener("midwest-address:geocode", this.render);
|
|
3318
3311
|
}
|
|
3319
3312
|
get form() {
|
|
3320
3313
|
return this.element.closest("form");
|
|
@@ -3332,7 +3325,7 @@ class FormLiveSummary extends Controller {
|
|
|
3332
3325
|
}
|
|
3333
3326
|
const dl = document.createElement("dl");
|
|
3334
3327
|
dl.className = "midwest-form-live-summary-list";
|
|
3335
|
-
fields.forEach(({ label, value, swatchColor }) => {
|
|
3328
|
+
fields.forEach(({ label, value, swatchColor, details }) => {
|
|
3336
3329
|
const dt = Object.assign(document.createElement("dt"), {
|
|
3337
3330
|
className: "midwest-form-live-summary-term",
|
|
3338
3331
|
textContent: label
|
|
@@ -3341,6 +3334,8 @@ class FormLiveSummary extends Controller {
|
|
|
3341
3334
|
dd.className = "midwest-form-live-summary-definition";
|
|
3342
3335
|
if (swatchColor !== void 0) {
|
|
3343
3336
|
dd.append(this.buildSwatch(swatchColor), value || "\u2014");
|
|
3337
|
+
} else if (details && Object.keys(details).length > 0) {
|
|
3338
|
+
dd.append(this.buildGeocode(value || "\u2014", details));
|
|
3344
3339
|
} else {
|
|
3345
3340
|
dd.textContent = value || "\u2014";
|
|
3346
3341
|
}
|
|
@@ -3368,6 +3363,8 @@ class FormLiveSummary extends Controller {
|
|
|
3368
3363
|
return;
|
|
3369
3364
|
if (element.closest(".midwest-repeatable"))
|
|
3370
3365
|
return;
|
|
3366
|
+
if (element.closest(".midwest-address"))
|
|
3367
|
+
return;
|
|
3371
3368
|
if (element instanceof HTMLInputElement) {
|
|
3372
3369
|
if (SKIPPED_INPUT_TYPES.has(element.type))
|
|
3373
3370
|
return;
|
|
@@ -3487,6 +3484,29 @@ class FormLiveSummary extends Controller {
|
|
|
3487
3484
|
});
|
|
3488
3485
|
});
|
|
3489
3486
|
});
|
|
3487
|
+
form.querySelectorAll(".midwest-address").forEach((addr) => {
|
|
3488
|
+
if (this.element.contains(addr))
|
|
3489
|
+
return;
|
|
3490
|
+
const input = addr.querySelector('[data-midwest-address-target="input"]');
|
|
3491
|
+
const latInput = addr.querySelector('[data-midwest-address-target="latInput"]');
|
|
3492
|
+
const lngInput = addr.querySelector('[data-midwest-address-target="lngInput"]');
|
|
3493
|
+
const placeIdInput = addr.querySelector('[data-midwest-address-target="placeIdInput"]');
|
|
3494
|
+
const labelEl = addr.querySelector(".midwest-form-group__top label");
|
|
3495
|
+
const label = labelEl?.textContent?.trim() || this.prettifyName((input?.name ?? latInput?.name?.replace(/_latitude$/, "") ?? "").replace(/\[.*\]$/, ""));
|
|
3496
|
+
const value = input?.value?.trim() || "";
|
|
3497
|
+
const details = {};
|
|
3498
|
+
if (latInput?.value)
|
|
3499
|
+
details["Latitude"] = latInput.value;
|
|
3500
|
+
if (lngInput?.value)
|
|
3501
|
+
details["Longitude"] = lngInput.value;
|
|
3502
|
+
if (placeIdInput?.value)
|
|
3503
|
+
details["Place ID"] = placeIdInput.value;
|
|
3504
|
+
fields.push({
|
|
3505
|
+
label,
|
|
3506
|
+
value: value || "\u2014",
|
|
3507
|
+
details: Object.keys(details).length > 0 ? details : void 0
|
|
3508
|
+
});
|
|
3509
|
+
});
|
|
3490
3510
|
return fields;
|
|
3491
3511
|
}
|
|
3492
3512
|
// Builds a small inline colour swatch to display alongside a colour value.
|
|
@@ -3505,6 +3525,31 @@ class FormLiveSummary extends Controller {
|
|
|
3505
3525
|
}
|
|
3506
3526
|
return swatch;
|
|
3507
3527
|
}
|
|
3528
|
+
// Wraps an address value in a span that reveals a geocode detail popup on hover.
|
|
3529
|
+
buildGeocode(value, details) {
|
|
3530
|
+
const wrapper = document.createElement("span");
|
|
3531
|
+
wrapper.className = "midwest-form-live-summary-geocode";
|
|
3532
|
+
const text = document.createElement("span");
|
|
3533
|
+
text.className = "midwest-form-live-summary-geocode-text";
|
|
3534
|
+
text.textContent = value;
|
|
3535
|
+
const popup = document.createElement("span");
|
|
3536
|
+
popup.className = "midwest-form-live-summary-geocode-popup";
|
|
3537
|
+
popup.setAttribute("aria-hidden", "true");
|
|
3538
|
+
Object.entries(details).forEach(([key, val]) => {
|
|
3539
|
+
const row = document.createElement("span");
|
|
3540
|
+
row.className = "midwest-form-live-summary-geocode-row";
|
|
3541
|
+
const keyEl = document.createElement("span");
|
|
3542
|
+
keyEl.className = "midwest-form-live-summary-geocode-key";
|
|
3543
|
+
keyEl.textContent = key;
|
|
3544
|
+
const valEl = document.createElement("span");
|
|
3545
|
+
valEl.className = "midwest-form-live-summary-geocode-val";
|
|
3546
|
+
valEl.textContent = val;
|
|
3547
|
+
row.append(keyEl, valEl);
|
|
3548
|
+
popup.append(row);
|
|
3549
|
+
});
|
|
3550
|
+
wrapper.append(text, popup);
|
|
3551
|
+
return wrapper;
|
|
3552
|
+
}
|
|
3508
3553
|
// Finds the label associated with a specific element via `for` attribute,
|
|
3509
3554
|
// wrapping label, or falls back to a prettified version of the element name.
|
|
3510
3555
|
labelFor(el) {
|
|
@@ -4871,10 +4916,7 @@ class Notification extends Controller {
|
|
|
4871
4916
|
connect() {
|
|
4872
4917
|
this.container = this.element.closest("[popover]");
|
|
4873
4918
|
this.container?.showPopover?.();
|
|
4874
|
-
|
|
4875
|
-
this.element.classList.add("visible");
|
|
4876
|
-
});
|
|
4877
|
-
const frame = this.element.parentElement;
|
|
4919
|
+
const frame = this.element.closest("turbo-frame");
|
|
4878
4920
|
if (frame) {
|
|
4879
4921
|
this.observer = new MutationObserver(() => this.syncCountdown());
|
|
4880
4922
|
this.observer.observe(frame, { childList: true });
|
|
@@ -4893,14 +4935,8 @@ class Notification extends Controller {
|
|
|
4893
4935
|
}
|
|
4894
4936
|
}
|
|
4895
4937
|
close() {
|
|
4896
|
-
if (!this.element.classList.contains("animated")) {
|
|
4897
|
-
this.element.remove();
|
|
4898
|
-
return;
|
|
4899
|
-
}
|
|
4900
4938
|
this.element.classList.add("closing");
|
|
4901
|
-
setTimeout(() =>
|
|
4902
|
-
this.element.remove();
|
|
4903
|
-
}, 350);
|
|
4939
|
+
setTimeout(() => (this.element.parentElement ?? this.element).remove(), 350);
|
|
4904
4940
|
}
|
|
4905
4941
|
syncCountdown() {
|
|
4906
4942
|
const timerEl = this.element.querySelector('[data-controller~="midwest-countdown-timer"]');
|
|
@@ -4912,7 +4948,8 @@ class Notification extends Controller {
|
|
|
4912
4948
|
);
|
|
4913
4949
|
if (!timer)
|
|
4914
4950
|
return;
|
|
4915
|
-
const
|
|
4951
|
+
const wrapper = this.element.parentElement ?? this.element;
|
|
4952
|
+
const isTop = wrapper === wrapper.parentElement?.firstElementChild;
|
|
4916
4953
|
if (isTop) {
|
|
4917
4954
|
timer.resume();
|
|
4918
4955
|
} else {
|
|
@@ -4986,36 +5023,51 @@ class Table extends Controller {
|
|
|
4986
5023
|
}
|
|
4987
5024
|
}
|
|
4988
5025
|
|
|
5026
|
+
function observeViewport(element, options = {}) {
|
|
5027
|
+
const { threshold = 0, rootMargin = "0px", once = false, onEnter, onLeave } = options;
|
|
5028
|
+
const observer = new IntersectionObserver(
|
|
5029
|
+
(entries) => {
|
|
5030
|
+
for (const entry of entries) {
|
|
5031
|
+
if (entry.isIntersecting) {
|
|
5032
|
+
onEnter?.(entry);
|
|
5033
|
+
if (once)
|
|
5034
|
+
observer.disconnect();
|
|
5035
|
+
} else {
|
|
5036
|
+
onLeave?.(entry);
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
},
|
|
5040
|
+
{ threshold, rootMargin }
|
|
5041
|
+
);
|
|
5042
|
+
observer.observe(element);
|
|
5043
|
+
return () => observer.disconnect();
|
|
5044
|
+
}
|
|
5045
|
+
|
|
4989
5046
|
class LoadMore extends Controller {
|
|
4990
5047
|
static values = {
|
|
4991
5048
|
url: String,
|
|
4992
5049
|
tbodyId: String
|
|
4993
5050
|
};
|
|
4994
|
-
|
|
5051
|
+
teardown = null;
|
|
4995
5052
|
loading = false;
|
|
4996
5053
|
connect() {
|
|
4997
5054
|
if (!this.urlValue)
|
|
4998
5055
|
return;
|
|
4999
|
-
this.
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
// Start fetching 300px before the sentinel enters the viewport so rows
|
|
5005
|
-
// appear before the user actually reaches the bottom.
|
|
5006
|
-
{ rootMargin: "0px 0px 300px 0px" }
|
|
5007
|
-
);
|
|
5008
|
-
this.observer.observe(this.element);
|
|
5056
|
+
this.teardown = observeViewport(this.element, {
|
|
5057
|
+
rootMargin: "0px 0px 300px 0px",
|
|
5058
|
+
once: true,
|
|
5059
|
+
onEnter: () => this.load()
|
|
5060
|
+
});
|
|
5009
5061
|
}
|
|
5010
5062
|
disconnect() {
|
|
5011
|
-
this.
|
|
5012
|
-
this.
|
|
5063
|
+
this.teardown?.();
|
|
5064
|
+
this.teardown = null;
|
|
5013
5065
|
}
|
|
5014
5066
|
async load() {
|
|
5015
5067
|
if (this.loading)
|
|
5016
5068
|
return;
|
|
5017
5069
|
this.loading = true;
|
|
5018
|
-
this.
|
|
5070
|
+
this.teardown?.();
|
|
5019
5071
|
try {
|
|
5020
5072
|
const response = await fetch(this.urlValue, {
|
|
5021
5073
|
headers: {
|
|
@@ -6947,35 +6999,29 @@ class ViewportObserver extends Controller {
|
|
|
6947
6999
|
once: { type: Boolean, default: false },
|
|
6948
7000
|
visibleClass: { type: String, default: "" }
|
|
6949
7001
|
};
|
|
6950
|
-
|
|
7002
|
+
teardown = null;
|
|
6951
7003
|
connect() {
|
|
6952
|
-
this.
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
7004
|
+
this.teardown = observeViewport(this.element, {
|
|
7005
|
+
threshold: this.thresholdValue,
|
|
7006
|
+
rootMargin: this.rootMarginValue,
|
|
7007
|
+
once: this.onceValue,
|
|
7008
|
+
onEnter: (entry) => {
|
|
7009
|
+
this.element.setAttribute("data-intersecting", "true");
|
|
7010
|
+
if (this.visibleClassValue)
|
|
7011
|
+
this.element.classList.add(this.visibleClassValue);
|
|
7012
|
+
this.dispatch("enter", { detail: { entry }, bubbles: true });
|
|
6956
7013
|
},
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
7014
|
+
onLeave: (entry) => {
|
|
7015
|
+
this.element.setAttribute("data-intersecting", "false");
|
|
7016
|
+
if (this.visibleClassValue)
|
|
7017
|
+
this.element.classList.remove(this.visibleClassValue);
|
|
7018
|
+
this.dispatch("leave", { detail: { entry }, bubbles: true });
|
|
7019
|
+
}
|
|
7020
|
+
});
|
|
6960
7021
|
}
|
|
6961
7022
|
disconnect() {
|
|
6962
|
-
this.
|
|
6963
|
-
this.
|
|
6964
|
-
}
|
|
6965
|
-
handle(entry) {
|
|
6966
|
-
if (entry.isIntersecting) {
|
|
6967
|
-
this.element.setAttribute("data-intersecting", "true");
|
|
6968
|
-
if (this.visibleClassValue)
|
|
6969
|
-
this.element.classList.add(this.visibleClassValue);
|
|
6970
|
-
this.dispatch("enter", { detail: { entry }, bubbles: true });
|
|
6971
|
-
if (this.onceValue)
|
|
6972
|
-
this.disconnect();
|
|
6973
|
-
} else {
|
|
6974
|
-
this.element.setAttribute("data-intersecting", "false");
|
|
6975
|
-
if (this.visibleClassValue)
|
|
6976
|
-
this.element.classList.remove(this.visibleClassValue);
|
|
6977
|
-
this.dispatch("leave", { detail: { entry }, bubbles: true });
|
|
6978
|
-
}
|
|
7023
|
+
this.teardown?.();
|
|
7024
|
+
this.teardown = null;
|
|
6979
7025
|
}
|
|
6980
7026
|
}
|
|
6981
7027
|
|
|
@@ -7068,14 +7114,14 @@ const FOCUSABLE = [
|
|
|
7068
7114
|
].join(", ");
|
|
7069
7115
|
const GAP = 12;
|
|
7070
7116
|
class Onboarding extends Controller {
|
|
7071
|
-
static targets = ["step", "overlay", "card", "title", "body", "back", "next", "done", "arrow"];
|
|
7117
|
+
static targets = ["step", "overlay", "card", "title", "body", "back", "next", "done", "arrow", "motion"];
|
|
7072
7118
|
static values = {
|
|
7073
|
-
autoOpen: { type: Boolean, default: false }
|
|
7074
|
-
confirm: { type: String, default: "" }
|
|
7119
|
+
autoOpen: { type: Boolean, default: false }
|
|
7075
7120
|
};
|
|
7076
7121
|
steps = [];
|
|
7077
7122
|
index = 0;
|
|
7078
7123
|
active = false;
|
|
7124
|
+
justOpened = false;
|
|
7079
7125
|
previousFocus = null;
|
|
7080
7126
|
highlighted = null;
|
|
7081
7127
|
connect() {
|
|
@@ -7105,6 +7151,7 @@ class Onboarding extends Controller {
|
|
|
7105
7151
|
this.previousFocus = document.activeElement;
|
|
7106
7152
|
this.overlayTarget.hidden = false;
|
|
7107
7153
|
this.cardTarget.hidden = false;
|
|
7154
|
+
this.justOpened = true;
|
|
7108
7155
|
document.addEventListener("keydown", this.handleKeydown, true);
|
|
7109
7156
|
window.addEventListener("resize", this.reposition);
|
|
7110
7157
|
window.addEventListener("scroll", this.reposition, true);
|
|
@@ -7139,14 +7186,14 @@ class Onboarding extends Controller {
|
|
|
7139
7186
|
cancel() {
|
|
7140
7187
|
if (!this.active)
|
|
7141
7188
|
return;
|
|
7142
|
-
if (this.confirmValue && !window.confirm(this.confirmValue))
|
|
7143
|
-
return;
|
|
7144
7189
|
this.teardown();
|
|
7145
7190
|
this.dispatch("cancel", { bubbles: true });
|
|
7146
7191
|
}
|
|
7147
7192
|
render(index) {
|
|
7148
7193
|
this.index = index;
|
|
7149
7194
|
const step = this.steps[index];
|
|
7195
|
+
const replay = !this.justOpened;
|
|
7196
|
+
this.justOpened = false;
|
|
7150
7197
|
this.clearHighlight();
|
|
7151
7198
|
this.titleTarget.textContent = step.title;
|
|
7152
7199
|
this.renderBody(step);
|
|
@@ -7158,6 +7205,9 @@ class Onboarding extends Controller {
|
|
|
7158
7205
|
} else {
|
|
7159
7206
|
this.center();
|
|
7160
7207
|
}
|
|
7208
|
+
if (replay && this.hasMotionTarget) {
|
|
7209
|
+
this.replayMotion(this.cardTarget.dataset.placement ?? "center");
|
|
7210
|
+
}
|
|
7161
7211
|
this.dispatch("show", { detail: { index, name: step.name }, bubbles: true });
|
|
7162
7212
|
this.focusCard();
|
|
7163
7213
|
}
|
|
@@ -7259,9 +7309,34 @@ class Onboarding extends Controller {
|
|
|
7259
7309
|
this.cardTarget.focus();
|
|
7260
7310
|
});
|
|
7261
7311
|
}
|
|
7312
|
+
// Re-triggers the midwest_motion @starting-style entry on step transitions.
|
|
7313
|
+
// The initial show is handled by @starting-style firing when the card's
|
|
7314
|
+
// `hidden` is removed; subsequent steps need an explicit re-trigger since the
|
|
7315
|
+
// card stays in the DOM. Toggling `hidden` on the motion element triggers
|
|
7316
|
+
// @starting-style again; the rAF ensures both DOM writes land in the same
|
|
7317
|
+
// paint so there is no blank-card flash.
|
|
7318
|
+
replayMotion(placement) {
|
|
7319
|
+
const el = this.motionTarget;
|
|
7320
|
+
const dir = { bottom: "up", top: "down", right: "left", left: "right" };
|
|
7321
|
+
el.classList.remove("from-up", "from-down", "from-left", "from-right");
|
|
7322
|
+
const from = dir[placement];
|
|
7323
|
+
if (from)
|
|
7324
|
+
el.classList.add(`from-${from}`);
|
|
7325
|
+
el.hidden = true;
|
|
7326
|
+
requestAnimationFrame(() => {
|
|
7327
|
+
if (this.active)
|
|
7328
|
+
el.hidden = false;
|
|
7329
|
+
});
|
|
7330
|
+
}
|
|
7262
7331
|
teardown() {
|
|
7263
7332
|
this.active = false;
|
|
7264
7333
|
this.clearHighlight();
|
|
7334
|
+
this.element.querySelectorAll("[popover]").forEach((el) => {
|
|
7335
|
+
try {
|
|
7336
|
+
el.hidePopover();
|
|
7337
|
+
} catch {
|
|
7338
|
+
}
|
|
7339
|
+
});
|
|
7265
7340
|
this.overlayTarget.hidden = true;
|
|
7266
7341
|
this.cardTarget.hidden = true;
|
|
7267
7342
|
document.removeEventListener("keydown", this.handleKeydown, true);
|
|
@@ -7873,6 +7948,912 @@ class Playlist extends Controller {
|
|
|
7873
7948
|
}
|
|
7874
7949
|
}
|
|
7875
7950
|
|
|
7951
|
+
class Motion extends Controller {
|
|
7952
|
+
static values = {
|
|
7953
|
+
threshold: { type: Number, default: 0.15 },
|
|
7954
|
+
once: { type: Boolean, default: true }
|
|
7955
|
+
};
|
|
7956
|
+
teardown = null;
|
|
7957
|
+
connect() {
|
|
7958
|
+
this.teardown = observeViewport(this.element, {
|
|
7959
|
+
threshold: this.thresholdValue,
|
|
7960
|
+
once: this.onceValue,
|
|
7961
|
+
onEnter: () => this.reveal(),
|
|
7962
|
+
onLeave: this.onceValue ? void 0 : () => this.reset()
|
|
7963
|
+
});
|
|
7964
|
+
}
|
|
7965
|
+
disconnect() {
|
|
7966
|
+
this.teardown?.();
|
|
7967
|
+
this.teardown = null;
|
|
7968
|
+
}
|
|
7969
|
+
reveal() {
|
|
7970
|
+
this.element.setAttribute("data-motion-visible", "");
|
|
7971
|
+
this.dispatch("reveal", { bubbles: true });
|
|
7972
|
+
}
|
|
7973
|
+
reset() {
|
|
7974
|
+
this.element.removeAttribute("data-motion-visible");
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
|
|
7978
|
+
class PageTransition extends Controller {
|
|
7979
|
+
static values = {
|
|
7980
|
+
style: { type: String, default: "fade" },
|
|
7981
|
+
duration: { type: String, default: "" },
|
|
7982
|
+
easing: { type: String, default: "" }
|
|
7983
|
+
};
|
|
7984
|
+
injectedMeta = null;
|
|
7985
|
+
connect() {
|
|
7986
|
+
if (this.styleValue === "none")
|
|
7987
|
+
return;
|
|
7988
|
+
if (!document.querySelector('meta[name="view-transition"]')) {
|
|
7989
|
+
const meta = document.createElement("meta");
|
|
7990
|
+
meta.name = "view-transition";
|
|
7991
|
+
meta.content = "same-origin";
|
|
7992
|
+
document.head.appendChild(meta);
|
|
7993
|
+
this.injectedMeta = meta;
|
|
7994
|
+
}
|
|
7995
|
+
document.documentElement.dataset.midwestPageTransition = this.styleValue;
|
|
7996
|
+
if (this.durationValue) {
|
|
7997
|
+
document.documentElement.style.setProperty("--page-transition-duration", this.durationValue);
|
|
7998
|
+
}
|
|
7999
|
+
if (this.easingValue) {
|
|
8000
|
+
document.documentElement.style.setProperty("--page-transition-easing", this.easingValue);
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
disconnect() {
|
|
8004
|
+
this.injectedMeta?.remove();
|
|
8005
|
+
this.injectedMeta = null;
|
|
8006
|
+
delete document.documentElement.dataset.midwestPageTransition;
|
|
8007
|
+
document.documentElement.style.removeProperty("--page-transition-duration");
|
|
8008
|
+
document.documentElement.style.removeProperty("--page-transition-easing");
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
|
|
8012
|
+
let mapkitScriptPromise = null;
|
|
8013
|
+
let googleMapsScriptPromise = null;
|
|
8014
|
+
let leafletScriptPromise = null;
|
|
8015
|
+
function loadMapKitScript() {
|
|
8016
|
+
if (window.mapkit)
|
|
8017
|
+
return Promise.resolve();
|
|
8018
|
+
if (mapkitScriptPromise)
|
|
8019
|
+
return mapkitScriptPromise;
|
|
8020
|
+
mapkitScriptPromise = new Promise((resolve, reject) => {
|
|
8021
|
+
const script = document.createElement("script");
|
|
8022
|
+
script.src = "https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js";
|
|
8023
|
+
script.crossOrigin = "anonymous";
|
|
8024
|
+
script.addEventListener("load", () => resolve());
|
|
8025
|
+
script.addEventListener("error", () => reject(new Error("Failed to load MapKit JS")));
|
|
8026
|
+
document.head.appendChild(script);
|
|
8027
|
+
});
|
|
8028
|
+
return mapkitScriptPromise;
|
|
8029
|
+
}
|
|
8030
|
+
function loadLeafletScript() {
|
|
8031
|
+
if (window.L)
|
|
8032
|
+
return Promise.resolve();
|
|
8033
|
+
if (leafletScriptPromise)
|
|
8034
|
+
return leafletScriptPromise;
|
|
8035
|
+
leafletScriptPromise = new Promise((resolve, reject) => {
|
|
8036
|
+
if (!document.querySelector("link[data-midwest-leaflet]")) {
|
|
8037
|
+
const link = document.createElement("link");
|
|
8038
|
+
link.rel = "stylesheet";
|
|
8039
|
+
link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
|
|
8040
|
+
link.dataset.midwestLeaflet = "";
|
|
8041
|
+
document.head.appendChild(link);
|
|
8042
|
+
}
|
|
8043
|
+
const script = document.createElement("script");
|
|
8044
|
+
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
|
|
8045
|
+
script.addEventListener("load", () => resolve());
|
|
8046
|
+
script.addEventListener("error", () => reject(new Error("Failed to load Leaflet")));
|
|
8047
|
+
document.head.appendChild(script);
|
|
8048
|
+
});
|
|
8049
|
+
return leafletScriptPromise;
|
|
8050
|
+
}
|
|
8051
|
+
function loadGoogleMapsScript(apiKey) {
|
|
8052
|
+
if (window.google?.maps)
|
|
8053
|
+
return Promise.resolve();
|
|
8054
|
+
if (googleMapsScriptPromise)
|
|
8055
|
+
return googleMapsScriptPromise;
|
|
8056
|
+
googleMapsScriptPromise = new Promise((resolve, reject) => {
|
|
8057
|
+
window.__midwestGoogleMapsReady = resolve;
|
|
8058
|
+
const script = document.createElement("script");
|
|
8059
|
+
script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&callback=__midwestGoogleMapsReady&loading=async&libraries=places`;
|
|
8060
|
+
script.addEventListener("error", () => reject(new Error("Failed to load Google Maps")));
|
|
8061
|
+
document.head.appendChild(script);
|
|
8062
|
+
});
|
|
8063
|
+
return googleMapsScriptPromise;
|
|
8064
|
+
}
|
|
8065
|
+
|
|
8066
|
+
const LEAFLET_TILES = {
|
|
8067
|
+
MutedStandard: {
|
|
8068
|
+
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
|
8069
|
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
|
8070
|
+
},
|
|
8071
|
+
Standard: {
|
|
8072
|
+
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
8073
|
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
8074
|
+
maxZoom: 19
|
|
8075
|
+
},
|
|
8076
|
+
Hybrid: {
|
|
8077
|
+
url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
|
|
8078
|
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
|
8079
|
+
},
|
|
8080
|
+
Satellite: {
|
|
8081
|
+
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
|
8082
|
+
attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",
|
|
8083
|
+
maxZoom: 18
|
|
8084
|
+
},
|
|
8085
|
+
Dark: {
|
|
8086
|
+
url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
|
|
8087
|
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
|
8088
|
+
}
|
|
8089
|
+
};
|
|
8090
|
+
const GOOGLE_MUTED_STYLES = [
|
|
8091
|
+
{ elementType: "geometry", stylers: [{ color: "#f5f5f5" }] },
|
|
8092
|
+
{ elementType: "labels.icon", stylers: [{ visibility: "off" }] },
|
|
8093
|
+
{ elementType: "labels.text.fill", stylers: [{ color: "#616161" }] },
|
|
8094
|
+
{ elementType: "labels.text.stroke", stylers: [{ color: "#f5f5f5" }] },
|
|
8095
|
+
{ featureType: "administrative.land_parcel", elementType: "labels", stylers: [{ visibility: "off" }] },
|
|
8096
|
+
{ featureType: "poi", elementType: "geometry", stylers: [{ color: "#eeeeee" }] },
|
|
8097
|
+
{ featureType: "poi.park", elementType: "geometry", stylers: [{ color: "#e5e5e5" }] },
|
|
8098
|
+
{ featureType: "road", elementType: "geometry", stylers: [{ color: "#ffffff" }] },
|
|
8099
|
+
{ featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#dadada" }] },
|
|
8100
|
+
{ featureType: "water", elementType: "geometry", stylers: [{ color: "#c9c9c9" }] }
|
|
8101
|
+
];
|
|
8102
|
+
const GOOGLE_DARK_STYLES = [
|
|
8103
|
+
{ elementType: "geometry", stylers: [{ color: "#242f3e" }] },
|
|
8104
|
+
{ elementType: "labels.text.stroke", stylers: [{ color: "#242f3e" }] },
|
|
8105
|
+
{ elementType: "labels.text.fill", stylers: [{ color: "#746855" }] },
|
|
8106
|
+
{ featureType: "administrative.locality", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }] },
|
|
8107
|
+
{ featureType: "poi", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }] },
|
|
8108
|
+
{ featureType: "poi.park", elementType: "geometry", stylers: [{ color: "#263c3f" }] },
|
|
8109
|
+
{ featureType: "poi.park", elementType: "labels.text.fill", stylers: [{ color: "#6b9a76" }] },
|
|
8110
|
+
{ featureType: "road", elementType: "geometry", stylers: [{ color: "#38414e" }] },
|
|
8111
|
+
{ featureType: "road", elementType: "geometry.stroke", stylers: [{ color: "#212a37" }] },
|
|
8112
|
+
{ featureType: "road", elementType: "labels.text.fill", stylers: [{ color: "#9ca5b3" }] },
|
|
8113
|
+
{ featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#746855" }] },
|
|
8114
|
+
{ featureType: "road.highway", elementType: "geometry.stroke", stylers: [{ color: "#1f2835" }] },
|
|
8115
|
+
{ featureType: "road.highway", elementType: "labels.text.fill", stylers: [{ color: "#f3d19c" }] },
|
|
8116
|
+
{ featureType: "transit", elementType: "geometry", stylers: [{ color: "#2f3948" }] },
|
|
8117
|
+
{ featureType: "transit.station", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }] },
|
|
8118
|
+
{ featureType: "water", elementType: "geometry", stylers: [{ color: "#17263c" }] },
|
|
8119
|
+
{ featureType: "water", elementType: "labels.text.fill", stylers: [{ color: "#515c6d" }] },
|
|
8120
|
+
{ featureType: "water", elementType: "labels.text.stroke", stylers: [{ color: "#17263c" }] }
|
|
8121
|
+
];
|
|
8122
|
+
let Map$1 = class Map extends Controller {
|
|
8123
|
+
static values = {
|
|
8124
|
+
latitude: { type: Number, default: 37.334886 },
|
|
8125
|
+
longitude: { type: Number, default: -122.008988 },
|
|
8126
|
+
span: { type: Number, default: 0.1 },
|
|
8127
|
+
mapType: { type: String, default: "MutedStandard" },
|
|
8128
|
+
colorScheme: { type: String, default: "Auto" },
|
|
8129
|
+
provider: { type: String, default: "" },
|
|
8130
|
+
token: { type: String, default: "" },
|
|
8131
|
+
tokenUrl: { type: String, default: "" },
|
|
8132
|
+
apiKey: { type: String, default: "" },
|
|
8133
|
+
zoom: { type: Number, default: -1 },
|
|
8134
|
+
markers: { type: Array, default: [] },
|
|
8135
|
+
showsUserLocation: { type: Boolean, default: false },
|
|
8136
|
+
showsMapTypeControl: { type: Boolean, default: false },
|
|
8137
|
+
showsZoomControl: { type: Boolean, default: true },
|
|
8138
|
+
showsCompass: { type: String, default: "Adaptive" },
|
|
8139
|
+
rotation: { type: Number, default: 0 }
|
|
8140
|
+
};
|
|
8141
|
+
mapInstance = null;
|
|
8142
|
+
async connect() {
|
|
8143
|
+
const provider = this.providerValue;
|
|
8144
|
+
if (!provider)
|
|
8145
|
+
return;
|
|
8146
|
+
try {
|
|
8147
|
+
if (provider === "mapkit") {
|
|
8148
|
+
await loadMapKitScript();
|
|
8149
|
+
this.initMapKit();
|
|
8150
|
+
this.initMapKitMap();
|
|
8151
|
+
} else if (provider === "google") {
|
|
8152
|
+
await loadGoogleMapsScript(this.apiKeyValue);
|
|
8153
|
+
this.initGoogleMap();
|
|
8154
|
+
} else if (provider === "leaflet") {
|
|
8155
|
+
await loadLeafletScript();
|
|
8156
|
+
this.initLeafletMap();
|
|
8157
|
+
}
|
|
8158
|
+
} catch (err) {
|
|
8159
|
+
console.error(`[midwest-map] Failed to initialise ${provider} map:`, err);
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
disconnect() {
|
|
8163
|
+
if (this.mapInstance) {
|
|
8164
|
+
if (this.providerValue === "mapkit")
|
|
8165
|
+
this.mapInstance.destroy();
|
|
8166
|
+
if (this.providerValue === "leaflet")
|
|
8167
|
+
this.mapInstance.remove();
|
|
8168
|
+
this.mapInstance = null;
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
tokenValueChanged() {
|
|
8172
|
+
if (this.mapInstance && this.providerValue === "mapkit")
|
|
8173
|
+
this.initMapKit();
|
|
8174
|
+
}
|
|
8175
|
+
// --- MapKit ---
|
|
8176
|
+
initMapKit() {
|
|
8177
|
+
const mk = window.mapkit;
|
|
8178
|
+
const token = this.tokenValue;
|
|
8179
|
+
const tokenUrl = this.tokenUrlValue;
|
|
8180
|
+
mk.init({
|
|
8181
|
+
authorizationCallback: (done) => {
|
|
8182
|
+
if (token) {
|
|
8183
|
+
done(token);
|
|
8184
|
+
} else if (tokenUrl) {
|
|
8185
|
+
fetch(tokenUrl).then((r) => r.text()).then(done).catch((err) => console.error("[midwest-map] Token fetch failed:", err));
|
|
8186
|
+
}
|
|
8187
|
+
},
|
|
8188
|
+
language: document.documentElement.lang || "en"
|
|
8189
|
+
});
|
|
8190
|
+
}
|
|
8191
|
+
initMapKitMap() {
|
|
8192
|
+
const mk = window.mapkit;
|
|
8193
|
+
const region = new mk.CoordinateRegion(
|
|
8194
|
+
new mk.Coordinate(this.latitudeValue, this.longitudeValue),
|
|
8195
|
+
new mk.CoordinateSpan(this.spanValue, this.spanValue)
|
|
8196
|
+
);
|
|
8197
|
+
const colorScheme = this.colorSchemeValue === "Auto" ? window.matchMedia("(prefers-color-scheme: dark)").matches ? mk.Map.ColorSchemes.Dark : mk.Map.ColorSchemes.Light : mk.Map.ColorSchemes[this.colorSchemeValue];
|
|
8198
|
+
this.mapInstance = new mk.Map(this.element, {
|
|
8199
|
+
region,
|
|
8200
|
+
mapType: mk.Map.MapTypes[this.mapTypeValue],
|
|
8201
|
+
colorScheme,
|
|
8202
|
+
showsUserLocation: this.showsUserLocationValue,
|
|
8203
|
+
showsMapTypeControl: this.showsMapTypeControlValue,
|
|
8204
|
+
showsZoomControl: this.showsZoomControlValue,
|
|
8205
|
+
showsCompass: mk.FeatureVisibility[this.showsCompassValue],
|
|
8206
|
+
rotation: this.rotationValue
|
|
8207
|
+
});
|
|
8208
|
+
this.addMapKitMarkers();
|
|
8209
|
+
this.element.dataset.mapLoaded = "true";
|
|
8210
|
+
}
|
|
8211
|
+
addMapKitMarkers() {
|
|
8212
|
+
const mk = window.mapkit;
|
|
8213
|
+
if (!this.markersValue.length)
|
|
8214
|
+
return;
|
|
8215
|
+
const annotations = this.markersValue.map((m) => {
|
|
8216
|
+
const opts = {};
|
|
8217
|
+
if (m.title)
|
|
8218
|
+
opts.title = m.title;
|
|
8219
|
+
if (m.subtitle)
|
|
8220
|
+
opts.subtitle = m.subtitle;
|
|
8221
|
+
if (m.color)
|
|
8222
|
+
opts.color = m.color;
|
|
8223
|
+
if (m.glyphText)
|
|
8224
|
+
opts.glyphText = m.glyphText;
|
|
8225
|
+
return new mk.MarkerAnnotation(
|
|
8226
|
+
new mk.Coordinate(m.latitude, m.longitude),
|
|
8227
|
+
opts
|
|
8228
|
+
);
|
|
8229
|
+
});
|
|
8230
|
+
this.mapInstance.addAnnotations(annotations);
|
|
8231
|
+
const openIndex = this.markersValue.findIndex((m) => m.open);
|
|
8232
|
+
if (openIndex >= 0) {
|
|
8233
|
+
this.mapInstance.selectedAnnotation = annotations[openIndex];
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
// --- Google Maps ---
|
|
8237
|
+
initGoogleMap() {
|
|
8238
|
+
const g = window.google;
|
|
8239
|
+
const zoom = this.zoomValue > 0 ? this.zoomValue : this.spanToZoom(this.spanValue);
|
|
8240
|
+
const isDark = this.colorSchemeValue === "Dark" || this.colorSchemeValue === "Auto" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
8241
|
+
const mapTypeId = this.googleMapTypeId();
|
|
8242
|
+
const opts = {
|
|
8243
|
+
center: { lat: this.latitudeValue, lng: this.longitudeValue },
|
|
8244
|
+
zoom,
|
|
8245
|
+
mapTypeId,
|
|
8246
|
+
disableDefaultUI: true,
|
|
8247
|
+
zoomControl: this.showsZoomControlValue,
|
|
8248
|
+
mapTypeControl: this.showsMapTypeControlValue,
|
|
8249
|
+
fullscreenControl: false,
|
|
8250
|
+
streetViewControl: false
|
|
8251
|
+
};
|
|
8252
|
+
const styles = this.googleMapStyles(isDark, mapTypeId);
|
|
8253
|
+
if (styles)
|
|
8254
|
+
opts.styles = styles;
|
|
8255
|
+
this.mapInstance = new g.maps.Map(this.element, opts);
|
|
8256
|
+
this.addGoogleMarkers();
|
|
8257
|
+
this.element.dataset.mapLoaded = "true";
|
|
8258
|
+
}
|
|
8259
|
+
googleMapTypeId() {
|
|
8260
|
+
const types = {
|
|
8261
|
+
Standard: "roadmap",
|
|
8262
|
+
Hybrid: "hybrid",
|
|
8263
|
+
Satellite: "satellite",
|
|
8264
|
+
MutedStandard: "roadmap"
|
|
8265
|
+
};
|
|
8266
|
+
return types[this.mapTypeValue] ?? "roadmap";
|
|
8267
|
+
}
|
|
8268
|
+
googleMapStyles(isDark, mapTypeId) {
|
|
8269
|
+
if (mapTypeId !== "roadmap")
|
|
8270
|
+
return null;
|
|
8271
|
+
if (isDark)
|
|
8272
|
+
return GOOGLE_DARK_STYLES;
|
|
8273
|
+
if (this.mapTypeValue === "MutedStandard")
|
|
8274
|
+
return GOOGLE_MUTED_STYLES;
|
|
8275
|
+
return null;
|
|
8276
|
+
}
|
|
8277
|
+
addGoogleMarkers() {
|
|
8278
|
+
const g = window.google;
|
|
8279
|
+
if (!this.markersValue.length)
|
|
8280
|
+
return;
|
|
8281
|
+
this.markersValue.forEach((m) => {
|
|
8282
|
+
const marker = new g.maps.Marker({
|
|
8283
|
+
map: this.mapInstance,
|
|
8284
|
+
position: { lat: m.latitude, lng: m.longitude },
|
|
8285
|
+
title: m.title ?? ""
|
|
8286
|
+
});
|
|
8287
|
+
if (m.open && m.title) {
|
|
8288
|
+
const content = m.subtitle ? `<strong>${m.title}</strong><br><span style="color:#777">${m.subtitle}</span>` : `<strong>${m.title}</strong>`;
|
|
8289
|
+
const infoWindow = new g.maps.InfoWindow({ content });
|
|
8290
|
+
infoWindow.open(this.mapInstance, marker);
|
|
8291
|
+
}
|
|
8292
|
+
});
|
|
8293
|
+
}
|
|
8294
|
+
// --- Leaflet ---
|
|
8295
|
+
initLeafletMap() {
|
|
8296
|
+
const L = window.L;
|
|
8297
|
+
const zoom = this.zoomValue > 0 ? this.zoomValue : this.spanToZoom(this.spanValue);
|
|
8298
|
+
const isDark = this.colorSchemeValue === "Dark" || this.colorSchemeValue === "Auto" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
8299
|
+
const tileKey = isDark ? "Dark" : this.mapTypeValue;
|
|
8300
|
+
const tile = LEAFLET_TILES[tileKey] ?? LEAFLET_TILES.MutedStandard;
|
|
8301
|
+
this.mapInstance = L.map(this.element, {
|
|
8302
|
+
center: [this.latitudeValue, this.longitudeValue],
|
|
8303
|
+
zoom,
|
|
8304
|
+
zoomControl: this.showsZoomControlValue,
|
|
8305
|
+
attributionControl: true
|
|
8306
|
+
});
|
|
8307
|
+
L.tileLayer(tile.url, {
|
|
8308
|
+
attribution: tile.attribution,
|
|
8309
|
+
maxZoom: tile.maxZoom ?? 19
|
|
8310
|
+
}).addTo(this.mapInstance);
|
|
8311
|
+
this.addLeafletMarkers();
|
|
8312
|
+
this.element.dataset.mapLoaded = "true";
|
|
8313
|
+
}
|
|
8314
|
+
addLeafletMarkers() {
|
|
8315
|
+
const L = window.L;
|
|
8316
|
+
if (!this.markersValue.length)
|
|
8317
|
+
return;
|
|
8318
|
+
this.markersValue.forEach((m) => {
|
|
8319
|
+
const icon = this.leafletMarkerIcon(m.color, m.glyphText);
|
|
8320
|
+
const marker = L.marker([m.latitude, m.longitude], { icon }).addTo(this.mapInstance);
|
|
8321
|
+
if (m.title) {
|
|
8322
|
+
const content = m.subtitle ? `<strong>${m.title}</strong><br><span style="color:#777">${m.subtitle}</span>` : `<strong>${m.title}</strong>`;
|
|
8323
|
+
marker.bindPopup(content);
|
|
8324
|
+
if (m.open)
|
|
8325
|
+
marker.openPopup();
|
|
8326
|
+
}
|
|
8327
|
+
});
|
|
8328
|
+
}
|
|
8329
|
+
// Returns a Leaflet icon — default blue when no color/glyph, otherwise a
|
|
8330
|
+
// custom SVG pin rendered via divIcon.
|
|
8331
|
+
leafletMarkerIcon(color, glyphText) {
|
|
8332
|
+
const L = window.L;
|
|
8333
|
+
if (!color && !glyphText)
|
|
8334
|
+
return new L.Icon.Default();
|
|
8335
|
+
const fill = color ?? "#2563eb";
|
|
8336
|
+
const inner = glyphText ? `<text x="12" y="16" text-anchor="middle" font-size="9" font-family="system-ui,sans-serif" fill="white">${glyphText}</text>` : `<circle cx="12" cy="12" r="4" fill="rgba(255,255,255,0.9)"/>`;
|
|
8337
|
+
const svg = [
|
|
8338
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="24" height="36">',
|
|
8339
|
+
` <path d="M12 0C5.373 0 0 5.373 0 12c0 9 12 24 12 24s12-15 12-24C24 5.373 18.627 0 12 0z" fill="${fill}"/>`,
|
|
8340
|
+
` ${inner}`,
|
|
8341
|
+
"</svg>"
|
|
8342
|
+
].join("");
|
|
8343
|
+
return L.divIcon({
|
|
8344
|
+
html: svg,
|
|
8345
|
+
className: "",
|
|
8346
|
+
iconSize: [24, 36],
|
|
8347
|
+
iconAnchor: [12, 36],
|
|
8348
|
+
popupAnchor: [0, -36]
|
|
8349
|
+
});
|
|
8350
|
+
}
|
|
8351
|
+
// Approximate MapKit coordinate span to a Web Mercator zoom level.
|
|
8352
|
+
// CoordinateSpan 360° = zoom 0; halving span adds 1 zoom level.
|
|
8353
|
+
spanToZoom(span) {
|
|
8354
|
+
return Math.round(Math.log2(360 / span));
|
|
8355
|
+
}
|
|
8356
|
+
};
|
|
8357
|
+
|
|
8358
|
+
const DEBOUNCE_MS = 300;
|
|
8359
|
+
const GOOGLE_PREVIEW_STYLES = [
|
|
8360
|
+
{ elementType: "geometry", stylers: [{ color: "#f5f5f5" }] },
|
|
8361
|
+
{ elementType: "labels.icon", stylers: [{ visibility: "off" }] },
|
|
8362
|
+
{ elementType: "labels.text.fill", stylers: [{ color: "#616161" }] },
|
|
8363
|
+
{ elementType: "labels.text.stroke", stylers: [{ color: "#f5f5f5" }] },
|
|
8364
|
+
{ featureType: "road", elementType: "geometry", stylers: [{ color: "#ffffff" }] },
|
|
8365
|
+
{ featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#dadada" }] },
|
|
8366
|
+
{ featureType: "water", elementType: "geometry", stylers: [{ color: "#c9c9c9" }] },
|
|
8367
|
+
{ featureType: "poi", elementType: "geometry", stylers: [{ color: "#eeeeee" }] }
|
|
8368
|
+
];
|
|
8369
|
+
class Address extends Controller {
|
|
8370
|
+
static targets = [
|
|
8371
|
+
"input",
|
|
8372
|
+
"dropdown",
|
|
8373
|
+
"suggestions",
|
|
8374
|
+
"latInput",
|
|
8375
|
+
"lngInput",
|
|
8376
|
+
"placeIdInput",
|
|
8377
|
+
"mapContainer"
|
|
8378
|
+
];
|
|
8379
|
+
static values = {
|
|
8380
|
+
provider: { type: String, default: "" },
|
|
8381
|
+
mapProvider: { type: String, default: "" },
|
|
8382
|
+
apiKey: { type: String, default: "" },
|
|
8383
|
+
token: { type: String, default: "" },
|
|
8384
|
+
tokenUrl: { type: String, default: "" },
|
|
8385
|
+
countryCodes: { type: Array, default: [] },
|
|
8386
|
+
showMap: { type: Boolean, default: false },
|
|
8387
|
+
nominatimUrl: { type: String, default: "" }
|
|
8388
|
+
};
|
|
8389
|
+
searchTimer;
|
|
8390
|
+
highlightedIndex = -1;
|
|
8391
|
+
mapInstance = null;
|
|
8392
|
+
mapMarker = null;
|
|
8393
|
+
mkSearch = null;
|
|
8394
|
+
autocompleteService = null;
|
|
8395
|
+
placesService = null;
|
|
8396
|
+
// ── Named handlers ────────────────────────────────────────────────────────
|
|
8397
|
+
onToggle = (e) => {
|
|
8398
|
+
const opened = e.newState === "open";
|
|
8399
|
+
if (this.hasInputTarget) {
|
|
8400
|
+
this.inputTarget.setAttribute("aria-expanded", String(opened));
|
|
8401
|
+
}
|
|
8402
|
+
if (!opened) {
|
|
8403
|
+
this.highlightedIndex = -1;
|
|
8404
|
+
}
|
|
8405
|
+
};
|
|
8406
|
+
onFocusOut = (e) => {
|
|
8407
|
+
if (!this.element.contains(e.relatedTarget)) {
|
|
8408
|
+
this.closeDropdown();
|
|
8409
|
+
}
|
|
8410
|
+
};
|
|
8411
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
8412
|
+
async connect() {
|
|
8413
|
+
this.element.addEventListener("focusout", this.onFocusOut);
|
|
8414
|
+
const provider = this.providerValue;
|
|
8415
|
+
const mapProv = this.effectiveMapProvider();
|
|
8416
|
+
if (!provider && !mapProv)
|
|
8417
|
+
return;
|
|
8418
|
+
try {
|
|
8419
|
+
if (provider === "mapkit") {
|
|
8420
|
+
await loadMapKitScript();
|
|
8421
|
+
this.initMapKit();
|
|
8422
|
+
} else if (provider === "google") {
|
|
8423
|
+
await loadGoogleMapsScript(this.apiKeyValue);
|
|
8424
|
+
this.initGoogle();
|
|
8425
|
+
}
|
|
8426
|
+
if (this.showMapValue && mapProv === "leaflet") {
|
|
8427
|
+
await loadLeafletScript();
|
|
8428
|
+
}
|
|
8429
|
+
} catch (err) {
|
|
8430
|
+
console.error("[midwest-address] Failed to initialise:", err);
|
|
8431
|
+
}
|
|
8432
|
+
}
|
|
8433
|
+
disconnect() {
|
|
8434
|
+
clearTimeout(this.searchTimer);
|
|
8435
|
+
this.element.removeEventListener("focusout", this.onFocusOut);
|
|
8436
|
+
if (this.mapInstance) {
|
|
8437
|
+
const mapProv = this.effectiveMapProvider();
|
|
8438
|
+
if (mapProv === "mapkit")
|
|
8439
|
+
this.mapInstance.destroy();
|
|
8440
|
+
if (mapProv === "leaflet")
|
|
8441
|
+
this.mapInstance.remove();
|
|
8442
|
+
}
|
|
8443
|
+
this.mapInstance = null;
|
|
8444
|
+
}
|
|
8445
|
+
dropdownTargetConnected(el) {
|
|
8446
|
+
el.addEventListener("toggle", this.onToggle);
|
|
8447
|
+
}
|
|
8448
|
+
dropdownTargetDisconnected(el) {
|
|
8449
|
+
el.removeEventListener("toggle", this.onToggle);
|
|
8450
|
+
}
|
|
8451
|
+
// ── Public Stimulus actions ───────────────────────────────────────────────
|
|
8452
|
+
search() {
|
|
8453
|
+
this.clearGeocode();
|
|
8454
|
+
const query = this.inputTarget.value.trim();
|
|
8455
|
+
if (query.length < 2) {
|
|
8456
|
+
this.closeDropdown();
|
|
8457
|
+
return;
|
|
8458
|
+
}
|
|
8459
|
+
clearTimeout(this.searchTimer);
|
|
8460
|
+
this.searchTimer = setTimeout(() => {
|
|
8461
|
+
if (this.providerValue === "mapkit") {
|
|
8462
|
+
this.searchMapKit(query);
|
|
8463
|
+
} else if (this.providerValue === "google") {
|
|
8464
|
+
this.searchGoogle(query);
|
|
8465
|
+
} else if (this.providerValue === "nominatim") {
|
|
8466
|
+
this.searchNominatim(query);
|
|
8467
|
+
}
|
|
8468
|
+
}, DEBOUNCE_MS);
|
|
8469
|
+
}
|
|
8470
|
+
navigate(event) {
|
|
8471
|
+
const items = this.suggestionElements();
|
|
8472
|
+
if (!items.length)
|
|
8473
|
+
return;
|
|
8474
|
+
switch (event.key) {
|
|
8475
|
+
case "ArrowDown":
|
|
8476
|
+
event.preventDefault();
|
|
8477
|
+
this.highlightedIndex = Math.min(this.highlightedIndex + 1, items.length - 1);
|
|
8478
|
+
this.applyHighlight(items);
|
|
8479
|
+
break;
|
|
8480
|
+
case "ArrowUp":
|
|
8481
|
+
event.preventDefault();
|
|
8482
|
+
this.highlightedIndex = Math.max(this.highlightedIndex - 1, -1);
|
|
8483
|
+
this.applyHighlight(items);
|
|
8484
|
+
break;
|
|
8485
|
+
case "Enter":
|
|
8486
|
+
event.preventDefault();
|
|
8487
|
+
if (this.highlightedIndex >= 0)
|
|
8488
|
+
items[this.highlightedIndex].dispatchEvent(new MouseEvent("mousedown"));
|
|
8489
|
+
break;
|
|
8490
|
+
case "Escape":
|
|
8491
|
+
this.closeDropdown();
|
|
8492
|
+
break;
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
// ── Provider init ─────────────────────────────────────────────────────────
|
|
8496
|
+
initMapKit() {
|
|
8497
|
+
const mk = window.mapkit;
|
|
8498
|
+
const { tokenValue: token, tokenUrlValue: tokenUrl } = this;
|
|
8499
|
+
mk.init({
|
|
8500
|
+
authorizationCallback: (done) => {
|
|
8501
|
+
if (token) {
|
|
8502
|
+
done(token);
|
|
8503
|
+
} else if (tokenUrl) {
|
|
8504
|
+
fetch(tokenUrl).then((r) => r.text()).then(done).catch((err) => console.error("[midwest-address] Token fetch failed:", err));
|
|
8505
|
+
}
|
|
8506
|
+
},
|
|
8507
|
+
language: document.documentElement.lang || "en"
|
|
8508
|
+
});
|
|
8509
|
+
this.mkSearch = new mk.Search({ getsUserLocation: false });
|
|
8510
|
+
}
|
|
8511
|
+
initGoogle() {
|
|
8512
|
+
const g = window.google;
|
|
8513
|
+
this.autocompleteService = new g.maps.places.AutocompleteService();
|
|
8514
|
+
this.placesService = new g.maps.places.PlacesService(document.createElement("div"));
|
|
8515
|
+
}
|
|
8516
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
8517
|
+
searchMapKit(query) {
|
|
8518
|
+
if (!this.mkSearch)
|
|
8519
|
+
return;
|
|
8520
|
+
this.mkSearch.autocomplete(query, (err, data) => {
|
|
8521
|
+
if (err || !data?.results?.length) {
|
|
8522
|
+
this.closeDropdown();
|
|
8523
|
+
return;
|
|
8524
|
+
}
|
|
8525
|
+
const results = data.results.map((r) => ({
|
|
8526
|
+
id: r.completionURL ?? r.displayLines?.join(", ") ?? "",
|
|
8527
|
+
label: r.displayLines?.[0] ?? "",
|
|
8528
|
+
secondary: r.displayLines?.[1] ?? "",
|
|
8529
|
+
payload: r
|
|
8530
|
+
}));
|
|
8531
|
+
this.renderSuggestions(results);
|
|
8532
|
+
});
|
|
8533
|
+
}
|
|
8534
|
+
searchGoogle(query) {
|
|
8535
|
+
if (!this.autocompleteService)
|
|
8536
|
+
return;
|
|
8537
|
+
const request = { input: query };
|
|
8538
|
+
if (this.countryCodesValue.length) {
|
|
8539
|
+
request.componentRestrictions = { country: this.countryCodesValue };
|
|
8540
|
+
}
|
|
8541
|
+
this.autocompleteService.getPlacePredictions(
|
|
8542
|
+
request,
|
|
8543
|
+
(predictions, status) => {
|
|
8544
|
+
const g = window.google;
|
|
8545
|
+
if (status !== g.maps.places.PlacesServiceStatus.OK || !predictions?.length) {
|
|
8546
|
+
this.closeDropdown();
|
|
8547
|
+
return;
|
|
8548
|
+
}
|
|
8549
|
+
const results = predictions.map((p) => ({
|
|
8550
|
+
id: p.place_id,
|
|
8551
|
+
label: p.structured_formatting?.main_text ?? p.description,
|
|
8552
|
+
secondary: p.structured_formatting?.secondary_text ?? "",
|
|
8553
|
+
payload: p
|
|
8554
|
+
}));
|
|
8555
|
+
this.renderSuggestions(results);
|
|
8556
|
+
}
|
|
8557
|
+
);
|
|
8558
|
+
}
|
|
8559
|
+
// ── Suggestions UI ────────────────────────────────────────────────────────
|
|
8560
|
+
renderSuggestions(results) {
|
|
8561
|
+
if (!this.hasSuggestionsTarget)
|
|
8562
|
+
return;
|
|
8563
|
+
const list = this.suggestionsTarget;
|
|
8564
|
+
list.innerHTML = "";
|
|
8565
|
+
results.forEach((r, i) => {
|
|
8566
|
+
const li = document.createElement("li");
|
|
8567
|
+
li.className = "midwest-address__suggestion";
|
|
8568
|
+
li.setAttribute("role", "option");
|
|
8569
|
+
li.setAttribute("id", `${this.element.id || "addr"}-s${i}`);
|
|
8570
|
+
li.dataset.index = String(i);
|
|
8571
|
+
const main = document.createElement("span");
|
|
8572
|
+
main.className = "midwest-address__suggestion-main";
|
|
8573
|
+
main.textContent = r.label;
|
|
8574
|
+
li.appendChild(main);
|
|
8575
|
+
if (r.secondary) {
|
|
8576
|
+
const sec = document.createElement("span");
|
|
8577
|
+
sec.className = "midwest-address__suggestion-secondary";
|
|
8578
|
+
sec.textContent = r.secondary;
|
|
8579
|
+
li.appendChild(sec);
|
|
8580
|
+
}
|
|
8581
|
+
li.addEventListener("mousedown", (e) => {
|
|
8582
|
+
e.preventDefault();
|
|
8583
|
+
this.pickSuggestion(r);
|
|
8584
|
+
});
|
|
8585
|
+
list.appendChild(li);
|
|
8586
|
+
});
|
|
8587
|
+
this.openDropdown();
|
|
8588
|
+
this.highlightedIndex = -1;
|
|
8589
|
+
}
|
|
8590
|
+
openDropdown() {
|
|
8591
|
+
if (!this.hasDropdownTarget)
|
|
8592
|
+
return;
|
|
8593
|
+
if (!this.dropdownTarget.matches(":popover-open"))
|
|
8594
|
+
this.dropdownTarget.showPopover();
|
|
8595
|
+
}
|
|
8596
|
+
closeDropdown() {
|
|
8597
|
+
if (this.hasDropdownTarget && this.dropdownTarget.matches(":popover-open")) {
|
|
8598
|
+
this.dropdownTarget.hidePopover();
|
|
8599
|
+
}
|
|
8600
|
+
if (this.hasSuggestionsTarget)
|
|
8601
|
+
this.suggestionsTarget.innerHTML = "";
|
|
8602
|
+
this.highlightedIndex = -1;
|
|
8603
|
+
}
|
|
8604
|
+
suggestionElements() {
|
|
8605
|
+
if (!this.hasSuggestionsTarget)
|
|
8606
|
+
return [];
|
|
8607
|
+
return Array.from(this.suggestionsTarget.querySelectorAll(".midwest-address__suggestion"));
|
|
8608
|
+
}
|
|
8609
|
+
applyHighlight(items) {
|
|
8610
|
+
items.forEach((item, i) => {
|
|
8611
|
+
const active = i === this.highlightedIndex;
|
|
8612
|
+
item.classList.toggle("is-highlighted", active);
|
|
8613
|
+
item.setAttribute("aria-selected", String(active));
|
|
8614
|
+
if (active) {
|
|
8615
|
+
this.inputTarget.setAttribute("aria-activedescendant", item.id);
|
|
8616
|
+
item.scrollIntoView({ block: "nearest" });
|
|
8617
|
+
}
|
|
8618
|
+
});
|
|
8619
|
+
if (this.highlightedIndex < 0) {
|
|
8620
|
+
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
// ── Pick a suggestion ─────────────────────────────────────────────────────
|
|
8624
|
+
pickSuggestion(r) {
|
|
8625
|
+
this.closeDropdown();
|
|
8626
|
+
if (this.providerValue === "mapkit") {
|
|
8627
|
+
this.pickMapKit(r);
|
|
8628
|
+
} else if (this.providerValue === "google") {
|
|
8629
|
+
this.pickGoogle(r);
|
|
8630
|
+
} else if (this.providerValue === "nominatim") {
|
|
8631
|
+
this.pickNominatim(r);
|
|
8632
|
+
}
|
|
8633
|
+
}
|
|
8634
|
+
pickMapKit(r) {
|
|
8635
|
+
if (!this.mkSearch)
|
|
8636
|
+
return;
|
|
8637
|
+
this.mkSearch.search(r.payload, (err, data) => {
|
|
8638
|
+
if (err || !data?.places?.length)
|
|
8639
|
+
return;
|
|
8640
|
+
const place = data.places[0];
|
|
8641
|
+
const coord = place.coordinate;
|
|
8642
|
+
this.inputTarget.value = [r.label, r.secondary].filter(Boolean).join(", ");
|
|
8643
|
+
this.fillGeocode(coord.latitude, coord.longitude, place.id ?? "");
|
|
8644
|
+
this.updateMapPreview(coord.latitude, coord.longitude);
|
|
8645
|
+
});
|
|
8646
|
+
}
|
|
8647
|
+
pickGoogle(r) {
|
|
8648
|
+
if (!this.placesService || !r.id)
|
|
8649
|
+
return;
|
|
8650
|
+
const g = window.google;
|
|
8651
|
+
this.placesService.getDetails(
|
|
8652
|
+
{ placeId: r.id, fields: ["geometry", "formatted_address", "place_id"] },
|
|
8653
|
+
(place, status) => {
|
|
8654
|
+
if (status !== g.maps.places.PlacesServiceStatus.OK || !place)
|
|
8655
|
+
return;
|
|
8656
|
+
const lat = place.geometry.location.lat();
|
|
8657
|
+
const lng = place.geometry.location.lng();
|
|
8658
|
+
this.inputTarget.value = place.formatted_address;
|
|
8659
|
+
this.fillGeocode(lat, lng, place.place_id);
|
|
8660
|
+
this.updateMapPreview(lat, lng);
|
|
8661
|
+
}
|
|
8662
|
+
);
|
|
8663
|
+
}
|
|
8664
|
+
// ── Geocode field management ──────────────────────────────────────────────
|
|
8665
|
+
fillGeocode(lat, lng, placeId) {
|
|
8666
|
+
if (this.hasLatInputTarget)
|
|
8667
|
+
this.latInputTarget.value = String(lat);
|
|
8668
|
+
if (this.hasLngInputTarget)
|
|
8669
|
+
this.lngInputTarget.value = String(lng);
|
|
8670
|
+
if (this.hasPlaceIdInputTarget)
|
|
8671
|
+
this.placeIdInputTarget.value = placeId;
|
|
8672
|
+
this.dispatch("geocode", { detail: { lat, lng, placeId } });
|
|
8673
|
+
}
|
|
8674
|
+
clearGeocode() {
|
|
8675
|
+
if (this.hasLatInputTarget)
|
|
8676
|
+
this.latInputTarget.value = "";
|
|
8677
|
+
if (this.hasLngInputTarget)
|
|
8678
|
+
this.lngInputTarget.value = "";
|
|
8679
|
+
if (this.hasPlaceIdInputTarget)
|
|
8680
|
+
this.placeIdInputTarget.value = "";
|
|
8681
|
+
}
|
|
8682
|
+
// ── Nominatim search (OpenStreetMap, no API key) ──────────────────────────
|
|
8683
|
+
searchNominatim(query) {
|
|
8684
|
+
const base = this.nominatimUrlValue || "https://nominatim.openstreetmap.org";
|
|
8685
|
+
const url = `${base}/search?q=${encodeURIComponent(query)}&format=json&limit=5&addressdetails=1`;
|
|
8686
|
+
fetch(url, { headers: { "Accept-Language": document.documentElement.lang || "en" } }).then((r) => r.json()).then((data) => {
|
|
8687
|
+
if (!data?.length) {
|
|
8688
|
+
this.closeDropdown();
|
|
8689
|
+
return;
|
|
8690
|
+
}
|
|
8691
|
+
const results = data.map((r) => {
|
|
8692
|
+
const parts = r.display_name.split(", ");
|
|
8693
|
+
return {
|
|
8694
|
+
id: String(r.place_id),
|
|
8695
|
+
label: r.name || parts[0] || r.display_name,
|
|
8696
|
+
secondary: parts.length > 1 ? parts.slice(1).join(", ") : "",
|
|
8697
|
+
payload: r
|
|
8698
|
+
};
|
|
8699
|
+
});
|
|
8700
|
+
this.renderSuggestions(results);
|
|
8701
|
+
}).catch(() => this.closeDropdown());
|
|
8702
|
+
}
|
|
8703
|
+
pickNominatim(r) {
|
|
8704
|
+
const place = r.payload;
|
|
8705
|
+
const lat = parseFloat(place.lat);
|
|
8706
|
+
const lng = parseFloat(place.lon);
|
|
8707
|
+
this.inputTarget.value = place.display_name;
|
|
8708
|
+
this.fillGeocode(lat, lng, `${place.osm_type}:${place.osm_id}`);
|
|
8709
|
+
this.updateMapPreview(lat, lng);
|
|
8710
|
+
}
|
|
8711
|
+
// ── Map preview ───────────────────────────────────────────────────────────
|
|
8712
|
+
// Returns the provider used for the map preview. `mapProviderValue` is set
|
|
8713
|
+
// when the geocoder and map providers differ; falls back to `providerValue`
|
|
8714
|
+
// so existing single-provider setups (mapkit/google) work without change.
|
|
8715
|
+
effectiveMapProvider() {
|
|
8716
|
+
return this.mapProviderValue || this.providerValue;
|
|
8717
|
+
}
|
|
8718
|
+
updateMapPreview(lat, lng) {
|
|
8719
|
+
if (!this.hasMapContainerTarget || !this.showMapValue)
|
|
8720
|
+
return;
|
|
8721
|
+
this.mapContainerTarget.hidden = false;
|
|
8722
|
+
const mapProv = this.effectiveMapProvider();
|
|
8723
|
+
if (mapProv === "mapkit") {
|
|
8724
|
+
this.updateMapKitPreview(lat, lng);
|
|
8725
|
+
} else if (mapProv === "google") {
|
|
8726
|
+
this.updateGooglePreview(lat, lng);
|
|
8727
|
+
} else if (mapProv === "leaflet") {
|
|
8728
|
+
this.updateLeafletPreview(lat, lng);
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
updateMapKitPreview(lat, lng) {
|
|
8732
|
+
const mk = window.mapkit;
|
|
8733
|
+
const coord = new mk.Coordinate(lat, lng);
|
|
8734
|
+
if (!this.mapInstance) {
|
|
8735
|
+
this.mapInstance = new mk.Map(this.mapContainerTarget, {
|
|
8736
|
+
mapType: mk.Map.MapTypes.MutedStandard,
|
|
8737
|
+
showsCompass: mk.FeatureVisibility.Hidden,
|
|
8738
|
+
showsZoomControl: false,
|
|
8739
|
+
showsMapTypeControl: false,
|
|
8740
|
+
isScrollEnabled: false,
|
|
8741
|
+
isZoomEnabled: false
|
|
8742
|
+
});
|
|
8743
|
+
}
|
|
8744
|
+
this.mapInstance.setRegionAnimated(
|
|
8745
|
+
new mk.CoordinateRegion(coord, new mk.CoordinateSpan(0.01, 0.01))
|
|
8746
|
+
);
|
|
8747
|
+
if (this.mapMarker)
|
|
8748
|
+
this.mapInstance.removeAnnotation(this.mapMarker);
|
|
8749
|
+
this.mapMarker = new mk.MarkerAnnotation(coord);
|
|
8750
|
+
this.mapInstance.addAnnotation(this.mapMarker);
|
|
8751
|
+
}
|
|
8752
|
+
updateGooglePreview(lat, lng) {
|
|
8753
|
+
const g = window.google;
|
|
8754
|
+
const center = { lat, lng };
|
|
8755
|
+
if (!this.mapInstance) {
|
|
8756
|
+
this.mapInstance = new g.maps.Map(this.mapContainerTarget, {
|
|
8757
|
+
center,
|
|
8758
|
+
zoom: 15,
|
|
8759
|
+
disableDefaultUI: true,
|
|
8760
|
+
gestureHandling: "none",
|
|
8761
|
+
styles: GOOGLE_PREVIEW_STYLES
|
|
8762
|
+
});
|
|
8763
|
+
} else {
|
|
8764
|
+
this.mapInstance.setCenter(center);
|
|
8765
|
+
this.mapInstance.setZoom(15);
|
|
8766
|
+
}
|
|
8767
|
+
if (this.mapMarker)
|
|
8768
|
+
this.mapMarker.setMap(null);
|
|
8769
|
+
this.mapMarker = new g.maps.Marker({ map: this.mapInstance, position: center });
|
|
8770
|
+
}
|
|
8771
|
+
updateLeafletPreview(lat, lng) {
|
|
8772
|
+
const L = window.L;
|
|
8773
|
+
if (!L)
|
|
8774
|
+
return;
|
|
8775
|
+
if (!this.mapInstance) {
|
|
8776
|
+
this.mapInstance = L.map(this.mapContainerTarget, {
|
|
8777
|
+
center: [lat, lng],
|
|
8778
|
+
zoom: 15,
|
|
8779
|
+
zoomControl: false,
|
|
8780
|
+
attributionControl: false
|
|
8781
|
+
});
|
|
8782
|
+
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
|
|
8783
|
+
maxZoom: 19
|
|
8784
|
+
}).addTo(this.mapInstance);
|
|
8785
|
+
} else {
|
|
8786
|
+
this.mapInstance.setView([lat, lng], 15);
|
|
8787
|
+
}
|
|
8788
|
+
if (this.mapMarker)
|
|
8789
|
+
this.mapMarker.remove();
|
|
8790
|
+
this.mapMarker = L.marker([lat, lng]).addTo(this.mapInstance);
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8793
|
+
|
|
8794
|
+
class Popover extends Controller {
|
|
8795
|
+
animating = false;
|
|
8796
|
+
closeTimer = null;
|
|
8797
|
+
connect() {
|
|
8798
|
+
this.element.addEventListener("beforetoggle", this.onBeforeToggle);
|
|
8799
|
+
}
|
|
8800
|
+
disconnect() {
|
|
8801
|
+
this.element.removeEventListener("beforetoggle", this.onBeforeToggle);
|
|
8802
|
+
this.cleanUp();
|
|
8803
|
+
}
|
|
8804
|
+
onBeforeToggle = (event) => {
|
|
8805
|
+
const toggle = event;
|
|
8806
|
+
if (toggle.newState === "open") {
|
|
8807
|
+
if (this.closeTimer) {
|
|
8808
|
+
clearTimeout(this.closeTimer);
|
|
8809
|
+
this.closeTimer = null;
|
|
8810
|
+
}
|
|
8811
|
+
this.element.classList.remove("is-closing");
|
|
8812
|
+
this.animating = false;
|
|
8813
|
+
return;
|
|
8814
|
+
}
|
|
8815
|
+
if (toggle.newState !== "closed")
|
|
8816
|
+
return;
|
|
8817
|
+
if (window.matchMedia("(width < 700px)").matches)
|
|
8818
|
+
return;
|
|
8819
|
+
if (!this.element.classList.contains("midwest-motion"))
|
|
8820
|
+
return;
|
|
8821
|
+
if (this.animating)
|
|
8822
|
+
return;
|
|
8823
|
+
event.preventDefault();
|
|
8824
|
+
this.animating = true;
|
|
8825
|
+
this.element.classList.add("is-closing");
|
|
8826
|
+
this.closeTimer = setTimeout(() => {
|
|
8827
|
+
this.closeTimer = null;
|
|
8828
|
+
this.element.style.display = "none";
|
|
8829
|
+
this.element.style.transition = "none";
|
|
8830
|
+
this.element.hidePopover();
|
|
8831
|
+
this.animating = false;
|
|
8832
|
+
requestAnimationFrame(() => {
|
|
8833
|
+
this.element.style.removeProperty("display");
|
|
8834
|
+
this.element.style.removeProperty("transition");
|
|
8835
|
+
this.element.classList.remove("is-closing");
|
|
8836
|
+
});
|
|
8837
|
+
}, this.exitDuration());
|
|
8838
|
+
};
|
|
8839
|
+
cleanUp() {
|
|
8840
|
+
if (this.closeTimer) {
|
|
8841
|
+
clearTimeout(this.closeTimer);
|
|
8842
|
+
this.closeTimer = null;
|
|
8843
|
+
}
|
|
8844
|
+
this.element.classList.remove("is-closing");
|
|
8845
|
+
this.element.style.removeProperty("transition");
|
|
8846
|
+
this.animating = false;
|
|
8847
|
+
}
|
|
8848
|
+
exitDuration() {
|
|
8849
|
+
const durations = getComputedStyle(this.element).transitionDuration.split(",").map((s) => {
|
|
8850
|
+
const n = parseFloat(s);
|
|
8851
|
+
return s.trim().endsWith("ms") ? n : n * 1e3;
|
|
8852
|
+
});
|
|
8853
|
+
return Math.max(0, ...durations);
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
|
|
7876
8857
|
function registerMidwestControllers(application) {
|
|
7877
8858
|
application.register("midwest-card", Card);
|
|
7878
8859
|
application.register("midwest-banner", Banner);
|
|
@@ -7914,6 +8895,11 @@ function registerMidwestControllers(application) {
|
|
|
7914
8895
|
application.register("midwest-theme-switch", ThemeSwitch);
|
|
7915
8896
|
application.register("midwest-panorama", Panorama);
|
|
7916
8897
|
application.register("midwest-playlist", Playlist);
|
|
8898
|
+
application.register("midwest-motion", Motion);
|
|
8899
|
+
application.register("midwest-page-transition", PageTransition);
|
|
8900
|
+
application.register("midwest-map", Map$1);
|
|
8901
|
+
application.register("midwest-address", Address);
|
|
8902
|
+
application.register("midwest-popover", Popover);
|
|
7917
8903
|
}
|
|
7918
8904
|
|
|
7919
8905
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|