PageSourceSearch

https://seventhstate.io/wp-content/plugins/stylish-cost-calculator…cc-distance-element-scripts.js?m=1781529940

js seventhstate.io collected 2026-10-04 02:39:55 UTC 19,636 bytes, 563 lines download raw bytes

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.