(() => { if (window._navSearchReady) return; window._navSearchReady = true; const $ = (id) => document.getElementById(id); const submitFromInput = (input) => { const query = input.value.trim(); if (query) window.location.href = `/search?q=${encodeURIComponent(query)}`; }; // mobile-related code let savedScrollY = 0; let touchMoveHandler = null; const updateOverlayHeight = () => { const overlay = $("mobile-search-overlay"); if (!overlay) return; const layoutHeight = window.innerHeight; const visibleHeight = window.visualViewport?.height ?? layoutHeight; overlay.style.height = `${layoutHeight}px`; overlay.style.top = "0px"; const spacer = $("mobile-search-spacer"); if (spacer) spacer.style.height = `${Math.max(0, layoutHeight - visibleHeight)}px`; }; const openMobile = () => { const overlay = $("mobile-search-overlay"); const subs = $("subs"); if (!overlay || overlay.classList.contains("opacity-100")) return; overlay.classList.remove("opacity-0", "pointer-events-none"); overlay.classList.add("opacity-100", "pointer-events-auto"); overlay.setAttribute("aria-hidden", "false"); subs?.classList.remove("z-30"); savedScrollY = window.scrollY; Object.assign(document.body.style, { position: "fixed", top: `-${savedScrollY}px`, width: "100%", }); updateOverlayHeight(); if (window.visualViewport) { window.visualViewport.addEventListener( "resize", updateOverlayHeight, ); } $("mobile-search-input")?.focus({ preventScroll: true }); const results = $("mobile-search-results"); if (results && !touchMoveHandler) { touchMoveHandler = (e) => e.preventDefault(); results.addEventListener("touchmove", touchMoveHandler, { passive: false, }); } }; const closeMobile = () => { const overlay = $("mobile-search-overlay"); const subs = $("subs"); if (!overlay) return; overlay.classList.remove("opacity-100", "pointer-events-auto"); overlay.classList.add("opacity-0", "pointer-events-none"); overlay.setAttribute("aria-hidden", "true"); overlay.style.height = ""; overlay.style.top = ""; subs?.classList.add("z-30"); const spacer = $("mobile-search-spacer"); if (spacer) { spacer.style.height = ""; spacer.classList.add("hidden"); } if (window.visualViewport) { window.visualViewport.removeEventListener( "resize", updateOverlayHeight, ); } Object.assign(document.body.style, { position: "", top: "", width: "", }); window.scrollTo(0, savedScrollY); const input = $("mobile-search-input"); if (input) { input.value = ""; input.blur(); } $("mobile-search-results")?.replaceChildren(); }; // desktop-related things const clearDesktop = () => { $("topbar-search-results")?.replaceChildren(); const box = $("topbar-search-box"); box?.classList.add("rounded"); box?.classList.remove("rounded-t"); }; // events document.addEventListener("click", ({ target }) => { // mobile: open/close overlay via data-action buttons const action = target .closest("[data-action]") ?.getAttribute("data-action"); if (action === "open-mobile-search") { openMobile(); return; } if (action === "close-mobile-search") { closeMobile(); return; } // desktop: clicking outside the search container clears results const container = $("topbar-search-container"); if (container && !container.contains(target)) clearDesktop(); }); // desktop: keep the input focused when a result is clicked. // Safari does not move focus to links/buttons on click, so mousedown would // blur the input, fire focusout with a null relatedTarget, and clear the // results before the click lands. Preventing the mousedown default keeps // focus on the input while the click still navigates via the href. document.addEventListener("mousedown", (e) => { if ($("topbar-search-results")?.contains(e.target)) e.preventDefault(); }); // desktop: defer so a click on a result fires before results are cleared document.addEventListener("focusout", ({ target, relatedTarget }) => { const container = $("topbar-search-container"); if (container?.contains(target) && !container.contains(relatedTarget)) { setTimeout(clearDesktop, 0); } }); document.addEventListener("htmx:afterSwap", ({ detail: { target } }) => { if (!target) return; // desktop: toggle rounded corners based on whether results are open if (target.id === "topbar-search-results") { const box = $("topbar-search-box"); const open = target.children.length > 0; box?.classList.toggle("rounded", !open); box?.classList.toggle("rounded-t", open); return; } // mobile: restore touch listener and show spacer when results arrive if (target.id === "mobile-search-results") { if (touchMoveHandler) { target.removeEventListener("touchmove", touchMoveHandler); touchMoveHandler = null; } const hasResults = !!target.querySelector("[data-results-footer]"); $("mobile-search-spacer")?.classList.toggle("hidden", !hasResults); } }); document.addEventListener("keydown", (e) => { const { key, metaKey, ctrlKey } = e; const input = $("topbar-search-input"); const results = $("topbar-search-results"); const mobileOverlay = $("mobile-search-overlay"); const mobileInput = $("mobile-search-input"); const active = document.activeElement; // desktop: ⌘K / Ctrl+K focuses the search input if ((metaKey || ctrlKey) && key === "k") { e.preventDefault(); input?.focus(); input?.select(); return; } if (key === "Enter") { if (active === input) { e.preventDefault(); submitFromInput(input); return; } // desktop if (active === mobileInput) { e.preventDefault(); submitFromInput(mobileInput); return; } // mobile } if (key === "Escape") { // mobile: close the overlay if ( mobileOverlay && !mobileOverlay.classList.contains("opacity-0") ) { e.preventDefault(); closeMobile(); return; } // desktop: clear results and blur if (input) { const links = results ? [...results.querySelectorAll("[data-nav-result]")] : []; if (active === input || links.includes(active)) { e.preventDefault(); clearDesktop(); input.blur(); } } } // desktop: arrow key navigation through results if (!input || !results) return; const links = [...results.querySelectorAll("[data-nav-result]")]; const inputFocused = active === input; const focusedIndex = links.indexOf(active); if (key === "ArrowDown") { if (inputFocused && links.length) { e.preventDefault(); links[0].focus(); } else if (focusedIndex >= 0 && focusedIndex < links.length - 1) { e.preventDefault(); links[focusedIndex + 1].focus(); } } if (key === "ArrowUp") { if (focusedIndex === 0) { e.preventDefault(); input.focus(); } else if (focusedIndex > 0) { e.preventDefault(); links[focusedIndex - 1].focus(); } } }); })();