1/************************* 2 * 3 * Google maps API functions 4 * 5*************************/ 6let autocompleteService; 7let placesService; 8let Place; 9 10function sccloadGoogleMapsAPI( apikey ) { 11 if ( window.google && window.google.maps ) { 12 window.google.maps = undefined; 13 } 14 15 const existingScript = document.getElementById( 'scc-google-maps-script' ); 16 if ( existingScript ) { 17 existingScript.parentNode.removeChild( existingScript ); 18 } 19 20 const script = document.createElement( 'script' ); 21 script.type = 'text/javascript'; 22 script.src = `https://maps.googleapis.com/maps/api/js?key=${ apikey }&loading=async&libraries=places&callback=sccInitializeGoogleMaps`; 23 document.body.appendChild( script ); 24} 25window.sccloadGoogleMapsAPI = sccloadGoogleMapsAPI; 26 27async function sccInitializeGoogleMaps() { 28 async function waitForSccDataAndExecute() { 29 if ( typeof sccData !== 'undefined' ) { 30 try { 31 if ( typeof google !== 'undefined' && typeof google.maps !== 'undefined' && typeof google.maps.places !== 'undefined' ) { 32 // Check new Places API availability with correct parameters 33 const isNewApiAvailable = await sccCheckNewPlacesApiAvailability(); 34 35 if (isNewApiAvailable) { 36 const { Place: PlaceClass } = await google.maps.importLibrary( 'places' ); 37 Place = PlaceClass; 38 } else { 39 Place = null; 40 console.log( 'â ï¸ Falling back to legacy Places API' ); 41 } 42 43 const from_place_inputs = document.querySelectorAll( '.scc-distance-input-start' ); 44 const to_place_inputs = document.querySelectorAll( '.scc-distance-input-end' ); 45 const default_from_place_input = document.querySelectorAll( '.scc-default-from-address' ); 46 47 // Add this BEFORE creating any PlaceAutocompleteElement instances 48 if ( !window.gmpShadowPatched ) { 49 window.gmpShadowPatched = true; 50 const originalAttachShadow = Element.prototype.attachShadow; 51 52 Element.prototype.attachShadow = function( init ) { 53 if ( this.localName === 'gmp-place-autocomplete' ) { 54 // Force shadow DOM to be open 55 const shadow = originalAttachShadow.call( this, { ...init, mode: 'open' } ); 56 57 // Inject custom styles. Regular CSS won't have any effect on shadow DOM elements. 58 const style = document.createElement( 'style' ); 59 style.textContent = ` 60 .input-container .focus-ring { 61 display: none !important; 62 opacity: 0 !important; 63 } 64 65 .input-container input { 66 color: #454545; 67 } 68 69 .input-container .autocomplete-icon { 70 display: none !important; 71 opacity: 0 !important; 72 } 73 74 .input-container .clear-button { 75 display: none !important; 76 opacity: 0 !important; 77 } 78 79 .input-container { 80 border: none !important; 81 box-shadow: none !important; 82 } 83 84 .input-container:focus-within { 85 border: none !important; 86 box-shadow: none !important; 87 } 88 `; 89 shadow.appendChild( style ); 90 const calcId = document.querySelector( '.scc-distance-input' )?.getAttribute( 'data-formid' ); 91 setTimeout(() => { 92 const inputElement = shadow.querySelector( 'input' ); 93 if ( typeof sccGetTranslationByKey === 'function' ) { 94 inputElement.placeholder = sccGetTranslationByKey( calcId, 'Enter a location' ); 95 } 96 }, 0); 97 98 return shadow; 99 } 100 return originalAttachShadow.call( this, init ); 101 }; 102 } 103 104 105 from_place_inputs.forEach( async ( input ) => { 106 const calcId = input.getAttribute( 'data-formid' ); 107 108 // Create the autocomplete element 109 const { PlaceAutocompleteElement } = await google.maps.importLibrary( 'places' ); 110 111 // Create the autocomplete element 112 const autocomplete = new PlaceAutocompleteElement(); 113 autocomplete.style.outline = 'none'; 114 autocomplete.style.border = 'none'; 115 autocomplete.style.width = '130px'; 116 autocomplete.addEventListener( 'gmp-select', async ( event ) => { 117 const place = event.placePrediction.toPlace(); 118 await place.fetchFields({ 119 fields: [ 'displayName', 'formattedAddress', 'location' ] 120 }); 121 }); 122 123 const restrictCountriesStr = input.getAttribute( 'data-restrict-countries' ); 124 125 let restrictCountriesConfig = null; 126 if (restrictCountriesStr) { 127 try { 128 restrictCountriesConfig = JSON.parse( restrictCountriesStr ); 129 } catch ( e ) { 130 console.error( 'Parse error:', e ); 131 console.log( 'Failed to parse:', restrictCountriesStr ); 132 } 133 if ( restrictCountriesConfig && restrictCountriesConfig.enabled === 'true' && restrictCountriesConfig.countries?.length > 0 ) { 134 const countryList = restrictCountriesConfig.countries; 135 // Use componentRestrictions property
136 autocomplete.includedRegionCodes = countryList; 137 } 138 } 139 140 if ( input.onclick ) { 141 autocomplete.onclick = input.onclick; 142 } 143 144 Array.from( input.attributes ).forEach( attr => { 145 autocomplete.setAttribute( attr.name, attr.value ); 146 }); 147 148 setTimeout(() => { 149 if ( autocomplete.shadowRoot ) { 150 const inputElement = autocomplete.shadowRoot.querySelector( 'input' ); 151 if ( inputElement && input.value ) { 152 inputElement.value = input.value; 153 } 154 } 155 }, 100); 156 157 // Replace the input element with the autocomplete element 158 input.parentNode.replaceChild( autocomplete, input ); 159 sccGetGoogleDataInput( autocomplete, input ); // Note: both parameters might be autocomplete now*/ 160 }); 161 162 to_place_inputs.forEach( async ( input ) => { 163 const calcId = input.getAttribute( 'data-formid' ); 164 165 // Create the autocomplete element 166 const { PlaceAutocompleteElement } = await google.maps.importLibrary( 'places' ); 167 168 // Create the autocomplete element 169 const autocomplete = new PlaceAutocompleteElement(); 170 171 autocomplete.style.outline = 'none'; 172 autocomplete.style.border = 'none'; 173 autocomplete.style.width = '130px'; 174 175 // Set placeholder 176 autocomplete.placeholder = sccGetTranslationByKey( calcId, 'Enter a location' ); 177 178 autocomplete.addEventListener( 'gmp-select', async ( event ) => { 179 const place = event.placePrediction.toPlace(); 180 await place.fetchFields({ 181 fields: [ 'displayName', 'formattedAddress', 'location' ] 182 }); 183 184 }); 185 186 const restrictCountriesStr = input.getAttribute( 'data-restrict-countries' ); 187 188 let restrictCountriesConfig = null; 189 if ( restrictCountriesStr ) { 190 try { 191 restrictCountriesConfig = JSON.parse( restrictCountriesStr ); 192 } catch ( e ) { 193 console.error( 'Parse error:', e ); 194 console.log( 'Failed to parse:', restrictCountriesStr ); 195 } 196 197 if ( restrictCountriesConfig && restrictCountriesConfig.enabled === 'true' && restrictCountriesConfig.countries?.length > 0 ) { 198 const countryList = restrictCountriesConfig.countries; 199 // Use componentRestrictions property 200 autocomplete.includedRegionCodes = countryList; 201 } 202 } 203 204 if ( input.onclick ) { 205 autocomplete.onclick = input.onclick; 206 } 207 208 Array.from( input.attributes ).forEach( attr => { 209 autocomplete.setAttribute( attr.name, attr.value ); 210 }); 211 212 // Replace the input element with the autocomplete element 213 input.parentNode.replaceChild( autocomplete, input ); 214 sccGetGoogleDataInput( autocomplete, input ); // Note: both parameters might be autocomplete now*/ 215 } ); 216 217 218 if ( default_from_place_input.length > 0 ) { 219 default_from_place_input.forEach( async ( input ) => { 220 const { PlaceAutocompleteElement } = await google.maps.importLibrary( 'places' ); 221 const autocomplete = new PlaceAutocompleteElement(); 222 sccGetGoogleDataDefaultInput( autocomplete, input ); 223 } ); 224 } 225 } 226 } catch (error) { 227 console.log( 'Error initializing Google Maps:', error ); 228 } 229 } else { 230 setTimeout( waitForSccDataAndExecute, 100 ); 231 } 232 } 233 234 waitForSccDataAndExecute(); 235} 236window.sccInitializeGoogleMaps = sccInitializeGoogleMaps; 237 238function sccGetGoogleDataInput( autocomplete, input ) { 239 autocomplete.addEventListener( 'gmp-select', async ( event ) => { 240 const place = event.placePrediction.toPlace(); 241 242 // Fetch the fields you need 243 await place.fetchFields({ 244 fields: [ 'id', 'formattedAddress', 'location' ] 245 }); 246 247 const elId = input.getAttribute( 'data-elementid' ); 248 const formId = input.getAttribute( 'data-formid' ); 249 250 // Check if the place is from google suggestions 251 if ( place.id ) { 252 // Get the address and coordinates 253 const address = place.formattedAddress; 254 const coordinates = { 255 lat: place.location.lat(), 256 lng: place.location.lng(), 257 }; 258 259 // Add them to the autocomplete element as data- attributes 260 autocomplete.dataset.address = address; 261 autocomplete.dataset.coordinates = JSON.stringify( coordinates ); 262 autocomplete.dataset.validated = '1'; 263 264 const containerContext = autocomplete.closest( '.scc-distance' ); 265 const inputStart = containerContext.querySelector( '.scc-distance-input-start' ); 266 const inputEnd = containerContext.querySelector( '.scc-distance-input-end' ); 267 268 if (inputStart.getAttribute( 'data-validated' ) == '1' && inputEnd.getAttribute( 'data-validated' ) == '1' ) { 269 const start = inputStart.getAttribute( 'data-address' ); 270 const end = inputEnd.getAttribute( 'data-address' ); 271 sccCalculateDistanceOrTime( start, end, inputStart, inputEnd ).then(function( response ) { 272 if (response) { 273 inputStart.dataset.numericValue = response; 274 inputEnd.dataset.numericValue = response; 275 triggerSubmit( 12, autocomplete, elId, 0, formId ); 276 } 277 }).catch(function( error ) { 278 console.error( 'Error:', error ); 279 }); 280 } 281 } else { 282 autocomplete.dataset.validated = '0'; 283 } 284 }); 285} 286 287async function sccGetGoogleDataDefaultInput( autocomplete, input ) { 288 289 const parent = input.parentElement; 290 const target = parent.querySelector( '.scc-distance-google-input' ); 291
292 Array.from(input.attributes).forEach( attr => { 293 if ( attr.name === 'class' ) { 294 // Filter out the unwanted class 295 const classes = attr.value.split(' ').filter( cls => cls !== 'scc-default-from-address' ); 296 autocomplete.setAttribute( 'class', classes.join(' ') ); 297 } 298 }); 299 300 setTimeout(() => { 301 if (autocomplete.shadowRoot) { 302 const inputElement = autocomplete.shadowRoot.querySelector( 'input' ); 303 if ( inputElement ) { 304 inputElement.value = input.value; 305 } 306 } 307 }, 100); 308 309 if ( target ) { 310 target.parentNode.replaceChild( autocomplete, target ); 311 } 312 313 // Listen for every keystroke/change in the input 314 setTimeout( () => { 315 if ( autocomplete.shadowRoot ) { 316 const internalInput = autocomplete.shadowRoot.querySelector( 'input' ); 317 if ( internalInput ) { 318 internalInput.addEventListener( 'input', () => { 319 const value = internalInput.value; 320 321 // Check if empty 322 if ( value === '' || value.trim() === '' ) { 323 sccClearInputData( input, true ) 324 changeValue6( input.parentElement ); 325 } 326 }); 327 } 328 } 329 }, 100); 330 331 autocomplete.addEventListener( 'gmp-select', async ( event ) => { 332 const place = event.placePrediction.toPlace(); 333 // Fetch the fields you need 334 await place.fetchFields({ 335 fields: [ 'id', 'formattedAddress', 'location' ] 336 }); 337 // Check if the place is from google suggestions 338 if ( place.id ) { 339 // Get the address and coordinates 340 const address = place.formattedAddress; 341 input.value = address; 342 const coordinates = { 343 lat: place.location.lat(), 344 lng: place.location.lng(), 345 }; 346 // Add them to the input as data- attributes 347 input.dataset.address = address; 348 input.dataset.coordinates = JSON.stringify( coordinates ); 349 input.dataset.validated = '1'; 350 if ( input.getAttribute( 'data-validated' ) == '1' ) { 351 changeValue6( input.parentElement ); 352 } 353 } else { 354 input.dataset.validated = '0'; 355 if ( input.value == '' ) { 356 changeValue6( input.parentElement ); 357 } 358 } 359 } ); 360} 361 362function sccCopyDistanceAddress( input1Id, input2Id ) { 363 const input1 = document.getElementById( input1Id ); 364 const input2 = document.getElementById( input2Id ); 365 // trigger a fake input event 366 const inputEvent = new Event( 'input', { 367 bubbles: true, 368 cancelable: true, 369 } ); 370 const changeEvent = new Event( 'change', { 371 bubbles: true, 372 cancelable: true, 373 } ); 374 input1.addEventListener( 'input', function() { 375 const address = input1.value; 376 input2.value = address; 377 } ); 378 379 input1.addEventListener( 'change', function() { 380 setTimeout( function() { 381 const address = input1.value; 382 input2.value = address; 383 input2.dispatchEvent( inputEvent ); 384 input2.dispatchEvent( changeEvent ); 385 if ( input1.hasAttribute( 'data-address' ) ) { 386 input2.setAttribute( 'data-address', input1.getAttribute( 'data-address' ) ); 387 } 388 if ( input1.hasAttribute( 'data-coordinates' ) ) { 389 input2.setAttribute( 'data-coordinates', input1.getAttribute( 'data-coordinates' ) ); 390 } 391 if ( input1.hasAttribute( 'data-validated' ) ) { 392 input2.setAttribute( 'data-validated', input1.getAttribute( 'data-validated' ) ); 393 } 394 }, 1000 ); 395 if ( input2.classList.contains( 'scc-distance-input-end' ) ) { 396 input2.addEventListener( 'change', function() { 397 google.maps.event.trigger( input2, 'focus', {} ); 398 } ); 399 } 400 } ); 401} 402window.sccCopyDistanceAddress = sccCopyDistanceAddress; 403 404function sccClearInputData( input, clearTriggerSubmit = true ) { 405 // Clean the input value 406 input.value = ''; 407 const elId = input.getAttribute( 'data-elementid' ); 408 const formId = input.getAttribute( 'data-formid' ); 409 // Remove data- attributes 410 input.setAttribute( 'data-address', '' ); 411 input.setAttribute( 'data-coordinates', '' ); 412 input.setAttribute( 'data-validated', '' ); 413 input.setAttribute( 'data-numeric-value', '' ); 414 input.setAttribute( 'data-distance', '' ); 415} 416 417function sccCalculateDistanceOrTime( start, end, inputStart, inputEnd ) { // 418 const unitSystem = inputStart.getAttribute( 'data-distance-units' ) ? inputStart.getAttribute( 'data-distance-units' ) : 'km'; 419 return new Promise( function( resolve, reject ) { 420 let unitSystemAPI; 421 let totalDistanceOrTime = 0; 422 const minDistanceValueAttribute = inputEnd.getAttribute( 'data-min-distance-value' ); 423 const maxDistanceValueAttribute = inputEnd.getAttribute( 'data-max-distance-value' ); 424 const roundTripAttribute = inputEnd.getAttribute( 'data-round-trip' ); 425 426 const minDistanceValue = parseFloat( minDistanceValueAttribute ); 427 const maxDistanceValue = parseFloat( maxDistanceValueAttribute ); 428 const roundTrip = parseInt( roundTripAttribute ); 429 430 if ( unitSystem == 'km' ) { 431 unitSystemAPI = google.maps.UnitSystem.METRIC; 432 } else { 433 unitSystemAPI = google.maps.UnitSystem.IMPERIAL; 434 } 435 const service = new google.maps.DistanceMatrixService(); 436 service.getDistanceMatrix( 437 { 438 origins: [ start ], 439 destinations: [ end ], 440 travelMode: 'DRIVING', // Can also use 'BICYCLING', 'TRANSIT', 'WALKING' 441 unitSystem: unitSystemAPI, // Can also use UnitSystem.IMPERIAL for miles 442 }, 443 callback, 444 ); 445 446 function showErrorMessage( status = false, className, childValue = { className: undefined, text: undefined } ) { 447 const distanceContainer = inputStart.closest( '.scc-distance' ); 448 let nextSibling = distanceContainer.nextElementSibling; 449 450 while ( nextSibling ) { 451 if ( nextSibling.classList.contains( className ) ) { 452 if ( status ) { 453 nextSibling.classList.remove( 'scc-hidden' ); 454 if ( childValue.className ) { 455 const childElement = nextSibling.querySelector( childValue.className ); 456 childElement.innerHTML = childValue.text; 457 } 458 } else { 459 nextSibling.classList.add( 'scc-hidden' ); 460 } 461 break; 462 } 463 nextSibling = nextSibling.nextElementSibling; 464 } 465 } 466 467 showErrorMessage( false, 'scc-distance-or-time-out-of-limit-warning' ); 468 469 function callback( response, status ) { 470 if ( status == 'OK' ) { 471 const origins = response.originAddresses; 472 473 for ( let i = 0; i < origins.length; i++ ) { 474 const results = response.rows[ i ].elements; 475 for ( let j = 0; j < results.length; j++ ) { 476 const element = results[ j ]; 477 478 if ( element.status === 'ZERO_RESULTS' ) { 479 reject( showErrorMessage( true, 'scc-no-route-alert' ) ); 480 return; 481 } 482 showErrorMessage( false, 'scc-no-route-alert' ); 483 484 const distanceValue = element.distance.value; 485 486 if ( unitSystem === 'km' ) { 487 let valueInKilometers = distanceValue / 1000; 488 valueInKilometers = valueInKilometers.toFixed( 2 ); 489 totalDistanceOrTime = valueInKilometers; 490 } 491 if ( unitSystem === 'mi' ) { 492 let valueInMiles = distanceValue * 0.000621371; 493 valueInMiles = valueInMiles.toFixed( 2 ); 494 totalDistanceOrTime = valueInMiles; 495 } 496 if ( unitSystem === 'min' ) { 497 const valueInMinutes = element.duration.value / 60; 498 totalDistanceOrTime = valueInMinutes; 499 } 500 501 if ( roundTrip === 1 ) { 502 totalDistanceOrTime = totalDistanceOrTime * 2; 503 } 504 505 if ( minDistanceValue && minDistanceValue > totalDistanceOrTime ) { 506 const warningMessage = `The entered distance is below the minimum allowed. Please enter a value greater than or equal to ${ minDistanceValue }${ unitSystem } !`; 507 showErrorMessage( true, 'scc-distance-or-time-out-of-limit-warning', { className: '.scc-distance-or-time-out-of-limit-text', text: warningMessage } ); 508 } 509 510 if ( maxDistanceValue && maxDistanceValue < totalDistanceOrTime ) { 511 const warningMessage = `The entered value exceeds the maximum allowed. Please enter a value less than or equal to ${ maxDistanceValue }${ unitSystem } !`; 512 showErrorMessage( true, 'scc-distance-or-time-out-of-limit-warning', { className: '.scc-distance-or-time-out-of-limit-text', text: warningMessage } ); 513 } 514 515 response = totalDistanceOrTime; 516 resolve( response ); 517 } 518 } 519 } 520 }//end callback 521 } ); 522} 523window.sccCalculateDistanceOrTime = sccCalculateDistanceOrTime; 524 525/** 526 * Check if the new Google Places API is available and enabled 527 */ 528async function sccCheckNewPlacesApiAvailability() { 529 try { 530 // Import the new Places library 531 const { Place } = await google.maps.importLibrary("places"); 532 533 if (!Place || typeof Place.searchByText !== 'function') { 534 return false;
535 } 536 537 // Make a minimal test request with correct parameters 538 await Place.searchByText({ 539 textQuery: "test", // Required: search text 540 fields: ['id'], // Required: field mask 541 maxResultCount: 1 // Optional: limit results 542 }); 543 544 return true; 545 546 } catch (error) { 547 // Log specific error types for debugging 548 if (error.message) { 549 if (error.message.includes('API key')) { 550 console.warn('New Places API: Invalid or missing API key'); 551 } else if (error.message.includes('billing')) { 552 console.warn('New Places API: Billing account not set up'); 553 } else if (error.message.includes('quota')) { 554 console.warn('New Places API: Quota exceeded'); 555 } else if (error.message.includes('INVALID_REQUEST')) { 556 console.warn('New Places API: Invalid request parameters'); 557 } else { 558 console.warn('New Places API error:', error.message); 559 } 560 } 561 return false; 562 } 563}
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.