From 3180ddcbe4fa1437d59dfadd72ac9e3236f15e6c Mon Sep 17 00:00:00 2001 From: eti Date: Thu, 7 May 2026 23:20:16 +0200 Subject: [PATCH] appview: modernize topbar-search.js Signed-off-by: eti --- appview/pages/static/topbar-search.js | 412 +++++++++++++++----------- 1 file changed, 243 insertions(+), 169 deletions(-) diff --git a/appview/pages/static/topbar-search.js b/appview/pages/static/topbar-search.js index 71b1975c..cb1d40ca 100644 --- a/appview/pages/static/topbar-search.js +++ b/appview/pages/static/topbar-search.js @@ -1,171 +1,245 @@ -(function () { - if (window._navSearchReady) return; - window._navSearchReady = true; - - var scrollY = 0; - var vvListener = null; - var mobileResultsTouchLock = null; - - function updateOverlayHeight() { - var overlay = document.getElementById('mobile-search-overlay'); - if (!overlay) return; - var vv = window.visualViewport; - var layoutH = window.innerHeight; - var visH = vv ? vv.height : layoutH; - overlay.style.height = layoutH + 'px'; - overlay.style.top = '0px'; - var spacer = document.getElementById('mobile-search-spacer'); - if (spacer) spacer.style.height = Math.max(0, layoutH - visH) + 'px'; - } - - function openMobile() { - var overlay = document.getElementById('mobile-search-overlay'); - if (!overlay) return; - if (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'); - scrollY = window.scrollY; - document.body.style.position = 'fixed'; - document.body.style.top = '-' + scrollY + 'px'; - document.body.style.width = '100%'; - updateOverlayHeight(); - if (window.visualViewport) { - vvListener = updateOverlayHeight; - window.visualViewport.addEventListener('resize', vvListener); - } - var input = document.getElementById('mobile-search-input'); - if (input) input.focus({ preventScroll: true }); - var results = document.getElementById('mobile-search-results'); - if (results && !mobileResultsTouchLock) { - mobileResultsTouchLock = function (e) { e.preventDefault(); }; - results.addEventListener('touchmove', mobileResultsTouchLock, { passive: false }); - } - } - - function closeMobile() { - var overlay = document.getElementById('mobile-search-overlay'); - 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 = ''; - var spacer = document.getElementById('mobile-search-spacer'); - if (spacer) { spacer.style.height = ''; spacer.classList.add('hidden'); } - if (window.visualViewport && vvListener) { - window.visualViewport.removeEventListener('resize', vvListener); - vvListener = null; - } - document.body.style.position = ''; - document.body.style.top = ''; - document.body.style.width = ''; - window.scrollTo(0, scrollY); - var input = document.getElementById('mobile-search-input'); - if (input) { input.value = ''; input.blur(); } - var results = document.getElementById('mobile-search-results'); - if (results) results.innerHTML = ''; - } - - function clearDesktop() { - var r = document.getElementById('topbar-search-results'); - if (r) r.innerHTML = ''; - var b = document.getElementById('topbar-search-box'); - if (b) { b.classList.add('rounded'); b.classList.remove('rounded-t'); } - } - - function submitFromInput(input) { - var v = input.value.trim(); - if (v) window.location.href = '/search?q=' + encodeURIComponent(v); - } - - document.addEventListener('click', function (e) { - var t = e.target.closest('[data-action]'); - if (t) { - var a = t.getAttribute('data-action'); - if (a === 'open-mobile-search') { e.preventDefault(); openMobile(); return; } - if (a === 'close-mobile-search') { e.preventDefault(); closeMobile(); return; } - } - var c = document.getElementById('topbar-search-container'); - if (c && !c.contains(e.target)) clearDesktop(); - }); - - // Defer so a click on a result fires before results are cleared. - document.addEventListener('focusout', function (e) { - var c = document.getElementById('topbar-search-container'); - if (c && c.contains(e.target) && !c.contains(e.relatedTarget)) { - setTimeout(clearDesktop, 0); - } - }); - - document.addEventListener('htmx:afterSwap', function (e) { - var t = e.detail.target; - if (!t) return; - if (t.id === 'topbar-search-results') { - var b = document.getElementById('topbar-search-box'); - var open = t.children.length > 0; - if (b) { b.classList.toggle('rounded', !open); b.classList.toggle('rounded-t', open); } - return; - } - if (t.id === 'mobile-search-results') { - if (mobileResultsTouchLock) { - t.removeEventListener('touchmove', mobileResultsTouchLock); - mobileResultsTouchLock = null; - } - var spacer = document.getElementById('mobile-search-spacer'); - if (!spacer) return; - var hasResults = !!t.querySelector('[data-results-footer]'); - spacer.classList.toggle('hidden', !hasResults); - } - }); - - document.addEventListener('keydown', function (e) { - var input = document.getElementById('topbar-search-input'); - var results = document.getElementById('topbar-search-results'); - var mobileOverlay = document.getElementById('mobile-search-overlay'); - var mobileInput = document.getElementById('mobile-search-input'); - var active = document.activeElement; - - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - if (input) { input.focus(); input.select(); } - return; - } - - if (e.key === 'Enter') { - if (active === input) { e.preventDefault(); submitFromInput(input); return; } - if (active === mobileInput) { e.preventDefault(); submitFromInput(mobileInput); return; } - } - - if (e.key === 'Escape') { - if (mobileOverlay && !mobileOverlay.classList.contains('opacity-0')) { - e.preventDefault(); - closeMobile(); - return; - } - if (input) { - var ls = results ? Array.from(results.querySelectorAll('[data-nav-result]')) : []; - if (active === input || ls.indexOf(active) >= 0) { - e.preventDefault(); - clearDesktop(); - input.blur(); +(() => { + 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"); + 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"); + + savedScrollY = window.scrollY; + Object.assign(document.body.style, { + position: "fixed", + top: `-${savedScrollY}px`, + width: "100%", + }); + updateOverlayHeight(); + + if (window.visualViewport) { + window.visualViewport.addEventListener( + "resize", + updateOverlayHeight, + ); } - } - } - - if (!input || !results) return; - var links = Array.from(results.querySelectorAll('[data-nav-result]')); - var inInput = active === input; - var idx = links.indexOf(active); - - if (e.key === 'ArrowDown') { - if (inInput && links.length) { e.preventDefault(); links[0].focus(); } - else if (idx >= 0 && idx < links.length - 1) { e.preventDefault(); links[idx + 1].focus(); } - } - - if (e.key === 'ArrowUp') { - if (idx === 0) { e.preventDefault(); input.focus(); } - else if (idx > 0) { e.preventDefault(); links[idx - 1].focus(); } - } - }); + + $("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"); + 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 = ""; + + 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: 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(); + } + } + }); })(); -- 2.54.0