/* ===================================================================== RYMAN BUSINESS - NO SEARCH RESULTS + 404 (shared script) - No search results page: wires up the .rb-nsr markup already on the page. - 404 page (body.ex-404): builds the same layout and inserts it after body > .container, then wires it up. Styles: shared ryman-nsr.css in the header. ===================================================================== */ (function () { "use strict"; const CONFIG = { bodyClass404: "ex-404", mount404Selector: "body > .container:has(.middle.content)", mount404Position: "afterend", // sibling of the container queryParams: ["query", "q"], // first match wins; 'query' is what /search uses searchPath: "/search", suggestUrl: "/ajax/search-v2/search", // EvoX v2 search, suggestions only suggestLimit: 8, categoriesUrl: "/", // TODO: category landing URL homeUrl: "/", }; /* --------------------------------------------------------------- 404 markup --------------------------------------------------------------- */ const HTML_404 = `
404 · Page not found

Whoops, we couldn’t find the page you’re looking for

Here’s what you can do

Need a hand finding something?

Additional Search Options

`; /* --------------------------------------------------------------- Helpers --------------------------------------------------------------- */ const searchHref = (term) => `${CONFIG.searchPath}?${CONFIG.queryParams[0]}=${encodeURIComponent(term)}`; const mount404 = () => { const root = document.createElement("div"); root.className = "rb-nsr rb-nsr--404"; root.setAttribute("data-rb-nsr", ""); root.innerHTML = HTML_404; const target = document.querySelector(CONFIG.mount404Selector); if (target) { target.insertAdjacentElement(CONFIG.mount404Position, root); } else { const footer = document.querySelector("footer"); if (footer) footer.insertAdjacentElement("beforebegin", root); else document.body.appendChild(root); } return root; }; /* "furniture--3" -> "furniture", "catering-and-hospitality-1" -> "catering and hospitality" */ const termFromPath = () => { const segments = window.location.pathname.split("/").filter(Boolean); if (!segments.length) return ""; let term = segments[segments.length - 1]; try { term = decodeURIComponent(term); } catch (e) { /* keep raw */ } term = term .replace(/\.(html?|aspx?|php)$/i, "") .replace(/-{1,2}\d{1,2}$/, "") .replace(/[-_+]+/g, " ") .replace(/\s+/g, " ") .trim(); if (term.length < 3 || /^\d+$/.test(term)) return ""; return term; }; const initTabs = (root) => { const switchEl = root.querySelector("[data-rb-nsr-switch]"); if (!switchEl) return; const tabs = [...switchEl.querySelectorAll('[role="tab"]')]; const select = (tab) => { tabs.forEach((t) => { const on = t === tab; t.setAttribute("aria-selected", String(on)); t.tabIndex = on ? 0 : -1; const panel = root.querySelector(`#${t.getAttribute("aria-controls")}`); if (panel) panel.hidden = !on; }); }; tabs.forEach((tab, i) => { tab.addEventListener("click", () => select(tab)); tab.addEventListener("keydown", (e) => { if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return; e.preventDefault(); const next = tabs[(i + (e.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length]; select(next); next.focus(); }); }); }; const pickText = (item) => { if (typeof item === "string") return item; if (item && typeof item === "object") { return item.text || item.term || item.suggestion || item.name || item.value || ""; } return ""; }; const normalise = (data) => { if (!data) return []; const list = Array.isArray(data) ? data : data.suggestions || (data.data && data.data.suggestions) || []; return Array.isArray(list) ? list.map((item) => String(pickText(item)).trim()) : []; }; /* Related-search chips. includeTerm: true -> the term itself is the first chip (404) false -> the term is skipped, it already returned nothing (search) */ const initRelated = (root, term, includeTerm) => { const related = root.querySelector("[data-rb-nsr-related]"); if (!related || !term) return; const seen = new Set(includeTerm ? [] : [term.toLowerCase()]); let count = 0; const addChips = (terms) => { const frag = document.createDocumentFragment(); terms.forEach((t) => { const key = t.toLowerCase(); if (!t || seen.has(key) || count >= CONFIG.suggestLimit) return; seen.add(key); count += 1; const a = document.createElement("a"); a.className = "rb-nsr__chip"; a.href = searchHref(t); a.textContent = t; frag.appendChild(a); }); related.appendChild(frag); related.hidden = count === 0; }; if (includeTerm) addChips([term]); const url = `${CONFIG.suggestUrl}?q=${encodeURIComponent(term)}` + `&only_suggestions=true&suggestions_limit=${CONFIG.suggestLimit}`; fetch(url, { credentials: "include", headers: { Accept: "application/json", "X-Requested-With": "XMLHttpRequest" }, }) .then((res) => (res.ok ? res.json() : null)) .then((data) => addChips(normalise(data))) .catch(() => { /* suggestions are optional */ }); }; /* --------------------------------------------------------------- Page setups --------------------------------------------------------------- */ const initSearchPage = (root) => { const params = new URLSearchParams(window.location.search); const queryKey = CONFIG.queryParams.find((k) => params.get(k)) || CONFIG.queryParams[0]; const query = (params.get(queryKey) || "").trim(); const leadEl = root.querySelector("[data-rb-nsr-lead]"); const termEl = root.querySelector("[data-rb-nsr-term]"); const clearLink = root.querySelector("[data-rb-nsr-clear]"); /* heading */ if (query && termEl) { termEl.textContent = `\u201C${query}\u201D`; } else { if (leadEl) leadEl.textContent = "Whoops, we couldn\u2019t find anything for that search"; if (termEl) termEl.remove(); } /* "clear filters" - only when extra params are present */ const extraParams = [...params.keys()].filter((k) => !CONFIG.queryParams.includes(k)); if (clearLink && query && extraParams.length) { clearLink.href = searchHref(query); clearLink.hidden = false; } initRelated(root, query, false); }; const init404Page = (root) => { const homeLink = root.querySelector("[data-rb-nsr-home]"); if (homeLink) homeLink.href = CONFIG.homeUrl; initRelated(root, termFromPath(), true); }; /* --------------------------------------------------------------- Init --------------------------------------------------------------- */ const init = () => { const is404 = document.body.classList.contains(CONFIG.bodyClass404); let root = document.querySelector("[data-rb-nsr]"); if (!root && is404) root = mount404(); if (!root || root.dataset.rbNsrReady) return; root.dataset.rbNsrReady = "true"; /* shared */ const form = root.querySelector("[data-rb-nsr-form]"); const input = root.querySelector("[data-rb-nsr-input]"); const catLink = root.querySelector("[data-rb-nsr-categories]"); if (form) form.action = CONFIG.searchPath; if (input) input.name = CONFIG.queryParams[0]; if (catLink) catLink.href = CONFIG.categoriesUrl; initTabs(root); if (is404) init404Page(root); else initSearchPage(root); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();