1import { Controller } from "@hotwired/stimulus" 2import { loadGoogleMaps } from "lib/google_maps_loader" 3 4function findComponent(components, type) { 5 return components.find((c) => c.types?.includes(type)) 6} 7 8function readLongText(component) { 9 // Places API (New) uses longText/shortText; legacy uses long_name/short_name. 10 return component?.longText || component?.long_name 11} 12 13function readShortText(component) { 14 return component?.shortText || component?.short_name 15} 16 17export default class extends Controller { 18 static values = { apiKey: String } 19 static targets = [ 20 "widget", 21 "line1", 22 "line2", 23 "city", 24 "region", 25 "postalCode", 26 "country", 27 "formatted", 28 "placeId", 29 "latitude", 30 "longitude" 31 ] 32 33 connect() { 34 const apiKey = (this.apiKeyValue || "").trim() 35 if (!apiKey) return 36 if (!this.hasLine1Target) return 37 38 loadGoogleMaps(apiKey, ["places"]) 39 .then(() => this._initPlaces()) 40 .catch(() => { 41 // Autocomplete is progressive enhancement; keep the form usable without it. 42 }) 43 } 44 45 _initPlaces() { 46 // Clear stale place metadata if a user manually edits address fields after selecting a place. 47 ;[ 48 this.hasLine1Target ? this.line1Target : null, 49 this.hasLine2Target ? this.line2Target : null, 50 this.hasCityTarget ? this.cityTarget : null, 51 this.hasRegionTarget ? this.regionTarget : null, 52 this.hasPostalCodeTarget ? this.postalCodeTarget : null, 53 this.hasCountryTarget ? this.countryTarget : null 54 ] 55 .filter(Boolean) 56 .forEach((el) => el.addEventListener("input", () => this._clearPlaceMeta())) 57 58 // New customers should use PlaceAutocompleteElement (not legacy Autocomplete). 59 if (this.hasWidgetTarget) { 60 this._wirePlaceAutocompleteElement() 61 return 62 } 63 64 // Fallback (older integrations / views): legacy Autocomplete if available. 65 if (window.google?.maps?.places?.Autocomplete) this._wireLegacyAutocomplete() 66 } 67 68 _wireLegacyAutocomplete() { 69 this.autocomplete = new window.google.maps.places.Autocomplete(this.line1Target, { 70 fields: ["address_components", "geometry", "place_id", "formatted_address"], 71 types: ["address"] 72 }) 73 74 this.autocomplete.addListener("place_changed", () => { 75 const place = this.autocomplete?.getPlace?.() 76 if (!place) return 77 this._applyPlaceLikeResult({ 78 addressComponents: place.address_components, 79 formattedAddress: place.formatted_address, 80 placeId: place.place_id, 81 location: place.geometry?.location 82 }) 83 }) 84 } 85 86 _wirePlaceAutocompleteElement() { 87 const el = this.widgetTarget 88 89 el.addEventListener("gmp-select", async (event) => { 90 const placePrediction = event?.placePrediction || event?.detail?.placePrediction 91 if (!placePrediction?.toPlace) return 92 93 try { 94 const place = placePrediction.toPlace() 95 if (place?.fetchFields) { 96 await place.fetchFields({ fields: ["addressComponents", "formattedAddress", "location", "id"] }) 97 } 98 99 this._applyPlaceLikeResult({ 100 addressComponents: place.addressComponents, 101 formattedAddress: place.formattedAddress, 102 placeId: place.id || placePrediction.placeId, 103 location: place.location 104 }) 105 } catch { 106 // Keep the form usable even if Places fetch fails. 107 } 108 }) 109 } 110 111 _applyPlaceLikeResult({ addressComponents, formattedAddress, placeId, location }) { 112 const components = addressComponents || [] 113 114 const streetNumber = readShortText(findComponent(components, "street_number")) 115 const route = readShortText(findComponent(components, "route")) 116 const city = 117 readLongText(findComponent(components, "locality")) || 118 readLongText(findComponent(components, "postal_town")) || 119 readLongText(findComponent(components, "sublocality_level_1")) 120 const region = readShortText(findComponent(components, "administrative_area_level_1")) 121 const postalCode = readShortText(findComponent(components, "postal_code")) 122 const country = readShortText(findComponent(components, "country")) 123
124 const line1 = [streetNumber, route].filter(Boolean).join(" ").trim() 125 if (this.hasLine1Target && line1) this.line1Target.value = line1 126 127 if (this.hasCityTarget && city) this.cityTarget.value = city 128 if (this.hasRegionTarget && region) this.regionTarget.value = region 129 if (this.hasPostalCodeTarget && postalCode) this.postalCodeTarget.value = postalCode 130 if (this.hasCountryTarget && country) this.countryTarget.value = country 131 132 if (this.hasFormattedTarget) this.formattedTarget.value = formattedAddress || "" 133 if (this.hasPlaceIdTarget) this.placeIdTarget.value = placeId || "" 134 135 if (location) { 136 const lat = typeof location.lat === "function" ? location.lat() : location.lat 137 const lng = typeof location.lng === "function" ? location.lng() : location.lng 138 if (this.hasLatitudeTarget) this.latitudeTarget.value = lat == null ? "" : String(lat) 139 if (this.hasLongitudeTarget) this.longitudeTarget.value = lng == null ? "" : String(lng) 140 } else { 141 if (this.hasLatitudeTarget) this.latitudeTarget.value = "" 142 if (this.hasLongitudeTarget) this.longitudeTarget.value = "" 143 } 144 } 145 146 _clearPlaceMeta() { 147 // If the user edits the address after selecting a place, don't keep stale structured metadata around. 148 if (this.hasFormattedTarget) this.formattedTarget.value = "" 149 if (this.hasPlaceIdTarget) this.placeIdTarget.value = "" 150 if (this.hasLatitudeTarget) this.latitudeTarget.value = "" 151 if (this.hasLongitudeTarget) this.longitudeTarget.value = "" 152 } 153}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.