1/** 2 * Alpine.js component for Find Us Worldwide map functionality 3 */ 4var MY_MAPTYPE_ID = 'custom_style' 5 6var featureOpts = [ 7 { 8 featureType: 'water', 9 elementType: 'geometry', 10 stylers: [ 11 {hue: '#e4ebf1'}, 12 {saturation: -30}, 13 {lightness: 67}, 14 {visibility: 'on'} 15 ] 16 }, { 17 featureType: 'water', 18 elementType: 'labels', 19 stylers: [ 20 {hue: '#5e89ae'}, 21 {saturation: -27}, 22 {lightness: -31}, 23 {visibility: 'on'} 24 ] 25 }, { 26 featureType: 'road', 27 elementType: 'geometry', 28 stylers: [ 29 {hue: '#ffffff'}, 30 {saturation: -100}, 31 {lightness: 100}, 32 {visibility: 'on'} 33 ] 34 }, { 35 featureType: 'landscape', 36 elementType: 'geometry', 37 stylers: [ 38 {hue: '#7a9dbc'}, 39 {saturation: 8}, 40 {lightness: -32}, 41 {visibility: 'on'} 42 ] 43 }, { 44 featureType: 'poi', 45 elementType: 'geometry', 46 stylers: [ 47 {hue: '#7a9dbc'}, 48 {saturation: 33}, 49 {lightness: 20}, 50 {visibility: 'on'} 51 ] 52 }, { 53 featureType: 'administrative', 54 elementType: 'labels', 55 stylers: [ 56 {hue: '#7a9dbc'}, 57 {saturation: 33}, 58 {lightness: 20}, 59 {visibility: 'on'} 60 ] 61 }, { 62 featureType: 'road', 63 elementType: 'labels', 64 stylers: [ 65 {hue: '#5e89ae'}, 66 {saturation: -67}, 67 {lightness: -18}, 68 {visibility: 'on'} 69 ] 70 }, { 71 featureType: 'poi', 72 elementType: 'labels', 73 stylers: [ 74 {hue: '#e4ebf1'}, 75 {saturation: -26}, 76 {lightness: 20}, 77 {visibility: 'on'} 78 ] 79 }, { 80 featureType: 'administrative', 81 elementType: 'geometry.fill', 82 stylers: [ 83 {visibility: 'off'} 84 ] 85 } 86] 87 88document.addEventListener('alpine:init', () => { 89 90 Alpine.data('WorldwideLocationData', (locationsUrl) => ({ 91 // Data properties 92 locations: [], 93 selectedLocation: null, 94 selectedSupported: false, 95 infoWindows: [], 96 map: null, 97 markers: [], 98 99 // CSP build: optional chaining is unsupported in inline template expressions, 100 // so expose the selected-location fields as getters (JS itself keeps ?.). 101 get selectedUuid() { return this.selectedLocation?.uuid; }, 102 get selectedFlag() { return this.selectedLocation?.flag; }, 103 get selectedName() { return this.selectedLocation?.content?.name; }, 104 get selectedAddress() { return this.selectedLocation?.content?.address; }, 105 get selectedAddress2() { return this.selectedLocation?.content?.address2; }, 106 get selectedAddress3() { return this.selectedLocation?.content?.address3; }, 107 get selectedTown() { return this.selectedLocation?.content?.town; }, 108 get selectedContact() { return this.selectedLocation?.content?.contact; }, 109 get selectedPhone() { return this.selectedLocation?.content?.phone; }, 110 get selectedFax() { return this.selectedLocation?.content?.fax; }, 111 get selectedMail() { return this.selectedLocation?.content?.mail; }, 112 get selectedMail2() { return this.selectedLocation?.content?.mail2; }, 113 get selectedUrl() { return this.selectedLocation?.content?.url; }, 114 get selectedUrl2() { return this.selectedLocation?.content?.url2; }, 115 116 // Initialize the component 117 init() { 118 console.log('Initializing WorldwideLocationData component'); 119 // Use setTimeout to ensure DOM is ready 120 setTimeout(() => { 121 this.loadLocations(); 122 this.setupLocationClickHandlers(); 123 this.setupMap(); 124 }, 100); 125 }, 126 127 // Load locations from the API 128 async loadLocations() { 129 try { 130 const response = await fetch(locationsUrl); 131 if (!response.ok) { 132 throw new Error(`HTTP error! status: ${response.status}`); 133 } 134 const data = await response.json(); 135 this.locations = data.locations || []; 136 this.setupTypeahead(); 137 138 } catch (error) { 139 console.error('Error loading locations:', error); 140 // Fallback to global locationsForMap if available 141 if (typeof locationsForMap !== 'undefined') { 142 this.locations = locationsForMap || []; 143 } 144 } 145 }, 146 setupMap() { 147 // Wait for Google Maps API to be loaded 148 let attempts = 0; 149 const maxAttempts = 50; // 5 seconds max 150 151 const checkGoogleMaps = () => { 152 attempts++; 153 if (typeof google !== 'undefined' && google.maps) { 154 this.initializeMap(); 155 } else if (attempts < maxAttempts) { 156 setTimeout(checkGoogleMaps, 100); 157 } else { 158 console.error('Google Maps API failed to load after 5 seconds'); 159 } 160 }; 161 checkGoogleMaps(); 162 }, 163 164 // Initialize the Google Map 165 initializeMap() { 166 const mapElement = document.getElementById('map_canvas'); 167 if (!mapElement) { 168 console.error('Map canvas element not found'); 169 // Retry after a short delay 170 setTimeout(() => this.initializeMap(), 500); 171 return; 172 } 173
174 try { 175 // Check if element has dimensions 176 const rect = mapElement.getBoundingClientRect(); 177 if (rect.width === 0 || rect.height === 0) { 178 console.warn('Map canvas has no dimensions, retrying...'); 179 setTimeout(() => this.initializeMap(), 500); 180 return; 181 } 182 183 this.map = new google.maps.Map(mapElement, { 184 zoom: 2, 185 minZoom: 1, 186 disableDefaultUI: true, 187 zoomControl: true, 188 zoomControlOptions: { 189 style: google.maps.ZoomControlStyle.SMALL 190 }, 191 center: new google.maps.LatLng(30, 10), 192 mapTypeControlOptions: { 193 mapTypeIds: [google.maps.MapTypeId.ROADMAP, MY_MAPTYPE_ID] 194 }, 195 mapTypeId: MY_MAPTYPE_ID 196 }); 197 198 var markerImage = new google.maps.MarkerImage(markerImageURL, 199 new google.maps.Size(23, 31), 200 new google.maps.Point(0, 0), 201 new google.maps.Point(13, 31)) 202 203 var infowindow = new google.maps.InfoWindow() 204 205 var styledMapOptions = { 206 name: 'Custom Style' 207 } 208 209 var customMapType = new google.maps.StyledMapType(featureOpts, styledMapOptions) 210 211 this.map.mapTypes.set(MY_MAPTYPE_ID, customMapType) 212 213 locationsForMap.forEach((location) => { 214 this.setMarker(location, markerImage, template, infowindow, this.map); 215 }); 216 217 } catch (error) { 218 console.error('Error initializing map:', error); 219 } 220 }, 221 222 // Create a marker for a location 223 setMarker(location, markerImage, template, infowindow, map) { 224 if (location.showMarker) { 225 var marker = new google.maps.Marker({ 226 map: map, 227 position: new google.maps.LatLng(location.mapsLat, location.mapsLng), 228 title: location.name, 229 icon: markerImage 230 }) 231 this.markers.push(marker); 232 google.maps.event.addListener(marker, 'click', function () { 233 map.setZoom(8) 234 map.setCenter(marker.getPosition()) 235 infowindow.setContent(template(location)) 236 infowindow.open(marker.get('map'), marker) 237 }) 238 $('.aboutUsLocation a[data-uuid=' + location.uuid + ']').each(function () { 239 google.maps.event.addDomListener(this, 'click', function () { 240 google.maps.event.trigger(marker, 'click', {}) 241 }) 242 }) 243 } 244 }, 245 246 // Focus on a location on the map 247 focusOnLocation(location) { 248 if (!this.map || !location.mapsLat || !location.mapsLng) { 249 return; 250 } 251 252 try { 253 const position = {lat: parseFloat(location.mapsLat), lng: parseFloat(location.mapsLng)}; 254 this.map.setCenter(position); 255 this.map.setZoom(8); 256 257 // Find and trigger the marker 258 const marker = this.markers.find(m => 259 Math.abs(m.getPosition().lat() - position.lat) < 0.001 && 260 Math.abs(m.getPosition().lng() - position.lng) < 0.001 261 ); 262 263 if (marker) { 264 google.maps.event.trigger(marker, 'click'); 265 } 266 } catch (error) { 267 console.error('Error focusing on location:', error); 268 } 269 }, 270 271 // Generate info window content 272 generateInfoWindowContent(location) { 273 // Try to use Handlebars template if available 274 if (typeof template !== 'undefined' && template) { 275 try { 276 return template(location); 277 } catch (error) { 278 console.error('Error generating template content:', error); 279 } 280 } 281 282 // Check for global Handlebars and template element 283 if (typeof Handlebars !== 'undefined') { 284 const templateEl = document.getElementById('map-infowindow-template'); 285 if (templateEl && templateEl.innerHTML.trim()) { 286 try { 287 const compiledTemplate = Handlebars.compile(templateEl.innerHTML); 288 return compiledTemplate(location); 289 } catch (error) { 290 console.error('Error compiling Handlebars template:', error); 291 } 292 } 293 } 294 295 // Fallback content 296 let content = `<div>`; 297 if (location.flag) { 298 content += `<div class="aboutUsLocationFlag" style="background-image:url('${location.flag}'); background-repeat:no-repeat;"> </div>`; 299 } 300 content += `<p><strong>${location.content?.name || location.name}</strong></p>`; 301 if (location.content?.address) content += `<p>${location.content.address}</p>`; 302 if (location.content?.address2) content += `<p>${location.content.address2}</p>`; 303 if (location.content?.address3) content += `<p>${location.content.address3}</p>`; 304 if (location.content?.town) content += `<p>${location.content.town}</p>`; 305 if (location.content?.contact) content += `<p>${location.content.contact}</p>`; 306 if (location.content?.phone) content += `<p>Phone: ${location.content.phone}</p>`; 307 if (location.content?.fax) content += `<p>Fax: ${location.content.fax}</p>`; 308 if (location.content?.mail) content += `<p class="aboutUsLocationMail"><a href="mailto:${location.content.mail}">${location.content.mail}</a></p>`; 309 if (location.content?.mail2) content += `<p class="aboutUsLocationMail"><a href="mailto:${location.content.mail2}">${location.content.mail2}</a></p>`; 310 if (location.content?.url) content += `<p class="aboutUsLocationWebsite"><a href="https://${location.content.url}">${location.content.url}</a></p>`; 311 if (location.content?.url2) content += `<p class="aboutUsLocationWebsite"><a href="https://${location.content.url2}">${location.content.url2}</a></p>`; 312 content += `</div>`; 313 return content; 314 }, 315 316 // Close all info windows 317 closeAllInfoWindows() { 318 this.infoWindows.forEach(infoWindow => infoWindow.close()); 319 }, 320 321 // Select a location 322 selectLocation(location) { 323 if (!location) { 324 this.selectedLocation = null; 325 this.selectedSupported = false;
326 return; 327 } 328 329 // Check if this location is supported by another location 330 if (location.supportedBy) { 331 const supportingLocation = this.locations.find(loc => loc.uuid === location.supportedBy); 332 if (supportingLocation) { 333 this.selectedLocation = supportingLocation; 334 this.selectedSupported = true; 335 } else { 336 this.selectedLocation = location; 337 this.selectedSupported = false; 338 } 339 } else { 340 this.selectedLocation = location; 341 this.selectedSupported = false; 342 } 343 }, 344 345 // Setup typeahead functionality 346 setupTypeahead() { 347 // Wait for typeahead libraries to be loaded 348 const checkTypeahead = () => { 349 if (typeof Bloodhound !== 'undefined' && typeof $ !== 'undefined') { 350 this.initializeTypeahead(); 351 } else { 352 setTimeout(checkTypeahead, 100); 353 } 354 }; 355 checkTypeahead(); 356 }, 357 358 // Initialize typeahead 359 // Alpine.js version of AngularJS typeahead setup 360 initializeTypeahead() { 361 try { 362 const locationSearchEngine = new Bloodhound({ 363 datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'), 364 queryTokenizer: Bloodhound.tokenizers.whitespace, 365 limit: 5, 366 prefetch: { 367 url: locationsUrl, 368 ttl: 1800000, 369 filter: function (data) { 370 return data.locations.filter(el => el.hideInFurtherCountrySupport !== true); 371 } 372 } 373 }); 374 locationSearchEngine.initialize(); 375 376 const $searchInput = $('.typeaheadlocations'); 377 if ($searchInput.length) { 378 $searchInput.typeahead({ 379 hint: true, 380 highlight: true, 381 minLength: 1 382 }, { 383 name: 'locations', 384 source: locationSearchEngine.ttAdapter(), 385 display: 'name', 386 limit: 4, 387 templates: { 388 suggestion: Handlebars.compile($("#suggestionTemplate").html()) 389 } 390 }); 391 392 $searchInput.on('typeahead:selected', (ev, suggestion) => { 393 this.selectLocation(suggestion); 394 if (this.selectedSupported === true) { 395 this.focusOnLocation(this.selectedLocation); 396 } else { 397 this.focusOnLocation(suggestion); 398 } 399 }); 400 401 $searchInput.on('focusout', (ev) => { 402 const value = ev.target.value; 403 const location = this.locations.find(loc => loc.name.toLowerCase() === value.toLowerCase()); 404 if (location) { 405 this.selectLocation(location); 406 if (this.selectedSupported === true) { 407 this.focusOnLocation(this.selectedLocation); 408 } else { 409 this.focusOnLocation(location); 410 } 411 }}); 412 413 $searchInput.on('keypress', (ev) => { 414 if (ev.keyCode === 13) { 415 const value = ev.target.value; 416 const location = this.locations.find(loc => loc.name.toLowerCase() === value.toLowerCase()); 417 if (location) { 418 this.selectLocation(location); 419 if (this.selectedSupported === true) { 420 this.focusOnLocation(this.selectedLocation); 421 } else { 422 this.focusOnLocation(location); 423 } 424 } 425 }}); 426 427 $searchInput.parent().find(".tt-dropdown-menu").on('click', function (e) { 428 e.stopPropagation(); 429 }); 430 } 431 } catch (error) { 432 console.error('Error initializing typeahead:', error); 433 } 434 }, 435 436 // Setup location click handlers for static location list 437 setupLocationClickHandlers() { 438 document.addEventListener('click', (event) => { 439 const link = event.target.closest('a[data-uuid]'); 440 if (link) { 441 event.preventDefault(); 442 const uuid = link.getAttribute('data-uuid'); 443 const location = this.locations.find(loc => loc.uuid === uuid || loc.identifier === uuid); 444 if (location) { 445 this.selectLocation(location); 446 this.focusOnLocation(location); 447 448 // Scroll to map 449 const mapElement = document.getElementById('click-jump'); 450 if (mapElement) { 451 mapElement.scrollIntoView({behavior: 'smooth'}); 452 } 453 } 454 } 455 }); 456 }, 457 458 // Utility method to get location by UUID 459 getLocationByUuid(uuid) { 460 return this.locations.find(loc => loc.uuid === uuid || loc.identifier === uuid); 461 } 462 })); 463}); 464 465
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.