@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.
Files changed (41) hide show
  1. package/app/assets/javascript/midwest/index.ts +10 -0
  2. package/app/assets/javascript/midwest/map_providers.ts +61 -0
  3. package/app/assets/javascript/midwest/viewport_observer.ts +49 -0
  4. package/app/assets/javascript/midwest.js +1056 -70
  5. package/app/assets/javascript/midwest.js.map +1 -1
  6. package/app/assets/stylesheets/midwest.css +1 -1
  7. package/app/assets/stylesheets/midwest.tailwind.css +7 -1
  8. package/dist/css/midwest.css +1 -1
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +10 -0
  10. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  11. package/dist/javascript/collection/app/assets/javascript/midwest/map_providers.js +56 -0
  12. package/dist/javascript/collection/app/assets/javascript/midwest/map_providers.js.map +1 -0
  13. package/dist/javascript/collection/app/assets/javascript/midwest/viewport_observer.js +22 -0
  14. package/dist/javascript/collection/app/assets/javascript/midwest/viewport_observer.js.map +1 -0
  15. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +4 -7
  16. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
  17. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +0 -6
  18. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
  19. package/dist/javascript/collection/app/components/midwest/form/address_component/address_component_controller.js +441 -0
  20. package/dist/javascript/collection/app/components/midwest/form/address_component/address_component_controller.js.map +1 -0
  21. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +55 -1
  22. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  23. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +20 -25
  24. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -1
  25. package/dist/javascript/collection/app/components/midwest/map_component/map_component_controller.js +297 -0
  26. package/dist/javascript/collection/app/components/midwest/map_component/map_component_controller.js.map +1 -0
  27. package/dist/javascript/collection/app/components/midwest/motion_component/motion_component_controller.js +32 -0
  28. package/dist/javascript/collection/app/components/midwest/motion_component/motion_component_controller.js.map +1 -0
  29. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js +4 -12
  30. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js.map +1 -1
  31. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +34 -5
  32. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -1
  33. package/dist/javascript/collection/app/components/midwest/page_transition_component/page_transition_component_controller.js +38 -0
  34. package/dist/javascript/collection/app/components/midwest/page_transition_component/page_transition_component_controller.js.map +1 -0
  35. package/dist/javascript/collection/app/components/midwest/popover_component/popover_component_controller.js +67 -0
  36. package/dist/javascript/collection/app/components/midwest/popover_component/popover_component_controller.js.map +1 -0
  37. package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js +10 -14
  38. package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js.map +1 -1
  39. package/dist/javascript/midwest.js +1056 -70
  40. package/dist/javascript/midwest.js.map +1 -1
  41. 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.slides[0]?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
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
- const last = this.slides[this.slides.length - 1];
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
- const slide = this.slides[index];
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
- const firstSlide = this.slides[0];
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
- requestAnimationFrame(() => {
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 isTop = this.element === this.element.parentElement?.firstElementChild;
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
- observer = null;
5051
+ teardown = null;
4995
5052
  loading = false;
4996
5053
  connect() {
4997
5054
  if (!this.urlValue)
4998
5055
  return;
4999
- this.observer = new IntersectionObserver(
5000
- (entries) => {
5001
- if (entries[0].isIntersecting)
5002
- this.load();
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.observer?.disconnect();
5012
- this.observer = null;
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.observer?.disconnect();
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
- observer = null;
7002
+ teardown = null;
6951
7003
  connect() {
6952
- this.observer = new IntersectionObserver(
6953
- (entries) => {
6954
- for (const entry of entries)
6955
- this.handle(entry);
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
- { threshold: this.thresholdValue, rootMargin: this.rootMarginValue }
6958
- );
6959
- this.observer.observe(this.element);
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.observer?.disconnect();
6963
- this.observer = null;
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
8070
+ },
8071
+ Standard: {
8072
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
8073
+ attribution: '&copy; <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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <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 &copy; Esri &mdash; 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <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 };