1import { 2 getCookieValue, 3 setCookie, 4 removeCookie, 5 getClosestStore, 6 onStoreChanged, 7 dispatchStoreChanged, 8} from "./utils.js"; 9 10const GOOGLE_MAPS_BASE_URL = 11 "https://www.google.com/maps/dir/?api=1&destination="; 12 13/** 14 * Displays a loading state for all dynamic header elements. 15 */ 16function setSkeletonLoading() { 17 const elementsWithDataAttr = document.querySelectorAll("[data-dlh]"); 18 elementsWithDataAttr.forEach((element) => { 19 element.setAttribute("data-dlh-loading", "true"); 20 element.textContent = ""; 21 }); 22} 23 24/** 25 * Updates the header store display with the selected store data. 26 * Optionally sets a cookie to persist the selected store. 27 * Also updates all dynamic [data-dlh] elements and block links. 28 * 29 * @param {number} storePostId - The ID of the selected store. 30 * @param {boolean} shouldSave - Whether to save the selection in a cookie. 31 */ 32function updateHeader(storePostId, shouldSave = false) { 33 // Show loading placeholder in target elements 34 setSkeletonLoading(); 35 36 // Find the selected store in the global store list 37 const selectedStore = KSAL_DATA.stores.find( 38 (store) => store.post_id == storePostId 39 ); 40 if (!selectedStore) return; 41 42 // Optionally save the selected store ID in a cookie (7-day expiry) 43 if (shouldSave) { 44 setCookie("dlh_store_id", storePostId, 7); 45 } 46 47 /** 48 * Updates the text content of all elements with the matching data-dlh selector. 49 * Preserves optional 'before' and 'after' prefix/suffix defined via data attributes. 50 * 51 * @param {string} selector - The `data-dlh` key (e.g., 'dlh-store-name'). 52 * @param {string} value - The value to display inside the target elements. 53 */ 54 const updateElementText = (selector, value) => { 55 const elements = document.querySelectorAll(`[data-dlh='${selector}']`); 56 elements.forEach((element) => { 57 const before = element.dataset.before || ""; 58 const after = element.dataset.after || ""; 59 if (value != null) { 60 element.textContent = `${before}${value}${after}`; 61 } 62 element.removeAttribute("data-dlh-loading"); 63 }); 64 }; 65 66 // Map of store fields to their respective data-dlh attribute keys 67 const fieldMap = { 68 "store-name": selectedStore.label, 69 address: selectedStore.address, 70 city: selectedStore.city, 71 state: selectedStore.state, 72 zip: selectedStore.zip, 73 phone: selectedStore.phone, 74 }; 75 76 // Update each dynamic store field in the DOM 77 Object.entries(fieldMap).forEach(([field, value]) => { 78 updateElementText(`dlh-${field}`, value); 79 }); 80 81 // Update any links that depend on the store context 82 updateBlockLinks(selectedStore); 83 84 // Dispatch custom event to notify listeners that the store changed 85 dispatchStoreChanged("header", storePostId); 86} 87 88/** 89 * Updates <a> and Kadence icon links with correct store links. 90 */ 91function updateBlockLinks(store) { 92 const linkBlocks = document.querySelectorAll(".dlh-parent-link"); 93 94 linkBlocks.forEach((linkElement) => { 95 const linkType = linkElement.dataset.linkType; 96 if (!linkType) return; 97 98 let href = ""; 99 let target = "_self"; 100 101 switch (linkType) { 102 case "single": 103 href = store.permalink || ""; 104 break; 105 case "directions": 106 const destination = [store.address, store.city, store.state] 107 .filter(Boolean) 108 .join(", "); 109 href = `${GOOGLE_MAPS_BASE_URL}${encodeURIComponent(destination)}`; 110 target = "_blank"; 111 break; 112 case "phone": 113 const cleanedPhone = store.phone 114 ? store.phone.replace(/[^0-9+]/g, "") 115 : ""; 116 href = `tel:${cleanedPhone}`; 117 break; 118 } 119 120 if (href) { 121 linkElement.href = href; 122 linkElement.target = target; 123 } 124 }); 125 126 const iconBlocks = document.querySelectorAll(".wp-block-kadence-icon"); 127 128 iconBlocks.forEach((iconBlock) => { 129 const iconClass = Array.from(iconBlock.classList).find((cls) => 130 cls.startsWith("dlh-icon-link-") 131 ); 132 if (!iconClass) return; 133 134 const iconType = iconClass.replace("dlh-icon-link-", ""); 135 const iconInner = iconBlock.querySelector(".wp-block-kadence-single-icon"); 136 if (!iconInner) return; 137 138 // Unwrap <a> if already wrapped 139 if (iconInner.parentElement.tagName.toLowerCase() === "a") { 140 const anchorWrapper = iconInner.parentElement; 141 anchorWrapper.replaceWith(iconInner); 142 } 143 144 let href = ""; 145 let target = "_self"; 146 147 switch (iconType) { 148 case "single": 149 href = store.permalink; 150 break; 151 case "directions": 152 const destination = `${store.address}, ${store.city}, ${store.state}`; 153 href = `${GOOGLE_MAPS_BASE_URL}${encodeURIComponent(destination)}`; 154 target = "_blank"; 155 break; 156 case "phone": 157 const cleanedPhone = store.phone.replace(/[^0-9+]/g, ""); 158 href = `tel:${cleanedPhone}`; 159 break; 160 } 161 162 if (href) { 163 const anchor = document.createElement("a"); 164 anchor.href = href; 165 anchor.target = target; 166 anchor.appendChild(iconInner); 167 iconBlock.appendChild(anchor); 168 } 169 }); 170} 171 172/** 173 * Fetches IP-based geolocation from the server via MaxMind DB. 174 */ 175function fetchLocationFromServer() { 176 return fetch("/wp-json/dlh/v1/ip-location") 177 .then((res) => (res.ok ? res.json() : null)) 178 .catch(() => null); 179} 180 181/** 182 * Uses browser geolocation (GPS) to update to closest store. 183 */ 184function handleGeolocationClick() { 185 navigator.geolocation.getCurrentPosition( 186 (position) => { 187 const nearest = getClosestStore( 188 position.coords.latitude, 189 position.coords.longitude, 190 KSAL_DATA.stores 191 ); 192 if (nearest) { 193 removeCookie("dlh_store_id"); 194 updateHeader(nearest.post_id, true); 195 } 196 }, 197 () => {
198 // Do nothing if permission is denied or geolocation fails 199 console.warn("User denied geolocation or it failed."); 200 }, 201 { enableHighAccuracy: true, timeout: 10000 } 202 ); 203} 204 205// Init: choose store from cookie or fallback to IP location 206document.addEventListener("DOMContentLoaded", async () => { 207 // Check if Closest Store Locator block is present 208 const hasClosestStoreBlock = document.querySelector(".dlh-parent-link"); 209 if (!hasClosestStoreBlock) { 210 return; 211 } 212 213 // Always bind the precise location button 214 const preciseBtn = document.getElementById("geolocation-btn"); 215 if (preciseBtn) { 216 preciseBtn.addEventListener("click", (e) => { 217 e.preventDefault(); 218 handleGeolocationClick(); 219 }); 220 221 // Accessibility: If not a real button, add role, tabindex, and keyboard support 222 if (preciseBtn.tagName !== "BUTTON") { 223 preciseBtn.setAttribute("role", "button"); 224 preciseBtn.setAttribute("tabindex", "0"); 225 preciseBtn.addEventListener("keydown", (e) => { 226 if (e.key === "Enter" || e.key === " ") { 227 e.preventDefault(); 228 handleGeolocationClick(); 229 } 230 }); 231 } 232 } 233 // Check cookie first 234 const savedId = getCookieValue("dlh_store_id"); 235 if (savedId) { 236 updateHeader(savedId, false); 237 return; 238 } 239 // No cookie: try IP-based geolocation 240 const geoData = await fetchLocationFromServer(); 241 if (geoData?.lat && geoData?.lon) { 242 const nearest = getClosestStore(geoData.lat, geoData.lon, KSAL_DATA.stores); 243 if (nearest) { 244 updateHeader(nearest.post_id, true); 245 return; 246 } 247 } 248 // Use fallback values if available 249 if (KSAL_DATA.fallbackText && KSAL_DATA.fallbackUrl) { 250 const parentLink = document.querySelector(".dlh-parent-link"); 251 252 if (parentLink) { 253 // Clear any existing content 254 parentLink.innerHTML = ""; 255 256 // Set proper href and attributes 257 parentLink.href = KSAL_DATA.fallbackUrl; 258 parentLink.setAttribute("data-link-type", "single"); 259 parentLink.setAttribute( 260 "data-link-style", 261 KSAL_DATA.defaultStyle || "none" 262 ); 263 parentLink.target = "_self"; 264 parentLink.rel = "noopener noreferrer nofollow"; 265 266 // Create wrapper div and span 267 const wrapper = document.createElement("div"); 268 wrapper.className = `dlh-inline-link dlh-style-${ 269 KSAL_DATA.defaultStyle || "none" 270 }`; 271 272 const span = document.createElement("span"); 273 const NORMAL_FONT_WEIGHT = "400"; 274 span.className = "wp-block-dynamic-location-dlh-address"; 275 span.dataset.dlh = "dlh-address"; 276 span.style.fontWeight = NORMAL_FONT_WEIGHT; 277 span.textContent = KSAL_DATA.fallbackText; 278 279 wrapper.appendChild(span); 280 parentLink.appendChild(wrapper); 281 282 // Remove loading state if it exists 283 parentLink.removeAttribute("data-dlh-loading"); 284 } else { 285 // Fallback in case parentLink doesn't exist 286 const container = document.querySelector(".dlh-inline-link") || document.body; 287 const fallbackAnchor = document.createElement("a"); 288 fallbackAnchor.className = "dlh-parent-link"; 289 fallbackAnchor.href = KSAL_DATA.fallbackUrl; 290 fallbackAnchor.setAttribute("data-link-type", "single"); 291 fallbackAnchor.setAttribute( 292 "data-link-style", 293 KSAL_DATA.defaultStyle || "none" 294 ); 295 fallbackAnchor.target = "_self"; 296 fallbackAnchor.rel = "noopener noreferrer nofollow"; 297 298 const wrapper = document.createElement("div"); 299 wrapper.className = `dlh-inline-link dlh-style-${ 300 KSAL_DATA.defaultStyle || "none" 301 }`; 302 303 const span = document.createElement("span"); 304 span.className = "wp-block-dynamic-location-dlh-address"; 305 span.dataset.dlh = "dlh-address"; 306 span.style.fontWeight = NORMAL_FONT_WEIGHT; 307 span.textContent = KSAL_DATA.fallbackText; 308 309 wrapper.appendChild(span); 310 fallbackAnchor.appendChild(wrapper); 311 container.appendChild(fallbackAnchor); 312 } 313 } 314}); 315 316/** 317 * Handles store changes from manual selection (Choose Location button). 318 */ 319onStoreChanged(({ source, newPostId }) => { 320 321 if (source === "header") return; 322 updateHeader(newPostId, true); 323});
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.