@unabridged/midwest 0.24.3 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/app/assets/javascript/midwest/index.ts +8 -0
  2. package/app/assets/javascript/midwest/map_providers.ts +61 -0
  3. package/app/assets/javascript/midwest.js +1009 -9
  4. package/app/assets/javascript/midwest.js.map +1 -1
  5. package/app/assets/stylesheets/midwest.css +1 -1
  6. package/app/assets/stylesheets/midwest.tailwind.css +13 -1
  7. package/dist/css/midwest.css +1 -1
  8. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  10. package/dist/javascript/collection/app/assets/javascript/midwest/map_providers.js +56 -0
  11. package/dist/javascript/collection/app/assets/javascript/midwest/map_providers.js.map +1 -0
  12. package/dist/javascript/collection/app/components/midwest/form/address_component/address_component_controller.js +441 -0
  13. package/dist/javascript/collection/app/components/midwest/form/address_component/address_component_controller.js.map +1 -0
  14. package/dist/javascript/collection/app/components/midwest/form/form_controller.js +37 -3
  15. package/dist/javascript/collection/app/components/midwest/form/form_controller.js.map +1 -1
  16. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +55 -1
  17. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  18. package/dist/javascript/collection/app/components/midwest/map_component/map_component_controller.js +297 -0
  19. package/dist/javascript/collection/app/components/midwest/map_component/map_component_controller.js.map +1 -0
  20. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +34 -5
  21. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -1
  22. package/dist/javascript/collection/app/components/midwest/page_transition_component/page_transition_component_controller.js +38 -0
  23. package/dist/javascript/collection/app/components/midwest/page_transition_component/page_transition_component_controller.js.map +1 -0
  24. package/dist/javascript/collection/app/components/midwest/popover_component/popover_component_controller.js +67 -0
  25. package/dist/javascript/collection/app/components/midwest/popover_component/popover_component_controller.js.map +1 -0
  26. package/dist/javascript/midwest.js +1009 -9
  27. package/dist/javascript/midwest.js.map +1 -1
  28. package/package.json +1 -1
@@ -3299,6 +3299,7 @@ class FormLiveSummary extends Controller {
3299
3299
  this.form?.addEventListener("midwest-autocomplete:change", this.render);
3300
3300
  this.form?.addEventListener("midwest-color-picker:update", this.render);
3301
3301
  this.form?.addEventListener("midwest-repeatable:change", this.render);
3302
+ this.form?.addEventListener("midwest-address:geocode", this.render);
3302
3303
  }
3303
3304
  disconnect() {
3304
3305
  this.form?.removeEventListener("input", this.render);
@@ -3306,6 +3307,7 @@ class FormLiveSummary extends Controller {
3306
3307
  this.form?.removeEventListener("midwest-autocomplete:change", this.render);
3307
3308
  this.form?.removeEventListener("midwest-color-picker:update", this.render);
3308
3309
  this.form?.removeEventListener("midwest-repeatable:change", this.render);
3310
+ this.form?.removeEventListener("midwest-address:geocode", this.render);
3309
3311
  }
3310
3312
  get form() {
3311
3313
  return this.element.closest("form");
@@ -3323,7 +3325,7 @@ class FormLiveSummary extends Controller {
3323
3325
  }
3324
3326
  const dl = document.createElement("dl");
3325
3327
  dl.className = "midwest-form-live-summary-list";
3326
- fields.forEach(({ label, value, swatchColor }) => {
3328
+ fields.forEach(({ label, value, swatchColor, details }) => {
3327
3329
  const dt = Object.assign(document.createElement("dt"), {
3328
3330
  className: "midwest-form-live-summary-term",
3329
3331
  textContent: label
@@ -3332,6 +3334,8 @@ class FormLiveSummary extends Controller {
3332
3334
  dd.className = "midwest-form-live-summary-definition";
3333
3335
  if (swatchColor !== void 0) {
3334
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));
3335
3339
  } else {
3336
3340
  dd.textContent = value || "\u2014";
3337
3341
  }
@@ -3359,6 +3363,8 @@ class FormLiveSummary extends Controller {
3359
3363
  return;
3360
3364
  if (element.closest(".midwest-repeatable"))
3361
3365
  return;
3366
+ if (element.closest(".midwest-address"))
3367
+ return;
3362
3368
  if (element instanceof HTMLInputElement) {
3363
3369
  if (SKIPPED_INPUT_TYPES.has(element.type))
3364
3370
  return;
@@ -3478,6 +3484,29 @@ class FormLiveSummary extends Controller {
3478
3484
  });
3479
3485
  });
3480
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
+ });
3481
3510
  return fields;
3482
3511
  }
3483
3512
  // Builds a small inline colour swatch to display alongside a colour value.
@@ -3496,6 +3525,31 @@ class FormLiveSummary extends Controller {
3496
3525
  }
3497
3526
  return swatch;
3498
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
+ }
3499
3553
  // Finds the label associated with a specific element via `for` attribute,
3500
3554
  // wrapping label, or falls back to a prettified version of the element name.
3501
3555
  labelFor(el) {
@@ -3525,13 +3579,30 @@ class FormLiveSummary extends Controller {
3525
3579
  }
3526
3580
 
3527
3581
  class Form extends Controller {
3582
+ static values = {
3583
+ // Auto-submit the form (debounced) whenever a field changes.
3584
+ autosave: { type: Boolean, default: false },
3585
+ autosaveDelay: { type: Number, default: 600 },
3586
+ // Emit success/error events and keep buttons interactive after an in-page
3587
+ // (non-navigating) Turbo submission.
3588
+ ajax: { type: Boolean, default: false }
3589
+ };
3590
+ autosaveTimer;
3528
3591
  connect() {
3529
3592
  this.element.addEventListener("submit", this.handleSubmit);
3530
3593
  this.element.addEventListener("turbo:submit-end", this.handleTurboEnd);
3594
+ if (this.autosaveValue) {
3595
+ this.element.addEventListener("input", this.scheduleAutosave);
3596
+ this.element.addEventListener("change", this.scheduleAutosave);
3597
+ }
3531
3598
  }
3532
3599
  disconnect() {
3533
3600
  this.element.removeEventListener("submit", this.handleSubmit);
3534
3601
  this.element.removeEventListener("turbo:submit-end", this.handleTurboEnd);
3602
+ this.element.removeEventListener("input", this.scheduleAutosave);
3603
+ this.element.removeEventListener("change", this.scheduleAutosave);
3604
+ if (this.autosaveTimer != null)
3605
+ clearTimeout(this.autosaveTimer);
3535
3606
  }
3536
3607
  handleSubmit = () => {
3537
3608
  this.submitButtons.forEach((btn) => {
@@ -3539,15 +3610,32 @@ class Form extends Controller {
3539
3610
  btn.disabled = true;
3540
3611
  });
3541
3612
  };
3542
- // Restore buttons if the submission fails (e.g. a Turbo Frame validation
3543
- // error that re-renders the form without a full page transition).
3544
3613
  handleTurboEnd = (event) => {
3545
- if (!event.detail?.success) {
3614
+ const detail = event.detail;
3615
+ const success = detail?.success ?? false;
3616
+ if (!success || this.ajaxValue || this.autosaveValue) {
3546
3617
  this.submitButtons.forEach((btn) => {
3547
3618
  btn.classList.remove("loading");
3548
3619
  btn.disabled = false;
3549
3620
  });
3550
3621
  }
3622
+ if (this.ajaxValue) {
3623
+ this.dispatch(success ? "success" : "error", { detail });
3624
+ }
3625
+ };
3626
+ // Debounce field changes into a single background submission. `requestSubmit`
3627
+ // runs constraint validation and fires the `submit` event, so buttons pick up
3628
+ // their loading state and Turbo handles the request just like a manual submit.
3629
+ scheduleAutosave = () => {
3630
+ if (this.autosaveTimer != null)
3631
+ clearTimeout(this.autosaveTimer);
3632
+ this.autosaveTimer = setTimeout(() => {
3633
+ if (typeof this.element.requestSubmit === "function") {
3634
+ this.element.requestSubmit();
3635
+ } else {
3636
+ this.element.submit();
3637
+ }
3638
+ }, this.autosaveDelayValue);
3551
3639
  };
3552
3640
  get submitButtons() {
3553
3641
  return Array.from(
@@ -7060,14 +7148,14 @@ const FOCUSABLE = [
7060
7148
  ].join(", ");
7061
7149
  const GAP = 12;
7062
7150
  class Onboarding extends Controller {
7063
- static targets = ["step", "overlay", "card", "title", "body", "back", "next", "done", "arrow"];
7151
+ static targets = ["step", "overlay", "card", "title", "body", "back", "next", "done", "arrow", "motion"];
7064
7152
  static values = {
7065
- autoOpen: { type: Boolean, default: false },
7066
- confirm: { type: String, default: "" }
7153
+ autoOpen: { type: Boolean, default: false }
7067
7154
  };
7068
7155
  steps = [];
7069
7156
  index = 0;
7070
7157
  active = false;
7158
+ justOpened = false;
7071
7159
  previousFocus = null;
7072
7160
  highlighted = null;
7073
7161
  connect() {
@@ -7097,6 +7185,7 @@ class Onboarding extends Controller {
7097
7185
  this.previousFocus = document.activeElement;
7098
7186
  this.overlayTarget.hidden = false;
7099
7187
  this.cardTarget.hidden = false;
7188
+ this.justOpened = true;
7100
7189
  document.addEventListener("keydown", this.handleKeydown, true);
7101
7190
  window.addEventListener("resize", this.reposition);
7102
7191
  window.addEventListener("scroll", this.reposition, true);
@@ -7131,14 +7220,14 @@ class Onboarding extends Controller {
7131
7220
  cancel() {
7132
7221
  if (!this.active)
7133
7222
  return;
7134
- if (this.confirmValue && !window.confirm(this.confirmValue))
7135
- return;
7136
7223
  this.teardown();
7137
7224
  this.dispatch("cancel", { bubbles: true });
7138
7225
  }
7139
7226
  render(index) {
7140
7227
  this.index = index;
7141
7228
  const step = this.steps[index];
7229
+ const replay = !this.justOpened;
7230
+ this.justOpened = false;
7142
7231
  this.clearHighlight();
7143
7232
  this.titleTarget.textContent = step.title;
7144
7233
  this.renderBody(step);
@@ -7150,6 +7239,9 @@ class Onboarding extends Controller {
7150
7239
  } else {
7151
7240
  this.center();
7152
7241
  }
7242
+ if (replay && this.hasMotionTarget) {
7243
+ this.replayMotion(this.cardTarget.dataset.placement ?? "center");
7244
+ }
7153
7245
  this.dispatch("show", { detail: { index, name: step.name }, bubbles: true });
7154
7246
  this.focusCard();
7155
7247
  }
@@ -7251,9 +7343,34 @@ class Onboarding extends Controller {
7251
7343
  this.cardTarget.focus();
7252
7344
  });
7253
7345
  }
7346
+ // Re-triggers the midwest_motion @starting-style entry on step transitions.
7347
+ // The initial show is handled by @starting-style firing when the card's
7348
+ // `hidden` is removed; subsequent steps need an explicit re-trigger since the
7349
+ // card stays in the DOM. Toggling `hidden` on the motion element triggers
7350
+ // @starting-style again; the rAF ensures both DOM writes land in the same
7351
+ // paint so there is no blank-card flash.
7352
+ replayMotion(placement) {
7353
+ const el = this.motionTarget;
7354
+ const dir = { bottom: "up", top: "down", right: "left", left: "right" };
7355
+ el.classList.remove("from-up", "from-down", "from-left", "from-right");
7356
+ const from = dir[placement];
7357
+ if (from)
7358
+ el.classList.add(`from-${from}`);
7359
+ el.hidden = true;
7360
+ requestAnimationFrame(() => {
7361
+ if (this.active)
7362
+ el.hidden = false;
7363
+ });
7364
+ }
7254
7365
  teardown() {
7255
7366
  this.active = false;
7256
7367
  this.clearHighlight();
7368
+ this.element.querySelectorAll("[popover]").forEach((el) => {
7369
+ try {
7370
+ el.hidePopover();
7371
+ } catch {
7372
+ }
7373
+ });
7257
7374
  this.overlayTarget.hidden = true;
7258
7375
  this.cardTarget.hidden = true;
7259
7376
  document.removeEventListener("keydown", this.handleKeydown, true);
@@ -7892,6 +8009,885 @@ class Motion extends Controller {
7892
8009
  }
7893
8010
  }
7894
8011
 
8012
+ class PageTransition extends Controller {
8013
+ static values = {
8014
+ style: { type: String, default: "fade" },
8015
+ duration: { type: String, default: "" },
8016
+ easing: { type: String, default: "" }
8017
+ };
8018
+ injectedMeta = null;
8019
+ connect() {
8020
+ if (this.styleValue === "none")
8021
+ return;
8022
+ if (!document.querySelector('meta[name="view-transition"]')) {
8023
+ const meta = document.createElement("meta");
8024
+ meta.name = "view-transition";
8025
+ meta.content = "same-origin";
8026
+ document.head.appendChild(meta);
8027
+ this.injectedMeta = meta;
8028
+ }
8029
+ document.documentElement.dataset.midwestPageTransition = this.styleValue;
8030
+ if (this.durationValue) {
8031
+ document.documentElement.style.setProperty("--page-transition-duration", this.durationValue);
8032
+ }
8033
+ if (this.easingValue) {
8034
+ document.documentElement.style.setProperty("--page-transition-easing", this.easingValue);
8035
+ }
8036
+ }
8037
+ disconnect() {
8038
+ this.injectedMeta?.remove();
8039
+ this.injectedMeta = null;
8040
+ delete document.documentElement.dataset.midwestPageTransition;
8041
+ document.documentElement.style.removeProperty("--page-transition-duration");
8042
+ document.documentElement.style.removeProperty("--page-transition-easing");
8043
+ }
8044
+ }
8045
+
8046
+ let mapkitScriptPromise = null;
8047
+ let googleMapsScriptPromise = null;
8048
+ let leafletScriptPromise = null;
8049
+ function loadMapKitScript() {
8050
+ if (window.mapkit)
8051
+ return Promise.resolve();
8052
+ if (mapkitScriptPromise)
8053
+ return mapkitScriptPromise;
8054
+ mapkitScriptPromise = new Promise((resolve, reject) => {
8055
+ const script = document.createElement("script");
8056
+ script.src = "https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js";
8057
+ script.crossOrigin = "anonymous";
8058
+ script.addEventListener("load", () => resolve());
8059
+ script.addEventListener("error", () => reject(new Error("Failed to load MapKit JS")));
8060
+ document.head.appendChild(script);
8061
+ });
8062
+ return mapkitScriptPromise;
8063
+ }
8064
+ function loadLeafletScript() {
8065
+ if (window.L)
8066
+ return Promise.resolve();
8067
+ if (leafletScriptPromise)
8068
+ return leafletScriptPromise;
8069
+ leafletScriptPromise = new Promise((resolve, reject) => {
8070
+ if (!document.querySelector("link[data-midwest-leaflet]")) {
8071
+ const link = document.createElement("link");
8072
+ link.rel = "stylesheet";
8073
+ link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
8074
+ link.dataset.midwestLeaflet = "";
8075
+ document.head.appendChild(link);
8076
+ }
8077
+ const script = document.createElement("script");
8078
+ script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
8079
+ script.addEventListener("load", () => resolve());
8080
+ script.addEventListener("error", () => reject(new Error("Failed to load Leaflet")));
8081
+ document.head.appendChild(script);
8082
+ });
8083
+ return leafletScriptPromise;
8084
+ }
8085
+ function loadGoogleMapsScript(apiKey) {
8086
+ if (window.google?.maps)
8087
+ return Promise.resolve();
8088
+ if (googleMapsScriptPromise)
8089
+ return googleMapsScriptPromise;
8090
+ googleMapsScriptPromise = new Promise((resolve, reject) => {
8091
+ window.__midwestGoogleMapsReady = resolve;
8092
+ const script = document.createElement("script");
8093
+ script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&callback=__midwestGoogleMapsReady&loading=async&libraries=places`;
8094
+ script.addEventListener("error", () => reject(new Error("Failed to load Google Maps")));
8095
+ document.head.appendChild(script);
8096
+ });
8097
+ return googleMapsScriptPromise;
8098
+ }
8099
+
8100
+ const LEAFLET_TILES = {
8101
+ MutedStandard: {
8102
+ url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
8103
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
8104
+ },
8105
+ Standard: {
8106
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
8107
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
8108
+ maxZoom: 19
8109
+ },
8110
+ Hybrid: {
8111
+ url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
8112
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
8113
+ },
8114
+ Satellite: {
8115
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
8116
+ attribution: "Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",
8117
+ maxZoom: 18
8118
+ },
8119
+ Dark: {
8120
+ url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
8121
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
8122
+ }
8123
+ };
8124
+ const GOOGLE_MUTED_STYLES = [
8125
+ { elementType: "geometry", stylers: [{ color: "#f5f5f5" }] },
8126
+ { elementType: "labels.icon", stylers: [{ visibility: "off" }] },
8127
+ { elementType: "labels.text.fill", stylers: [{ color: "#616161" }] },
8128
+ { elementType: "labels.text.stroke", stylers: [{ color: "#f5f5f5" }] },
8129
+ { featureType: "administrative.land_parcel", elementType: "labels", stylers: [{ visibility: "off" }] },
8130
+ { featureType: "poi", elementType: "geometry", stylers: [{ color: "#eeeeee" }] },
8131
+ { featureType: "poi.park", elementType: "geometry", stylers: [{ color: "#e5e5e5" }] },
8132
+ { featureType: "road", elementType: "geometry", stylers: [{ color: "#ffffff" }] },
8133
+ { featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#dadada" }] },
8134
+ { featureType: "water", elementType: "geometry", stylers: [{ color: "#c9c9c9" }] }
8135
+ ];
8136
+ const GOOGLE_DARK_STYLES = [
8137
+ { elementType: "geometry", stylers: [{ color: "#242f3e" }] },
8138
+ { elementType: "labels.text.stroke", stylers: [{ color: "#242f3e" }] },
8139
+ { elementType: "labels.text.fill", stylers: [{ color: "#746855" }] },
8140
+ { featureType: "administrative.locality", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }] },
8141
+ { featureType: "poi", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }] },
8142
+ { featureType: "poi.park", elementType: "geometry", stylers: [{ color: "#263c3f" }] },
8143
+ { featureType: "poi.park", elementType: "labels.text.fill", stylers: [{ color: "#6b9a76" }] },
8144
+ { featureType: "road", elementType: "geometry", stylers: [{ color: "#38414e" }] },
8145
+ { featureType: "road", elementType: "geometry.stroke", stylers: [{ color: "#212a37" }] },
8146
+ { featureType: "road", elementType: "labels.text.fill", stylers: [{ color: "#9ca5b3" }] },
8147
+ { featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#746855" }] },
8148
+ { featureType: "road.highway", elementType: "geometry.stroke", stylers: [{ color: "#1f2835" }] },
8149
+ { featureType: "road.highway", elementType: "labels.text.fill", stylers: [{ color: "#f3d19c" }] },
8150
+ { featureType: "transit", elementType: "geometry", stylers: [{ color: "#2f3948" }] },
8151
+ { featureType: "transit.station", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }] },
8152
+ { featureType: "water", elementType: "geometry", stylers: [{ color: "#17263c" }] },
8153
+ { featureType: "water", elementType: "labels.text.fill", stylers: [{ color: "#515c6d" }] },
8154
+ { featureType: "water", elementType: "labels.text.stroke", stylers: [{ color: "#17263c" }] }
8155
+ ];
8156
+ let Map$1 = class Map extends Controller {
8157
+ static values = {
8158
+ latitude: { type: Number, default: 37.334886 },
8159
+ longitude: { type: Number, default: -122.008988 },
8160
+ span: { type: Number, default: 0.1 },
8161
+ mapType: { type: String, default: "MutedStandard" },
8162
+ colorScheme: { type: String, default: "Auto" },
8163
+ provider: { type: String, default: "" },
8164
+ token: { type: String, default: "" },
8165
+ tokenUrl: { type: String, default: "" },
8166
+ apiKey: { type: String, default: "" },
8167
+ zoom: { type: Number, default: -1 },
8168
+ markers: { type: Array, default: [] },
8169
+ showsUserLocation: { type: Boolean, default: false },
8170
+ showsMapTypeControl: { type: Boolean, default: false },
8171
+ showsZoomControl: { type: Boolean, default: true },
8172
+ showsCompass: { type: String, default: "Adaptive" },
8173
+ rotation: { type: Number, default: 0 }
8174
+ };
8175
+ mapInstance = null;
8176
+ async connect() {
8177
+ const provider = this.providerValue;
8178
+ if (!provider)
8179
+ return;
8180
+ try {
8181
+ if (provider === "mapkit") {
8182
+ await loadMapKitScript();
8183
+ this.initMapKit();
8184
+ this.initMapKitMap();
8185
+ } else if (provider === "google") {
8186
+ await loadGoogleMapsScript(this.apiKeyValue);
8187
+ this.initGoogleMap();
8188
+ } else if (provider === "leaflet") {
8189
+ await loadLeafletScript();
8190
+ this.initLeafletMap();
8191
+ }
8192
+ } catch (err) {
8193
+ console.error(`[midwest-map] Failed to initialise ${provider} map:`, err);
8194
+ }
8195
+ }
8196
+ disconnect() {
8197
+ if (this.mapInstance) {
8198
+ if (this.providerValue === "mapkit")
8199
+ this.mapInstance.destroy();
8200
+ if (this.providerValue === "leaflet")
8201
+ this.mapInstance.remove();
8202
+ this.mapInstance = null;
8203
+ }
8204
+ }
8205
+ tokenValueChanged() {
8206
+ if (this.mapInstance && this.providerValue === "mapkit")
8207
+ this.initMapKit();
8208
+ }
8209
+ // --- MapKit ---
8210
+ initMapKit() {
8211
+ const mk = window.mapkit;
8212
+ const token = this.tokenValue;
8213
+ const tokenUrl = this.tokenUrlValue;
8214
+ mk.init({
8215
+ authorizationCallback: (done) => {
8216
+ if (token) {
8217
+ done(token);
8218
+ } else if (tokenUrl) {
8219
+ fetch(tokenUrl).then((r) => r.text()).then(done).catch((err) => console.error("[midwest-map] Token fetch failed:", err));
8220
+ }
8221
+ },
8222
+ language: document.documentElement.lang || "en"
8223
+ });
8224
+ }
8225
+ initMapKitMap() {
8226
+ const mk = window.mapkit;
8227
+ const region = new mk.CoordinateRegion(
8228
+ new mk.Coordinate(this.latitudeValue, this.longitudeValue),
8229
+ new mk.CoordinateSpan(this.spanValue, this.spanValue)
8230
+ );
8231
+ 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];
8232
+ this.mapInstance = new mk.Map(this.element, {
8233
+ region,
8234
+ mapType: mk.Map.MapTypes[this.mapTypeValue],
8235
+ colorScheme,
8236
+ showsUserLocation: this.showsUserLocationValue,
8237
+ showsMapTypeControl: this.showsMapTypeControlValue,
8238
+ showsZoomControl: this.showsZoomControlValue,
8239
+ showsCompass: mk.FeatureVisibility[this.showsCompassValue],
8240
+ rotation: this.rotationValue
8241
+ });
8242
+ this.addMapKitMarkers();
8243
+ this.element.dataset.mapLoaded = "true";
8244
+ }
8245
+ addMapKitMarkers() {
8246
+ const mk = window.mapkit;
8247
+ if (!this.markersValue.length)
8248
+ return;
8249
+ const annotations = this.markersValue.map((m) => {
8250
+ const opts = {};
8251
+ if (m.title)
8252
+ opts.title = m.title;
8253
+ if (m.subtitle)
8254
+ opts.subtitle = m.subtitle;
8255
+ if (m.color)
8256
+ opts.color = m.color;
8257
+ if (m.glyphText)
8258
+ opts.glyphText = m.glyphText;
8259
+ return new mk.MarkerAnnotation(
8260
+ new mk.Coordinate(m.latitude, m.longitude),
8261
+ opts
8262
+ );
8263
+ });
8264
+ this.mapInstance.addAnnotations(annotations);
8265
+ const openIndex = this.markersValue.findIndex((m) => m.open);
8266
+ if (openIndex >= 0) {
8267
+ this.mapInstance.selectedAnnotation = annotations[openIndex];
8268
+ }
8269
+ }
8270
+ // --- Google Maps ---
8271
+ initGoogleMap() {
8272
+ const g = window.google;
8273
+ const zoom = this.zoomValue > 0 ? this.zoomValue : this.spanToZoom(this.spanValue);
8274
+ const isDark = this.colorSchemeValue === "Dark" || this.colorSchemeValue === "Auto" && window.matchMedia("(prefers-color-scheme: dark)").matches;
8275
+ const mapTypeId = this.googleMapTypeId();
8276
+ const opts = {
8277
+ center: { lat: this.latitudeValue, lng: this.longitudeValue },
8278
+ zoom,
8279
+ mapTypeId,
8280
+ disableDefaultUI: true,
8281
+ zoomControl: this.showsZoomControlValue,
8282
+ mapTypeControl: this.showsMapTypeControlValue,
8283
+ fullscreenControl: false,
8284
+ streetViewControl: false
8285
+ };
8286
+ const styles = this.googleMapStyles(isDark, mapTypeId);
8287
+ if (styles)
8288
+ opts.styles = styles;
8289
+ this.mapInstance = new g.maps.Map(this.element, opts);
8290
+ this.addGoogleMarkers();
8291
+ this.element.dataset.mapLoaded = "true";
8292
+ }
8293
+ googleMapTypeId() {
8294
+ const types = {
8295
+ Standard: "roadmap",
8296
+ Hybrid: "hybrid",
8297
+ Satellite: "satellite",
8298
+ MutedStandard: "roadmap"
8299
+ };
8300
+ return types[this.mapTypeValue] ?? "roadmap";
8301
+ }
8302
+ googleMapStyles(isDark, mapTypeId) {
8303
+ if (mapTypeId !== "roadmap")
8304
+ return null;
8305
+ if (isDark)
8306
+ return GOOGLE_DARK_STYLES;
8307
+ if (this.mapTypeValue === "MutedStandard")
8308
+ return GOOGLE_MUTED_STYLES;
8309
+ return null;
8310
+ }
8311
+ addGoogleMarkers() {
8312
+ const g = window.google;
8313
+ if (!this.markersValue.length)
8314
+ return;
8315
+ this.markersValue.forEach((m) => {
8316
+ const marker = new g.maps.Marker({
8317
+ map: this.mapInstance,
8318
+ position: { lat: m.latitude, lng: m.longitude },
8319
+ title: m.title ?? ""
8320
+ });
8321
+ if (m.open && m.title) {
8322
+ const content = m.subtitle ? `<strong>${m.title}</strong><br><span style="color:#777">${m.subtitle}</span>` : `<strong>${m.title}</strong>`;
8323
+ const infoWindow = new g.maps.InfoWindow({ content });
8324
+ infoWindow.open(this.mapInstance, marker);
8325
+ }
8326
+ });
8327
+ }
8328
+ // --- Leaflet ---
8329
+ initLeafletMap() {
8330
+ const L = window.L;
8331
+ const zoom = this.zoomValue > 0 ? this.zoomValue : this.spanToZoom(this.spanValue);
8332
+ const isDark = this.colorSchemeValue === "Dark" || this.colorSchemeValue === "Auto" && window.matchMedia("(prefers-color-scheme: dark)").matches;
8333
+ const tileKey = isDark ? "Dark" : this.mapTypeValue;
8334
+ const tile = LEAFLET_TILES[tileKey] ?? LEAFLET_TILES.MutedStandard;
8335
+ this.mapInstance = L.map(this.element, {
8336
+ center: [this.latitudeValue, this.longitudeValue],
8337
+ zoom,
8338
+ zoomControl: this.showsZoomControlValue,
8339
+ attributionControl: true
8340
+ });
8341
+ L.tileLayer(tile.url, {
8342
+ attribution: tile.attribution,
8343
+ maxZoom: tile.maxZoom ?? 19
8344
+ }).addTo(this.mapInstance);
8345
+ this.addLeafletMarkers();
8346
+ this.element.dataset.mapLoaded = "true";
8347
+ }
8348
+ addLeafletMarkers() {
8349
+ const L = window.L;
8350
+ if (!this.markersValue.length)
8351
+ return;
8352
+ this.markersValue.forEach((m) => {
8353
+ const icon = this.leafletMarkerIcon(m.color, m.glyphText);
8354
+ const marker = L.marker([m.latitude, m.longitude], { icon }).addTo(this.mapInstance);
8355
+ if (m.title) {
8356
+ const content = m.subtitle ? `<strong>${m.title}</strong><br><span style="color:#777">${m.subtitle}</span>` : `<strong>${m.title}</strong>`;
8357
+ marker.bindPopup(content);
8358
+ if (m.open)
8359
+ marker.openPopup();
8360
+ }
8361
+ });
8362
+ }
8363
+ // Returns a Leaflet icon — default blue when no color/glyph, otherwise a
8364
+ // custom SVG pin rendered via divIcon.
8365
+ leafletMarkerIcon(color, glyphText) {
8366
+ const L = window.L;
8367
+ if (!color && !glyphText)
8368
+ return new L.Icon.Default();
8369
+ const fill = color ?? "#2563eb";
8370
+ 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)"/>`;
8371
+ const svg = [
8372
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="24" height="36">',
8373
+ ` <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}"/>`,
8374
+ ` ${inner}`,
8375
+ "</svg>"
8376
+ ].join("");
8377
+ return L.divIcon({
8378
+ html: svg,
8379
+ className: "",
8380
+ iconSize: [24, 36],
8381
+ iconAnchor: [12, 36],
8382
+ popupAnchor: [0, -36]
8383
+ });
8384
+ }
8385
+ // Approximate MapKit coordinate span to a Web Mercator zoom level.
8386
+ // CoordinateSpan 360° = zoom 0; halving span adds 1 zoom level.
8387
+ spanToZoom(span) {
8388
+ return Math.round(Math.log2(360 / span));
8389
+ }
8390
+ };
8391
+
8392
+ const DEBOUNCE_MS = 300;
8393
+ const GOOGLE_PREVIEW_STYLES = [
8394
+ { elementType: "geometry", stylers: [{ color: "#f5f5f5" }] },
8395
+ { elementType: "labels.icon", stylers: [{ visibility: "off" }] },
8396
+ { elementType: "labels.text.fill", stylers: [{ color: "#616161" }] },
8397
+ { elementType: "labels.text.stroke", stylers: [{ color: "#f5f5f5" }] },
8398
+ { featureType: "road", elementType: "geometry", stylers: [{ color: "#ffffff" }] },
8399
+ { featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#dadada" }] },
8400
+ { featureType: "water", elementType: "geometry", stylers: [{ color: "#c9c9c9" }] },
8401
+ { featureType: "poi", elementType: "geometry", stylers: [{ color: "#eeeeee" }] }
8402
+ ];
8403
+ class Address extends Controller {
8404
+ static targets = [
8405
+ "input",
8406
+ "dropdown",
8407
+ "suggestions",
8408
+ "latInput",
8409
+ "lngInput",
8410
+ "placeIdInput",
8411
+ "mapContainer"
8412
+ ];
8413
+ static values = {
8414
+ provider: { type: String, default: "" },
8415
+ mapProvider: { type: String, default: "" },
8416
+ apiKey: { type: String, default: "" },
8417
+ token: { type: String, default: "" },
8418
+ tokenUrl: { type: String, default: "" },
8419
+ countryCodes: { type: Array, default: [] },
8420
+ showMap: { type: Boolean, default: false },
8421
+ nominatimUrl: { type: String, default: "" }
8422
+ };
8423
+ searchTimer;
8424
+ highlightedIndex = -1;
8425
+ mapInstance = null;
8426
+ mapMarker = null;
8427
+ mkSearch = null;
8428
+ autocompleteService = null;
8429
+ placesService = null;
8430
+ // ── Named handlers ────────────────────────────────────────────────────────
8431
+ onToggle = (e) => {
8432
+ const opened = e.newState === "open";
8433
+ if (this.hasInputTarget) {
8434
+ this.inputTarget.setAttribute("aria-expanded", String(opened));
8435
+ }
8436
+ if (!opened) {
8437
+ this.highlightedIndex = -1;
8438
+ }
8439
+ };
8440
+ onFocusOut = (e) => {
8441
+ if (!this.element.contains(e.relatedTarget)) {
8442
+ this.closeDropdown();
8443
+ }
8444
+ };
8445
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
8446
+ async connect() {
8447
+ this.element.addEventListener("focusout", this.onFocusOut);
8448
+ const provider = this.providerValue;
8449
+ const mapProv = this.effectiveMapProvider();
8450
+ if (!provider && !mapProv)
8451
+ return;
8452
+ try {
8453
+ if (provider === "mapkit") {
8454
+ await loadMapKitScript();
8455
+ this.initMapKit();
8456
+ } else if (provider === "google") {
8457
+ await loadGoogleMapsScript(this.apiKeyValue);
8458
+ this.initGoogle();
8459
+ }
8460
+ if (this.showMapValue && mapProv === "leaflet") {
8461
+ await loadLeafletScript();
8462
+ }
8463
+ } catch (err) {
8464
+ console.error("[midwest-address] Failed to initialise:", err);
8465
+ }
8466
+ }
8467
+ disconnect() {
8468
+ clearTimeout(this.searchTimer);
8469
+ this.element.removeEventListener("focusout", this.onFocusOut);
8470
+ if (this.mapInstance) {
8471
+ const mapProv = this.effectiveMapProvider();
8472
+ if (mapProv === "mapkit")
8473
+ this.mapInstance.destroy();
8474
+ if (mapProv === "leaflet")
8475
+ this.mapInstance.remove();
8476
+ }
8477
+ this.mapInstance = null;
8478
+ }
8479
+ dropdownTargetConnected(el) {
8480
+ el.addEventListener("toggle", this.onToggle);
8481
+ }
8482
+ dropdownTargetDisconnected(el) {
8483
+ el.removeEventListener("toggle", this.onToggle);
8484
+ }
8485
+ // ── Public Stimulus actions ───────────────────────────────────────────────
8486
+ search() {
8487
+ this.clearGeocode();
8488
+ const query = this.inputTarget.value.trim();
8489
+ if (query.length < 2) {
8490
+ this.closeDropdown();
8491
+ return;
8492
+ }
8493
+ clearTimeout(this.searchTimer);
8494
+ this.searchTimer = setTimeout(() => {
8495
+ if (this.providerValue === "mapkit") {
8496
+ this.searchMapKit(query);
8497
+ } else if (this.providerValue === "google") {
8498
+ this.searchGoogle(query);
8499
+ } else if (this.providerValue === "nominatim") {
8500
+ this.searchNominatim(query);
8501
+ }
8502
+ }, DEBOUNCE_MS);
8503
+ }
8504
+ navigate(event) {
8505
+ const items = this.suggestionElements();
8506
+ if (!items.length)
8507
+ return;
8508
+ switch (event.key) {
8509
+ case "ArrowDown":
8510
+ event.preventDefault();
8511
+ this.highlightedIndex = Math.min(this.highlightedIndex + 1, items.length - 1);
8512
+ this.applyHighlight(items);
8513
+ break;
8514
+ case "ArrowUp":
8515
+ event.preventDefault();
8516
+ this.highlightedIndex = Math.max(this.highlightedIndex - 1, -1);
8517
+ this.applyHighlight(items);
8518
+ break;
8519
+ case "Enter":
8520
+ event.preventDefault();
8521
+ if (this.highlightedIndex >= 0)
8522
+ items[this.highlightedIndex].dispatchEvent(new MouseEvent("mousedown"));
8523
+ break;
8524
+ case "Escape":
8525
+ this.closeDropdown();
8526
+ break;
8527
+ }
8528
+ }
8529
+ // ── Provider init ─────────────────────────────────────────────────────────
8530
+ initMapKit() {
8531
+ const mk = window.mapkit;
8532
+ const { tokenValue: token, tokenUrlValue: tokenUrl } = this;
8533
+ mk.init({
8534
+ authorizationCallback: (done) => {
8535
+ if (token) {
8536
+ done(token);
8537
+ } else if (tokenUrl) {
8538
+ fetch(tokenUrl).then((r) => r.text()).then(done).catch((err) => console.error("[midwest-address] Token fetch failed:", err));
8539
+ }
8540
+ },
8541
+ language: document.documentElement.lang || "en"
8542
+ });
8543
+ this.mkSearch = new mk.Search({ getsUserLocation: false });
8544
+ }
8545
+ initGoogle() {
8546
+ const g = window.google;
8547
+ this.autocompleteService = new g.maps.places.AutocompleteService();
8548
+ this.placesService = new g.maps.places.PlacesService(document.createElement("div"));
8549
+ }
8550
+ // ── Search ────────────────────────────────────────────────────────────────
8551
+ searchMapKit(query) {
8552
+ if (!this.mkSearch)
8553
+ return;
8554
+ this.mkSearch.autocomplete(query, (err, data) => {
8555
+ if (err || !data?.results?.length) {
8556
+ this.closeDropdown();
8557
+ return;
8558
+ }
8559
+ const results = data.results.map((r) => ({
8560
+ id: r.completionURL ?? r.displayLines?.join(", ") ?? "",
8561
+ label: r.displayLines?.[0] ?? "",
8562
+ secondary: r.displayLines?.[1] ?? "",
8563
+ payload: r
8564
+ }));
8565
+ this.renderSuggestions(results);
8566
+ });
8567
+ }
8568
+ searchGoogle(query) {
8569
+ if (!this.autocompleteService)
8570
+ return;
8571
+ const request = { input: query };
8572
+ if (this.countryCodesValue.length) {
8573
+ request.componentRestrictions = { country: this.countryCodesValue };
8574
+ }
8575
+ this.autocompleteService.getPlacePredictions(
8576
+ request,
8577
+ (predictions, status) => {
8578
+ const g = window.google;
8579
+ if (status !== g.maps.places.PlacesServiceStatus.OK || !predictions?.length) {
8580
+ this.closeDropdown();
8581
+ return;
8582
+ }
8583
+ const results = predictions.map((p) => ({
8584
+ id: p.place_id,
8585
+ label: p.structured_formatting?.main_text ?? p.description,
8586
+ secondary: p.structured_formatting?.secondary_text ?? "",
8587
+ payload: p
8588
+ }));
8589
+ this.renderSuggestions(results);
8590
+ }
8591
+ );
8592
+ }
8593
+ // ── Suggestions UI ────────────────────────────────────────────────────────
8594
+ renderSuggestions(results) {
8595
+ if (!this.hasSuggestionsTarget)
8596
+ return;
8597
+ const list = this.suggestionsTarget;
8598
+ list.innerHTML = "";
8599
+ results.forEach((r, i) => {
8600
+ const li = document.createElement("li");
8601
+ li.className = "midwest-address__suggestion";
8602
+ li.setAttribute("role", "option");
8603
+ li.setAttribute("id", `${this.element.id || "addr"}-s${i}`);
8604
+ li.dataset.index = String(i);
8605
+ const main = document.createElement("span");
8606
+ main.className = "midwest-address__suggestion-main";
8607
+ main.textContent = r.label;
8608
+ li.appendChild(main);
8609
+ if (r.secondary) {
8610
+ const sec = document.createElement("span");
8611
+ sec.className = "midwest-address__suggestion-secondary";
8612
+ sec.textContent = r.secondary;
8613
+ li.appendChild(sec);
8614
+ }
8615
+ li.addEventListener("mousedown", (e) => {
8616
+ e.preventDefault();
8617
+ this.pickSuggestion(r);
8618
+ });
8619
+ list.appendChild(li);
8620
+ });
8621
+ this.openDropdown();
8622
+ this.highlightedIndex = -1;
8623
+ }
8624
+ openDropdown() {
8625
+ if (!this.hasDropdownTarget)
8626
+ return;
8627
+ if (!this.dropdownTarget.matches(":popover-open"))
8628
+ this.dropdownTarget.showPopover();
8629
+ }
8630
+ closeDropdown() {
8631
+ if (this.hasDropdownTarget && this.dropdownTarget.matches(":popover-open")) {
8632
+ this.dropdownTarget.hidePopover();
8633
+ }
8634
+ if (this.hasSuggestionsTarget)
8635
+ this.suggestionsTarget.innerHTML = "";
8636
+ this.highlightedIndex = -1;
8637
+ }
8638
+ suggestionElements() {
8639
+ if (!this.hasSuggestionsTarget)
8640
+ return [];
8641
+ return Array.from(this.suggestionsTarget.querySelectorAll(".midwest-address__suggestion"));
8642
+ }
8643
+ applyHighlight(items) {
8644
+ items.forEach((item, i) => {
8645
+ const active = i === this.highlightedIndex;
8646
+ item.classList.toggle("is-highlighted", active);
8647
+ item.setAttribute("aria-selected", String(active));
8648
+ if (active) {
8649
+ this.inputTarget.setAttribute("aria-activedescendant", item.id);
8650
+ item.scrollIntoView({ block: "nearest" });
8651
+ }
8652
+ });
8653
+ if (this.highlightedIndex < 0) {
8654
+ this.inputTarget.removeAttribute("aria-activedescendant");
8655
+ }
8656
+ }
8657
+ // ── Pick a suggestion ─────────────────────────────────────────────────────
8658
+ pickSuggestion(r) {
8659
+ this.closeDropdown();
8660
+ if (this.providerValue === "mapkit") {
8661
+ this.pickMapKit(r);
8662
+ } else if (this.providerValue === "google") {
8663
+ this.pickGoogle(r);
8664
+ } else if (this.providerValue === "nominatim") {
8665
+ this.pickNominatim(r);
8666
+ }
8667
+ }
8668
+ pickMapKit(r) {
8669
+ if (!this.mkSearch)
8670
+ return;
8671
+ this.mkSearch.search(r.payload, (err, data) => {
8672
+ if (err || !data?.places?.length)
8673
+ return;
8674
+ const place = data.places[0];
8675
+ const coord = place.coordinate;
8676
+ this.inputTarget.value = [r.label, r.secondary].filter(Boolean).join(", ");
8677
+ this.fillGeocode(coord.latitude, coord.longitude, place.id ?? "");
8678
+ this.updateMapPreview(coord.latitude, coord.longitude);
8679
+ });
8680
+ }
8681
+ pickGoogle(r) {
8682
+ if (!this.placesService || !r.id)
8683
+ return;
8684
+ const g = window.google;
8685
+ this.placesService.getDetails(
8686
+ { placeId: r.id, fields: ["geometry", "formatted_address", "place_id"] },
8687
+ (place, status) => {
8688
+ if (status !== g.maps.places.PlacesServiceStatus.OK || !place)
8689
+ return;
8690
+ const lat = place.geometry.location.lat();
8691
+ const lng = place.geometry.location.lng();
8692
+ this.inputTarget.value = place.formatted_address;
8693
+ this.fillGeocode(lat, lng, place.place_id);
8694
+ this.updateMapPreview(lat, lng);
8695
+ }
8696
+ );
8697
+ }
8698
+ // ── Geocode field management ──────────────────────────────────────────────
8699
+ fillGeocode(lat, lng, placeId) {
8700
+ if (this.hasLatInputTarget)
8701
+ this.latInputTarget.value = String(lat);
8702
+ if (this.hasLngInputTarget)
8703
+ this.lngInputTarget.value = String(lng);
8704
+ if (this.hasPlaceIdInputTarget)
8705
+ this.placeIdInputTarget.value = placeId;
8706
+ this.dispatch("geocode", { detail: { lat, lng, placeId } });
8707
+ }
8708
+ clearGeocode() {
8709
+ if (this.hasLatInputTarget)
8710
+ this.latInputTarget.value = "";
8711
+ if (this.hasLngInputTarget)
8712
+ this.lngInputTarget.value = "";
8713
+ if (this.hasPlaceIdInputTarget)
8714
+ this.placeIdInputTarget.value = "";
8715
+ }
8716
+ // ── Nominatim search (OpenStreetMap, no API key) ──────────────────────────
8717
+ searchNominatim(query) {
8718
+ const base = this.nominatimUrlValue || "https://nominatim.openstreetmap.org";
8719
+ const url = `${base}/search?q=${encodeURIComponent(query)}&format=json&limit=5&addressdetails=1`;
8720
+ fetch(url, { headers: { "Accept-Language": document.documentElement.lang || "en" } }).then((r) => r.json()).then((data) => {
8721
+ if (!data?.length) {
8722
+ this.closeDropdown();
8723
+ return;
8724
+ }
8725
+ const results = data.map((r) => {
8726
+ const parts = r.display_name.split(", ");
8727
+ return {
8728
+ id: String(r.place_id),
8729
+ label: r.name || parts[0] || r.display_name,
8730
+ secondary: parts.length > 1 ? parts.slice(1).join(", ") : "",
8731
+ payload: r
8732
+ };
8733
+ });
8734
+ this.renderSuggestions(results);
8735
+ }).catch(() => this.closeDropdown());
8736
+ }
8737
+ pickNominatim(r) {
8738
+ const place = r.payload;
8739
+ const lat = parseFloat(place.lat);
8740
+ const lng = parseFloat(place.lon);
8741
+ this.inputTarget.value = place.display_name;
8742
+ this.fillGeocode(lat, lng, `${place.osm_type}:${place.osm_id}`);
8743
+ this.updateMapPreview(lat, lng);
8744
+ }
8745
+ // ── Map preview ───────────────────────────────────────────────────────────
8746
+ // Returns the provider used for the map preview. `mapProviderValue` is set
8747
+ // when the geocoder and map providers differ; falls back to `providerValue`
8748
+ // so existing single-provider setups (mapkit/google) work without change.
8749
+ effectiveMapProvider() {
8750
+ return this.mapProviderValue || this.providerValue;
8751
+ }
8752
+ updateMapPreview(lat, lng) {
8753
+ if (!this.hasMapContainerTarget || !this.showMapValue)
8754
+ return;
8755
+ this.mapContainerTarget.hidden = false;
8756
+ const mapProv = this.effectiveMapProvider();
8757
+ if (mapProv === "mapkit") {
8758
+ this.updateMapKitPreview(lat, lng);
8759
+ } else if (mapProv === "google") {
8760
+ this.updateGooglePreview(lat, lng);
8761
+ } else if (mapProv === "leaflet") {
8762
+ this.updateLeafletPreview(lat, lng);
8763
+ }
8764
+ }
8765
+ updateMapKitPreview(lat, lng) {
8766
+ const mk = window.mapkit;
8767
+ const coord = new mk.Coordinate(lat, lng);
8768
+ if (!this.mapInstance) {
8769
+ this.mapInstance = new mk.Map(this.mapContainerTarget, {
8770
+ mapType: mk.Map.MapTypes.MutedStandard,
8771
+ showsCompass: mk.FeatureVisibility.Hidden,
8772
+ showsZoomControl: false,
8773
+ showsMapTypeControl: false,
8774
+ isScrollEnabled: false,
8775
+ isZoomEnabled: false
8776
+ });
8777
+ }
8778
+ this.mapInstance.setRegionAnimated(
8779
+ new mk.CoordinateRegion(coord, new mk.CoordinateSpan(0.01, 0.01))
8780
+ );
8781
+ if (this.mapMarker)
8782
+ this.mapInstance.removeAnnotation(this.mapMarker);
8783
+ this.mapMarker = new mk.MarkerAnnotation(coord);
8784
+ this.mapInstance.addAnnotation(this.mapMarker);
8785
+ }
8786
+ updateGooglePreview(lat, lng) {
8787
+ const g = window.google;
8788
+ const center = { lat, lng };
8789
+ if (!this.mapInstance) {
8790
+ this.mapInstance = new g.maps.Map(this.mapContainerTarget, {
8791
+ center,
8792
+ zoom: 15,
8793
+ disableDefaultUI: true,
8794
+ gestureHandling: "none",
8795
+ styles: GOOGLE_PREVIEW_STYLES
8796
+ });
8797
+ } else {
8798
+ this.mapInstance.setCenter(center);
8799
+ this.mapInstance.setZoom(15);
8800
+ }
8801
+ if (this.mapMarker)
8802
+ this.mapMarker.setMap(null);
8803
+ this.mapMarker = new g.maps.Marker({ map: this.mapInstance, position: center });
8804
+ }
8805
+ updateLeafletPreview(lat, lng) {
8806
+ const L = window.L;
8807
+ if (!L)
8808
+ return;
8809
+ if (!this.mapInstance) {
8810
+ this.mapInstance = L.map(this.mapContainerTarget, {
8811
+ center: [lat, lng],
8812
+ zoom: 15,
8813
+ zoomControl: false,
8814
+ attributionControl: false
8815
+ });
8816
+ L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
8817
+ maxZoom: 19
8818
+ }).addTo(this.mapInstance);
8819
+ } else {
8820
+ this.mapInstance.setView([lat, lng], 15);
8821
+ }
8822
+ if (this.mapMarker)
8823
+ this.mapMarker.remove();
8824
+ this.mapMarker = L.marker([lat, lng]).addTo(this.mapInstance);
8825
+ }
8826
+ }
8827
+
8828
+ class Popover extends Controller {
8829
+ animating = false;
8830
+ closeTimer = null;
8831
+ connect() {
8832
+ this.element.addEventListener("beforetoggle", this.onBeforeToggle);
8833
+ }
8834
+ disconnect() {
8835
+ this.element.removeEventListener("beforetoggle", this.onBeforeToggle);
8836
+ this.cleanUp();
8837
+ }
8838
+ onBeforeToggle = (event) => {
8839
+ const toggle = event;
8840
+ if (toggle.newState === "open") {
8841
+ if (this.closeTimer) {
8842
+ clearTimeout(this.closeTimer);
8843
+ this.closeTimer = null;
8844
+ }
8845
+ this.element.classList.remove("is-closing");
8846
+ this.animating = false;
8847
+ return;
8848
+ }
8849
+ if (toggle.newState !== "closed")
8850
+ return;
8851
+ if (window.matchMedia("(width < 700px)").matches)
8852
+ return;
8853
+ if (!this.element.classList.contains("midwest-motion"))
8854
+ return;
8855
+ if (this.animating)
8856
+ return;
8857
+ event.preventDefault();
8858
+ this.animating = true;
8859
+ this.element.classList.add("is-closing");
8860
+ this.closeTimer = setTimeout(() => {
8861
+ this.closeTimer = null;
8862
+ this.element.style.display = "none";
8863
+ this.element.style.transition = "none";
8864
+ this.element.hidePopover();
8865
+ this.animating = false;
8866
+ requestAnimationFrame(() => {
8867
+ this.element.style.removeProperty("display");
8868
+ this.element.style.removeProperty("transition");
8869
+ this.element.classList.remove("is-closing");
8870
+ });
8871
+ }, this.exitDuration());
8872
+ };
8873
+ cleanUp() {
8874
+ if (this.closeTimer) {
8875
+ clearTimeout(this.closeTimer);
8876
+ this.closeTimer = null;
8877
+ }
8878
+ this.element.classList.remove("is-closing");
8879
+ this.element.style.removeProperty("transition");
8880
+ this.animating = false;
8881
+ }
8882
+ exitDuration() {
8883
+ const durations = getComputedStyle(this.element).transitionDuration.split(",").map((s) => {
8884
+ const n = parseFloat(s);
8885
+ return s.trim().endsWith("ms") ? n : n * 1e3;
8886
+ });
8887
+ return Math.max(0, ...durations);
8888
+ }
8889
+ }
8890
+
7895
8891
  function registerMidwestControllers(application) {
7896
8892
  application.register("midwest-card", Card);
7897
8893
  application.register("midwest-banner", Banner);
@@ -7934,6 +8930,10 @@ function registerMidwestControllers(application) {
7934
8930
  application.register("midwest-panorama", Panorama);
7935
8931
  application.register("midwest-playlist", Playlist);
7936
8932
  application.register("midwest-motion", Motion);
8933
+ application.register("midwest-page-transition", PageTransition);
8934
+ application.register("midwest-map", Map$1);
8935
+ application.register("midwest-address", Address);
8936
+ application.register("midwest-popover", Popover);
7937
8937
  }
7938
8938
 
7939
8939
  export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };