1// Google Places address autocomplete for the booking forms. 2// 3// USAGE 4// ----- 5// 1. Add an attribute to address inputs: 6// data-address-autocomplete="full" single-field (street + city + zip combined) 7// data-address-autocomplete="street" street-only; sibling fields can be: 8// data-address-component="city" 9// data-address-component="zip" 10// data-address-component="state" 11// 2. Set the Google Places API key once, before this script runs: 12// <script>window.ALPHA_GMAPS_KEY = "YOUR_KEY";</script> 13// or on the script tag itself: 14// <script src="/js/address-autocomplete.js" data-google-places-key="YOUR_KEY" defer></script> 15// 16// If no key is provided the script no-ops silently and the browser's native 17// autocomplete (`autocomplete="street-address"`) continues to work. 18// 19// The autocomplete is biased to Westchester County, NY but does not restrict 20// â out-of-area customers can still type a full address. 21 22(function () { 23 var inputs = document.querySelectorAll('[data-address-autocomplete]'); 24 if (!inputs.length) return; 25 26 function getApiKey() { 27 if (window.ALPHA_GMAPS_KEY) return window.ALPHA_GMAPS_KEY; 28 var scripts = document.querySelectorAll('script[data-google-places-key]'); 29 for (var i = 0; i < scripts.length; i++) { 30 var k = scripts[i].getAttribute('data-google-places-key'); 31 if (k) return k; 32 } 33 return null; 34 } 35 36 var apiKey = getApiKey(); 37 if (!apiKey) return; 38 39 function loadGoogleMaps(key) { 40 if (window.google && window.google.maps && window.google.maps.places) { 41 return Promise.resolve(); 42 } 43 var existing = document.getElementById('alpha-google-maps-script'); 44 if (existing) { 45 return new Promise(function (resolve, reject) { 46 existing.addEventListener('load', resolve, { once: true }); 47 existing.addEventListener('error', reject, { once: true }); 48 }); 49 } 50 return new Promise(function (resolve, reject) { 51 var s = document.createElement('script'); 52 s.id = 'alpha-google-maps-script'; 53 s.src = 'https://maps.googleapis.com/maps/api/js?key=' + encodeURIComponent(key) + '&libraries=places&loading=async&v=weekly'; 54 s.async = true; 55 s.defer = true; 56 s.onload = resolve; 57 s.onerror = reject; 58 document.head.appendChild(s); 59 }); 60 } 61 62 function findSibling(input, type) { 63 var form = input.form || input.closest('form'); 64 if (!form) return null; 65 return form.querySelector('[data-address-component="' + type + '"]'); 66 } 67 68 function setVal(el, val) { 69 if (!el || val == null) return; 70 el.value = val; 71 el.dispatchEvent(new Event('input', { bubbles: true })); 72 el.dispatchEvent(new Event('change', { bubbles: true })); 73 // If the booking widget marked this field invalid, clear that now. 74 el.removeAttribute('aria-invalid'); 75 } 76 77 function extractAddressParts(place) { 78 var out = { street_number: '', route: '', city: '', state: '', zip: '', country: '' }; 79 if (!place.address_components) return out; 80 place.address_components.forEach(function (c) { 81 var t = c.types || []; 82 if (t.indexOf('street_number') > -1) out.street_number = c.long_name; 83 else if (t.indexOf('route') > -1) out.route = c.long_name; 84 else if (t.indexOf('locality') > -1) out.city = c.long_name; 85 else if (t.indexOf('sublocality_level_1') > -1 && !out.city) out.city = c.long_name; 86 else if (t.indexOf('sublocality') > -1 && !out.city) out.city = c.long_name; 87 else if (t.indexOf('postal_town') > -1 && !out.city) out.city = c.long_name; 88 else if (t.indexOf('administrative_area_level_1') > -1) out.state = c.short_name; 89 else if (t.indexOf('postal_code') > -1) out.zip = c.long_name; 90 else if (t.indexOf('country') > -1) out.country = c.short_name; 91 }); 92 return out; 93 } 94 95 function attach(input, bounds) { 96 var mode = input.getAttribute('data-address-autocomplete'); // "full" | "street" 97 var options = { 98 componentRestrictions: { country: 'us' }, 99 fields: ['formatted_address', 'address_components'], 100 types: ['address'] 101 }; 102 if (bounds) { 103 options.bounds = bounds; 104 // Don't strictly clip â bias only. Users outside Westchester still work. 105 options.strictBounds = false;
106 } 107 108 var ac = new google.maps.places.Autocomplete(input, options); 109 110 ac.addListener('place_changed', function () { 111 var place = ac.getPlace(); 112 if (!place || !place.address_components) return; 113 var parts = extractAddressParts(place); 114 var streetLine = (parts.street_number + ' ' + parts.route).trim(); 115 116 if (mode === 'full') { 117 setVal(input, place.formatted_address || 118 [streetLine, parts.city, (parts.state + ' ' + parts.zip).trim()].filter(Boolean).join(', ')); 119 } else { 120 setVal(input, streetLine || place.formatted_address || ''); 121 setVal(findSibling(input, 'city'), parts.city); 122 setVal(findSibling(input, 'zip'), parts.zip); 123 setVal(findSibling(input, 'state'), parts.state); 124 } 125 }); 126 127 // Stop Enter from submitting the form while picking a suggestion. 128 input.addEventListener('keydown', function (e) { 129 if (e.key !== 'Enter') return; 130 var dropdown = document.querySelector('.pac-container'); 131 if (dropdown && dropdown.offsetParent !== null) e.preventDefault(); 132 }); 133 } 134 135 loadGoogleMaps(apiKey).then(function () { 136 var bounds = null; 137 if (window.google && window.google.maps && window.google.maps.LatLngBounds) { 138 bounds = new google.maps.LatLngBounds( 139 new google.maps.LatLng(40.85, -73.95), // SW corner â just south of Yonkers 140 new google.maps.LatLng(41.40, -73.45) // NE corner â just north of Peekskill 141 ); 142 } 143 Array.prototype.forEach.call(inputs, function (input) { 144 try { attach(input, bounds); } catch (_) { /* fall back to native */ } 145 }); 146 }).catch(function () { 147 // Maps SDK failed to load â leave native autocomplete in place. 148 }); 149})();
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.