1import mapSettings from "../scripts/config-map.js"; 2 3/** 4 * Pan + zoom the map to a location, bounce its marker, and open its info window 5 * (which holds the location CTAs). Shared by the built-in selector and by any 6 * custom location UI (e.g. the Vision Source accordion) via window.royaMapFocus. 7 * @param {google.maps.Map} map 8 * @param {Array} markers 9 * @param {Array<google.maps.InfoWindow>} infoWindows 10 * @param {number} index - index of the location to focus 11 */ 12export function focusLocation(map, markers, infoWindows, index) { 13 if (!map || !markers || !markers[index]) return; 14 const marker = markers[index]; 15 const pos = marker.getPosition ? marker.getPosition() : marker.position; 16 // Newest click wins, so rapid clicks don't fight over the map. 17 const token = (window.__royaMapFocusToken = (window.__royaMapFocusToken || 0) + 1); 18 const isCurrent = () => window.__royaMapFocusToken === token; 19 20 // The info window is optional â some maps (e.g. the contact-page mini map) focus a 21 // location without opening any info box, just centering it. 22 const infoWindow = infoWindows && infoWindows[index]; 23 if (infoWindows) infoWindows.forEach((iw) => iw && iw.close()); 24 25 const center = () => { 26 // Vector (cloud-styled) maps: moveCamera drives the WebGL camera directly and 27 // repaints, where setZoom/setCenter can leave the first zoom-in unrendered. 28 if (typeof map.moveCamera === "function") { 29 map.moveCamera({ center: pos, zoom: 16 }); 30 } else { 31 map.setZoom(16); 32 map.setCenter(pos); 33 } 34 }; 35 36 if (marker.setAnimation) { 37 marker.setAnimation(window.google.maps.Animation.BOUNCE); 38 setTimeout(() => { 39 marker.setAnimation(null); 40 }, 1500); 41 } else if (marker.content) { 42 marker.content.animate( 43 [{ transform: "translateY(0)" }, { transform: "translateY(-20px)" }, { transform: "translateY(0)" }], 44 { duration: 700, iterations: 3, easing: "cubic-bezier(0.215, 0.610, 0.355, 1.000)" } 45 ); 46 } 47 48 let opened = false; 49 const open = () => { 50 if (opened || !isCurrent()) return; 51 opened = true; 52 if (infoWindow) infoWindow.open({ map, anchor: marker }); 53 }; 54 55 // On the very first interaction after a page load the vector map's camera/projection can 56 // still be settling, so a single move lands off-target â the marker isn't centered and 57 // the info window opens off to the side (a real mouse drag wakes it up, and a second 58 // click works, because the map has settled by then). Re-assert the size and camera 59 // repeatedly as it settles â actively forcing it to render â then open the info window 60 // once it has caught up. Re-asserting continues briefly after opening, and the info 61 // window (anchored to the marker) follows, so it ends up centered regardless. 62 const settle = () => { 63 if (!isCurrent()) return; 64 window.google.maps.event.trigger(map, "resize"); 65 center(); 66 }; 67 center(); 68 [100, 300, 600, 1000, 1500, 2000].forEach((t) => setTimeout(settle, t)); 69 window.google.maps.event.addListenerOnce(map, "idle", () => setTimeout(open, 700)); 70 setTimeout(open, 2300); // overall safety net 71} 72 73/** 74 * Adds a location selector dropdown to the map. 75 * @param {google.maps.Map} map - The map instance. 76 * @param {Array} locations - Array of valid location objects. 77 * @param {Array<google.maps.Marker|google.maps.marker.AdvancedMarkerElement>} markers - Array of map markers. 78 * @param {Array<google.maps.InfoWindow>} infoWindows - Array of info windows. 79 */ 80export function addLocationSelectorControl(map, locations, markers, infoWindows) { 81 const controlDiv = document.createElement("div"); 82 controlDiv.className = "relative bg-primary/80 backdrop-blur-xs shadow-md p-2 m-4 xs:w-[280px] rounded max-md:!relative"; 83 84 const controlUI = document.createElement("div"); 85 controlUI.className = "flex flex-col"; 86 87 const headerDiv = document.createElement("div"); 88 headerDiv.className = "flex justify-between items-center mb-2 px-2"; 89 90 const title = document.createElement("div"); 91 title.textContent = "Our Locations"; 92 title.className = "text-primary-foreground font-semibold text-sm"; 93 headerDiv.appendChild(title); 94 95 const toggleButton = document.createElement("button"); 96 toggleButton.textContent = "HIDE"; 97 toggleButton.className = "text-xs font-bold text-accent hover:text-accent/80 transition-colors"; 98 toggleButton.setAttribute("type", "button"); 99 headerDiv.appendChild(toggleButton); 100 101 controlUI.appendChild(headerDiv); 102 103 const locationList = document.createElement("div"); 104 locationList.id = "map-location-list"; 105 locationList.className = "location-list max-h-60 overflow-y-auto"; 106 107 locations.forEach((location, index) => { 108 const locationButton = document.createElement("button"); 109 locationButton.className = "group w-full text-left px-3 py-2 text-sm hover:bg-secondary hover:text-secondary-foreground focus:outline-none focus:ring-1 focus:ring-inset focus:ring-accent transition-colors rounded my-1"; 110 locationButton.innerHTML = ` 111 <div class="flex items-center">
112 <span class="rounded-full bg-primary group-hover:bg-accent group-hover:text-accent-foreground text-primary-foreground h-6 w-6 flex items-center justify-center mr-2 text-xs font-bold">${index + 1}</span> 113 <div> 114 <span class="text-primary-foreground group-hover:text-secondary-foreground font-medium block">${location.locationData.name || `Location ${index + 1}`}</span> 115 <span class="text-xs text-primary-foreground/80 group-hover:text-secondary-foreground/80 block">${location.locationData.addressCity || ""}, ${location.locationData.addressState || ""}</span> 116 </div> 117 </div> 118 `; 119 120 locationButton.addEventListener("click", () => { 121 focusLocation(map, markers, infoWindows, index); 122 locationList.style.display = "none"; 123 resetButton.style.display = "none"; 124 toggleButton.textContent = "SHOW"; 125 }); 126 127 locationList.appendChild(locationButton); 128 }); 129 130 controlUI.appendChild(locationList); 131 controlDiv.appendChild(controlUI); 132 133 const resetButton = document.createElement("button"); 134 resetButton.className = "btn btn-wide btn-md btn-rounded btn-accent mt-2"; 135 resetButton.textContent = "Show All Locations"; 136 resetButton.addEventListener("click", () => { 137 infoWindows.forEach((iw) => iw.close()); 138 const bounds = new window.google.maps.LatLngBounds(); 139 markers.forEach((marker) => { 140 const position = marker.getPosition ? marker.getPosition() : marker.position; 141 bounds.extend(position); 142 }); 143 map.fitBounds(bounds); 144 window.google.maps.event.addListenerOnce(map, "idle", () => { 145 if (map.getZoom() > 15) map.setZoom(15); 146 }); 147 }); 148 149 controlUI.appendChild(resetButton); 150 151 toggleButton.addEventListener("click", () => { 152 if (locationList.style.display === "none") { 153 locationList.style.display = "block"; 154 resetButton.style.display = "block"; 155 toggleButton.textContent = "HIDE"; 156 } else { 157 locationList.style.display = "none"; 158 resetButton.style.display = "none"; 159 toggleButton.textContent = "SHOW"; 160 } 161 }); 162 163 map.controls[window.google.maps.ControlPosition.LEFT_TOP].push(controlDiv); 164} 165 166/** 167 * Adds a style switcher control to the map. 168 * @param {google.maps.Map} map - The map instance. 169 * @param {Function} setMapStyleCallback - Callback function to set the map style. 170 */ 171export function addStyleSwitcherControl(map, setMapStyleCallback) { 172 const controlDiv = document.createElement("div"); 173 controlDiv.className = "bg-white shadow-md rounded-lg p-2 m-2"; 174 controlDiv.style.margin = "100px"; 175 176 const select = document.createElement("select"); 177 select.className = "text-sm px-2 py-1 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"; 178 179 mapSettings.availableStyles.forEach((style) => { 180 const option = document.createElement("option"); 181 option.value = style.id; 182 option.textContent = style.name; 183 option.selected = style.id === mapSettings.currentStyle; 184 select.appendChild(option); 185 }); 186 187 select.addEventListener("change", () => { 188 setMapStyleCallback(select.value); 189 }); 190 191 controlDiv.appendChild(select); 192 map.controls[window.google.maps.ControlPosition.TOP_RIGHT].push(controlDiv); 193}
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.