/** * Google Maps Frontend Integration * Handles all client-side Google Maps functionality */ class JVBGoogleMaps { constructor() { this.instances = new Map(); this.defaults = null; this.ready = false; if (typeof google !== 'undefined' && google.maps) { this.init(); } else { document.addEventListener('googleMapsReady', () => this.init()); } } init() { this.ready = true; this.defaults = window.jvbMapDefaults || { lat: 53.5461, lng: -113.4938, zoom: 14, }; document.querySelectorAll('[data-location-field-init]').forEach(field => { try { const jsonString = field.dataset.locationFieldInit; // Check if the data attribute exists and is not empty if (!jsonString || jsonString.trim() === '') { console.warn('Empty location field init data for field:', field); return; } // Attempt to parse the JSON const config = JSON.parse(jsonString); // Validate required properties if (!config.fieldId) { console.error('Missing fieldId in location field config'); return; } this.createLocationField(config); } catch (error) { console.error('Failed to parse location field config:', error); console.error('Data attribute value:', field.dataset.locationFieldInit); console.error('Field element:', field); } }); } /** * Create a location field with autocomplete and map */ createLocationField(config) { if (!this.ready) { console.warn('Google Maps not ready yet'); setTimeout(() => this.createLocationField(config), 100); return null; } const { fieldId, initialCoords, onLocationSelected } = config; // Get elements using data attribute const fieldContainer = document.querySelector(`[data-field="${fieldId}"]`); if (!fieldContainer) { console.error('Field container not found:', fieldId); return null; } const mapContainer = fieldContainer.querySelector('.location-map'); if ( !mapContainer) { console.error('Required elements not found in field:', fieldId); return null; } // Initialize map const mapOptions = { center: initialCoords || this.defaults, zoom: this.defaults.zoom, mapId: this.defaults.mapId, styles: this.getMapStyles(), disableDefaultUI: true, }; const map = new google.maps.Map(mapContainer, mapOptions); // Initialize marker if coordinates exist let marker = null; if (initialCoords && initialCoords.lat && initialCoords.lng) { marker = this.createMarker(map, initialCoords); } // Setup Places Autocomplete this.setupAutocomplete(fieldContainer, map, marker, onLocationSelected); // Store instance const instance = { map, marker, fieldContainer, config }; this.instances.set(fieldId, instance); return instance; } /** * Setup Places Autocomplete using the new API */ setupAutocomplete(fieldContainer, map, marker, onLocationSelected) { console.log('Setting up autocomplete'); const autocomplete = new google.maps.places.PlaceAutocompleteElement({ includedRegionCodes: ['ca'] }); let wrapper = fieldContainer.querySelector('.autocomplete-wrapper'); wrapper.append(autocomplete); let savedInput = fieldContainer.querySelector('[name="current_location[street]"]'); if (savedInput && savedInput.value !== '') { console.log('Saved value: ', savedInput.value); autocomplete.value = savedInput.value; } autocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { const place = placePrediction.toPlace(); await place.fetchFields({ fields: ['displayName', 'addressComponents', 'formattedAddress', 'location'] }); console.log('Display Name:',place.displayName); console.log('Formatted Address:',place.formattedAddress); console.log('Address Components:',place.addressComponents); console.log('Location:',place.location); // If the place has a geometry, then present it on a map. if (place.viewport) { map.fitBounds(place.viewport); } else { map.setCenter(place.location); map.setZoom(17); } const location = { lat: place.location.lat(), lng: place.location.lng(), address: place.displayName || place.formattedAddress || '', components: this.parseAddressComponents(place.addressComponents) }; console.log('Grabbed Location: ',location); // Update map and marker map.setCenter(place.location); if (marker) { marker.position = place.location; } else { marker = this.createMarker(map, place.location); } console.log('Updating field inputs'); // Update all hidden inputs this.updateFieldInputs(fieldContainer, location); // Update map links this.updateMapLinks(fieldContainer, location.lat, location.lng); // Call callback if (onLocationSelected) { onLocationSelected(location); } fieldContainer.dispatchEvent(new Event('change', {bubbles: true})); }); // Handle marker drag if (marker) { marker.addListener('dragend', (event) => { const newPos = { lat: event.latLng.lat(), lng: event.latLng.lng() }; this.reverseGeocode(newPos.lat, newPos.lng, (result) => { if (result) { const location = { ...newPos, address: result.formatted_address, components: result.components }; autocomplete.value = location.address; this.updateFieldInputs(fieldContainer, location); this.updateMapLinks(fieldContainer, location.lat, location.lng); if (onLocationSelected) { onLocationSelected(location); } } }); }); } return autocomplete; } /** * Create marker using Advanced Markers API */ createMarker(map, position) { if (google.maps.marker && google.maps.marker.AdvancedMarkerElement) { return new google.maps.marker.AdvancedMarkerElement({ map: map, position: position, gmpDraggable: true }); } // Fallback to regular marker return new google.maps.Marker({ position: position, map: map, draggable: true }); } /** * Parse address components from Places API */ parseAddressComponents(components) { const result = { street: '', city: '', province: '', country: '', postal_code: '' }; if (!Array.isArray(components)) return result; let streetNumber = ''; let route = ''; console.log('Attempting to parse address components...'); components.forEach(component => { const types = component.types || []; console.log(component); console.log(types); const text = component.longText || component.long_name || ''; const shortText = component.shortText || component.short_name || ''; if (types.includes('street_number')) streetNumber = text; if (types.includes('route')) route = text; if (types.includes('locality')) result.city = text; if (types.includes('administrative_area_level_1')) result.province = shortText; if (types.includes('country')) result.country = shortText; if (types.includes('postal_code')) result.postal_code = text; }); result.street = `${streetNumber} ${route}`.trim(); console.log('Final result: ', result); return result; } /** * Update hidden form inputs within the field container */ updateFieldInputs(fieldContainer, location) { // Update main fields const addressInput = fieldContainer.querySelector('[data-location-field="address"]'); const latInput = fieldContainer.querySelector('[data-location-field="lat"]'); const lngInput = fieldContainer.querySelector('[data-location-field="lng"]'); console.log('Address Input:', addressInput); console.log('Latitude Input:', latInput); console.log('lngInput Input:', lngInput); if (addressInput) addressInput.value = location.address || ''; if (latInput) latInput.value = location.lat || ''; if (lngInput) lngInput.value = location.lng || ''; console.log('Components: ', location.components); // Update component fields if (location.components) { Object.keys(location.components).forEach(key => { const input = fieldContainer.querySelector(`[data-location-field="${key}"]`); console.log('Component input: ', input); if (input) { input.value = location.components[key] || ''; } }); } // Trigger change events fieldContainer.querySelectorAll('input[type="hidden"]').forEach(input => { input.dispatchEvent(new Event('change', { bubbles: true })); }); } /** * Update map links */ updateMapLinks(fieldContainer, lat, lng) { const linksContainer = fieldContainer.querySelector('.location-links'); if (!linksContainer) { // Create links container if it doesn't exist const preview = fieldContainer.querySelector('.location-preview'); if (preview) { const newLinks = document.createElement('div'); newLinks.className = 'location-links'; newLinks.innerHTML = ` View in Google Maps View in Apple Maps `; preview.appendChild(newLinks); } } else { const googleLink = linksContainer.querySelector('.google-maps-link'); const appleLink = linksContainer.querySelector('.apple-maps-link'); if (googleLink) { googleLink.href = `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`; } if (appleLink) { appleLink.href = `https://maps.apple.com/?ll=${lat},${lng}`; } } } /** * Create a display-only map */ createDisplayMap(containerId, config) { if (!this.ready) return null; const container = document.getElementById(containerId); if (!container) return null; const mapOptions = { center: { lat: config.lat, lng: config.lng }, zoom: config.zoom || this.defaults.zoom, mapId: this.defaults.mapId, styles: this.getMapStyles(), disableDefaultUI: !config.interactive, gestureHandling: config.interactive ? 'auto' : 'none' }; const map = new google.maps.Map(container, mapOptions); if (config.show_marker) { const marker = this.createMarker(map, { lat: config.lat, lng: config.lng }); if (config.show_info_window && config.info_content) { const infoWindow = new google.maps.InfoWindow({ content: `