PageSourceSearch

https://www.nationalfloorsdirect.com/wp-content/plugins/roomvo-dyn…dlh-header-updater.js?x10333&ver=1762269544

js nationalfloorsdirect.com collected 2026-09-29 22:59:17 UTC 9,958 bytes, 323 lines download raw bytes

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.