/**
|
* 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 = `
|
<a href="https://www.google.com/maps/search/?api=1&query=${lat},${lng}"
|
target="_blank" class="google-maps-link">
|
<span>View in Google Maps</span>
|
</a>
|
<a href="https://maps.apple.com/?ll=${lat},${lng}"
|
target="_blank" class="apple-maps-link">
|
<span>View in Apple Maps</span>
|
</a>
|
`;
|
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: `<div class="map-info-window">${config.info_content}</div>`
|
});
|
|
marker.addListener('click', () => {
|
infoWindow.open(map, marker);
|
});
|
|
if (config.auto_open !== false) {
|
setTimeout(() => infoWindow.open(map, marker), 500);
|
}
|
}
|
}
|
|
this.instances.set(containerId, { map, config });
|
return map;
|
}
|
|
/**
|
* Reverse geocode coordinates to address
|
*/
|
reverseGeocode(lat, lng, callback) {
|
const geocoder = new google.maps.Geocoder();
|
|
geocoder.geocode({ location: { lat, lng } }, (results, status) => {
|
if (status === 'OK' && results[0]) {
|
const result = {
|
formatted_address: results[0].formatted_address,
|
components: this.parseAddressComponents(results[0].address_components)
|
};
|
callback(result);
|
} else {
|
callback(null);
|
}
|
});
|
}
|
|
/**
|
* Get default map styles
|
*/
|
getMapStyles() {
|
return [
|
{ elementType: "geometry", stylers: [{ color: "#f5f5f5" }] },
|
{ elementType: "labels.text.fill", stylers: [{ color: "#616161" }] },
|
{ elementType: "labels.text.stroke", stylers: [{ color: "#f5f5f5" }] },
|
{ featureType: "road", elementType: "geometry", stylers: [{ color: "#ffffff" }] },
|
{ featureType: "water", elementType: "geometry", stylers: [{ color: "#c9c9c9" }] },
|
{ featureType: "poi.business", stylers: [{ visibility: "off" }],},
|
{ featureType: "transit", elementType: "labels.icon", stylers: [{ visibility: "off" }],}
|
];
|
}
|
|
/**
|
* Get map instance
|
*/
|
getInstance(fieldId) {
|
return this.instances.get(fieldId);
|
}
|
|
/**
|
* Destroy map instance
|
*/
|
destroyInstance(fieldId) {
|
const instance = this.instances.get(fieldId);
|
if (instance) {
|
if (instance.map) {
|
google.maps.event.clearInstanceListeners(instance.map);
|
}
|
this.instances.delete(fieldId);
|
}
|
}
|
}
|
|
// Initialize when DOM is ready
|
document.addEventListener('DOMContentLoaded', () => {
|
if (!window.jvbMaps) {
|
window.jvbMaps = new JVBGoogleMaps();
|
}
|
});
|