Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview: modernize topbar-search.js

Signed-off-by: eti <eti@eti.tf>

eti (May 7, 2026, 11:20 PM +0200) 3180ddcb 10e5ad24

+229 -155
+229 -155
appview/pages/static/topbar-search.js
··· 1 - (function () { 2 - if (window._navSearchReady) return; 3 - window._navSearchReady = true; 1 + (() => { 2 + if (window._navSearchReady) return; 3 + window._navSearchReady = true; 4 4 5 - var scrollY = 0; 6 - var vvListener = null; 7 - var mobileResultsTouchLock = null; 5 + const $ = (id) => document.getElementById(id); 8 6 9 - function updateOverlayHeight() { 10 - var overlay = document.getElementById('mobile-search-overlay'); 11 - if (!overlay) return; 12 - var vv = window.visualViewport; 13 - var layoutH = window.innerHeight; 14 - var visH = vv ? vv.height : layoutH; 15 - overlay.style.height = layoutH + 'px'; 16 - overlay.style.top = '0px'; 17 - var spacer = document.getElementById('mobile-search-spacer'); 18 - if (spacer) spacer.style.height = Math.max(0, layoutH - visH) + 'px'; 19 - } 7 + const submitFromInput = (input) => { 8 + const query = input.value.trim(); 9 + if (query) 10 + window.location.href = `/search?q=${encodeURIComponent(query)}`; 11 + }; 20 12 21 - function openMobile() { 22 - var overlay = document.getElementById('mobile-search-overlay'); 23 - if (!overlay) return; 24 - if (overlay.classList.contains('opacity-100')) return; 25 - overlay.classList.remove('opacity-0', 'pointer-events-none'); 26 - overlay.classList.add('opacity-100', 'pointer-events-auto'); 27 - overlay.setAttribute('aria-hidden', 'false'); 28 - scrollY = window.scrollY; 29 - document.body.style.position = 'fixed'; 30 - document.body.style.top = '-' + scrollY + 'px'; 31 - document.body.style.width = '100%'; 32 - updateOverlayHeight(); 33 - if (window.visualViewport) { 34 - vvListener = updateOverlayHeight; 35 - window.visualViewport.addEventListener('resize', vvListener); 36 - } 37 - var input = document.getElementById('mobile-search-input'); 38 - if (input) input.focus({ preventScroll: true }); 39 - var results = document.getElementById('mobile-search-results'); 40 - if (results && !mobileResultsTouchLock) { 41 - mobileResultsTouchLock = function (e) { e.preventDefault(); }; 42 - results.addEventListener('touchmove', mobileResultsTouchLock, { passive: false }); 43 - } 44 - } 13 + // mobile-related code 14 + let savedScrollY = 0; 15 + let touchMoveHandler = null; 45 16 46 - function closeMobile() { 47 - var overlay = document.getElementById('mobile-search-overlay'); 48 - if (!overlay) return; 49 - overlay.classList.remove('opacity-100', 'pointer-events-auto'); 50 - overlay.classList.add('opacity-0', 'pointer-events-none'); 51 - overlay.setAttribute('aria-hidden', 'true'); 52 - overlay.style.height = ''; 53 - overlay.style.top = ''; 54 - var spacer = document.getElementById('mobile-search-spacer'); 55 - if (spacer) { spacer.style.height = ''; spacer.classList.add('hidden'); } 56 - if (window.visualViewport && vvListener) { 57 - window.visualViewport.removeEventListener('resize', vvListener); 58 - vvListener = null; 59 - } 60 - document.body.style.position = ''; 61 - document.body.style.top = ''; 62 - document.body.style.width = ''; 63 - window.scrollTo(0, scrollY); 64 - var input = document.getElementById('mobile-search-input'); 65 - if (input) { input.value = ''; input.blur(); } 66 - var results = document.getElementById('mobile-search-results'); 67 - if (results) results.innerHTML = ''; 68 - } 17 + const updateOverlayHeight = () => { 18 + const overlay = $("mobile-search-overlay"); 19 + if (!overlay) return; 69 20 70 - function clearDesktop() { 71 - var r = document.getElementById('topbar-search-results'); 72 - if (r) r.innerHTML = ''; 73 - var b = document.getElementById('topbar-search-box'); 74 - if (b) { b.classList.add('rounded'); b.classList.remove('rounded-t'); } 75 - } 21 + const layoutHeight = window.innerHeight; 22 + const visibleHeight = window.visualViewport?.height ?? layoutHeight; 76 23 77 - function submitFromInput(input) { 78 - var v = input.value.trim(); 79 - if (v) window.location.href = '/search?q=' + encodeURIComponent(v); 80 - } 24 + overlay.style.height = `${layoutHeight}px`; 25 + overlay.style.top = "0px"; 81 26 82 - document.addEventListener('click', function (e) { 83 - var t = e.target.closest('[data-action]'); 84 - if (t) { 85 - var a = t.getAttribute('data-action'); 86 - if (a === 'open-mobile-search') { e.preventDefault(); openMobile(); return; } 87 - if (a === 'close-mobile-search') { e.preventDefault(); closeMobile(); return; } 88 - } 89 - var c = document.getElementById('topbar-search-container'); 90 - if (c && !c.contains(e.target)) clearDesktop(); 91 - }); 27 + const spacer = $("mobile-search-spacer"); 28 + if (spacer) 29 + spacer.style.height = `${Math.max(0, layoutHeight - visibleHeight)}px`; 30 + }; 92 31 93 - // Defer so a click on a result fires before results are cleared. 94 - document.addEventListener('focusout', function (e) { 95 - var c = document.getElementById('topbar-search-container'); 96 - if (c && c.contains(e.target) && !c.contains(e.relatedTarget)) { 97 - setTimeout(clearDesktop, 0); 98 - } 99 - }); 32 + const openMobile = () => { 33 + const overlay = $("mobile-search-overlay"); 34 + if (!overlay || overlay.classList.contains("opacity-100")) return; 100 35 101 - document.addEventListener('htmx:afterSwap', function (e) { 102 - var t = e.detail.target; 103 - if (!t) return; 104 - if (t.id === 'topbar-search-results') { 105 - var b = document.getElementById('topbar-search-box'); 106 - var open = t.children.length > 0; 107 - if (b) { b.classList.toggle('rounded', !open); b.classList.toggle('rounded-t', open); } 108 - return; 109 - } 110 - if (t.id === 'mobile-search-results') { 111 - if (mobileResultsTouchLock) { 112 - t.removeEventListener('touchmove', mobileResultsTouchLock); 113 - mobileResultsTouchLock = null; 114 - } 115 - var spacer = document.getElementById('mobile-search-spacer'); 116 - if (!spacer) return; 117 - var hasResults = !!t.querySelector('[data-results-footer]'); 118 - spacer.classList.toggle('hidden', !hasResults); 119 - } 120 - }); 36 + overlay.classList.remove("opacity-0", "pointer-events-none"); 37 + overlay.classList.add("opacity-100", "pointer-events-auto"); 38 + overlay.setAttribute("aria-hidden", "false"); 121 39 122 - document.addEventListener('keydown', function (e) { 123 - var input = document.getElementById('topbar-search-input'); 124 - var results = document.getElementById('topbar-search-results'); 125 - var mobileOverlay = document.getElementById('mobile-search-overlay'); 126 - var mobileInput = document.getElementById('mobile-search-input'); 127 - var active = document.activeElement; 40 + savedScrollY = window.scrollY; 41 + Object.assign(document.body.style, { 42 + position: "fixed", 43 + top: `-${savedScrollY}px`, 44 + width: "100%", 45 + }); 46 + updateOverlayHeight(); 128 47 129 - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 130 - e.preventDefault(); 131 - if (input) { input.focus(); input.select(); } 132 - return; 133 - } 134 - 135 - if (e.key === 'Enter') { 136 - if (active === input) { e.preventDefault(); submitFromInput(input); return; } 137 - if (active === mobileInput) { e.preventDefault(); submitFromInput(mobileInput); return; } 138 - } 139 - 140 - if (e.key === 'Escape') { 141 - if (mobileOverlay && !mobileOverlay.classList.contains('opacity-0')) { 142 - e.preventDefault(); 143 - closeMobile(); 144 - return; 145 - } 146 - if (input) { 147 - var ls = results ? Array.from(results.querySelectorAll('[data-nav-result]')) : []; 148 - if (active === input || ls.indexOf(active) >= 0) { 149 - e.preventDefault(); 150 - clearDesktop(); 151 - input.blur(); 48 + if (window.visualViewport) { 49 + window.visualViewport.addEventListener( 50 + "resize", 51 + updateOverlayHeight, 52 + ); 152 53 } 153 - } 154 - } 155 54 156 - if (!input || !results) return; 157 - var links = Array.from(results.querySelectorAll('[data-nav-result]')); 158 - var inInput = active === input; 159 - var idx = links.indexOf(active); 55 + $("mobile-search-input")?.focus({ preventScroll: true }); 160 56 161 - if (e.key === 'ArrowDown') { 162 - if (inInput && links.length) { e.preventDefault(); links[0].focus(); } 163 - else if (idx >= 0 && idx < links.length - 1) { e.preventDefault(); links[idx + 1].focus(); } 164 - } 57 + const results = $("mobile-search-results"); 58 + if (results && !touchMoveHandler) { 59 + touchMoveHandler = (e) => e.preventDefault(); 60 + results.addEventListener("touchmove", touchMoveHandler, { 61 + passive: false, 62 + }); 63 + } 64 + }; 165 65 166 - if (e.key === 'ArrowUp') { 167 - if (idx === 0) { e.preventDefault(); input.focus(); } 168 - else if (idx > 0) { e.preventDefault(); links[idx - 1].focus(); } 169 - } 170 - }); 66 + const closeMobile = () => { 67 + const overlay = $("mobile-search-overlay"); 68 + if (!overlay) return; 69 + 70 + overlay.classList.remove("opacity-100", "pointer-events-auto"); 71 + overlay.classList.add("opacity-0", "pointer-events-none"); 72 + overlay.setAttribute("aria-hidden", "true"); 73 + overlay.style.height = ""; 74 + overlay.style.top = ""; 75 + 76 + const spacer = $("mobile-search-spacer"); 77 + if (spacer) { 78 + spacer.style.height = ""; 79 + spacer.classList.add("hidden"); 80 + } 81 + 82 + if (window.visualViewport) { 83 + window.visualViewport.removeEventListener( 84 + "resize", 85 + updateOverlayHeight, 86 + ); 87 + } 88 + 89 + Object.assign(document.body.style, { 90 + position: "", 91 + top: "", 92 + width: "", 93 + }); 94 + window.scrollTo(0, savedScrollY); 95 + 96 + const input = $("mobile-search-input"); 97 + if (input) { 98 + input.value = ""; 99 + input.blur(); 100 + } 101 + 102 + $("mobile-search-results")?.replaceChildren(); 103 + }; 104 + 105 + // desktop-related things 106 + const clearDesktop = () => { 107 + $("topbar-search-results")?.replaceChildren(); 108 + 109 + const box = $("topbar-search-box"); 110 + box?.classList.add("rounded"); 111 + box?.classList.remove("rounded-t"); 112 + }; 113 + 114 + // events 115 + document.addEventListener("click", ({ target }) => { 116 + // mobile: open/close overlay via data-action buttons 117 + const action = target 118 + .closest("[data-action]") 119 + ?.getAttribute("data-action"); 120 + if (action === "open-mobile-search") { 121 + openMobile(); 122 + return; 123 + } 124 + if (action === "close-mobile-search") { 125 + closeMobile(); 126 + return; 127 + } 128 + 129 + // desktop: clicking outside the search container clears results 130 + const container = $("topbar-search-container"); 131 + if (container && !container.contains(target)) clearDesktop(); 132 + }); 133 + 134 + // desktop: defer so a click on a result fires before results are cleared 135 + document.addEventListener("focusout", ({ target, relatedTarget }) => { 136 + const container = $("topbar-search-container"); 137 + if (container?.contains(target) && !container.contains(relatedTarget)) { 138 + setTimeout(clearDesktop, 0); 139 + } 140 + }); 141 + 142 + document.addEventListener("htmx:afterSwap", ({ detail: { target } }) => { 143 + if (!target) return; 144 + 145 + // desktop: toggle rounded corners based on whether results are open 146 + if (target.id === "topbar-search-results") { 147 + const box = $("topbar-search-box"); 148 + const open = target.children.length > 0; 149 + box?.classList.toggle("rounded", !open); 150 + box?.classList.toggle("rounded-t", open); 151 + return; 152 + } 153 + 154 + // mobile: restore touch listener and show spacer when results arrive 155 + if (target.id === "mobile-search-results") { 156 + if (touchMoveHandler) { 157 + target.removeEventListener("touchmove", touchMoveHandler); 158 + touchMoveHandler = null; 159 + } 160 + 161 + const hasResults = !!target.querySelector("[data-results-footer]"); 162 + $("mobile-search-spacer")?.classList.toggle("hidden", !hasResults); 163 + } 164 + }); 165 + 166 + document.addEventListener("keydown", (e) => { 167 + const { key, metaKey, ctrlKey } = e; 168 + const input = $("topbar-search-input"); 169 + const results = $("topbar-search-results"); 170 + const mobileOverlay = $("mobile-search-overlay"); 171 + const mobileInput = $("mobile-search-input"); 172 + const active = document.activeElement; 173 + 174 + // desktop: ⌘K / Ctrl+K focuses the search input 175 + if ((metaKey || ctrlKey) && key === "k") { 176 + e.preventDefault(); 177 + input?.focus(); 178 + input?.select(); 179 + return; 180 + } 181 + 182 + if (key === "Enter") { 183 + if (active === input) { 184 + e.preventDefault(); 185 + submitFromInput(input); 186 + return; 187 + } // desktop 188 + if (active === mobileInput) { 189 + e.preventDefault(); 190 + submitFromInput(mobileInput); 191 + return; 192 + } // mobile 193 + } 194 + 195 + if (key === "Escape") { 196 + // mobile: close the overlay 197 + if ( 198 + mobileOverlay && 199 + !mobileOverlay.classList.contains("opacity-0") 200 + ) { 201 + e.preventDefault(); 202 + closeMobile(); 203 + return; 204 + } 205 + // desktop: clear results and blur 206 + if (input) { 207 + const links = results 208 + ? [...results.querySelectorAll("[data-nav-result]")] 209 + : []; 210 + if (active === input || links.includes(active)) { 211 + e.preventDefault(); 212 + clearDesktop(); 213 + input.blur(); 214 + } 215 + } 216 + } 217 + 218 + // desktop: arrow key navigation through results 219 + if (!input || !results) return; 220 + 221 + const links = [...results.querySelectorAll("[data-nav-result]")]; 222 + const inputFocused = active === input; 223 + const focusedIndex = links.indexOf(active); 224 + 225 + if (key === "ArrowDown") { 226 + if (inputFocused && links.length) { 227 + e.preventDefault(); 228 + links[0].focus(); 229 + } else if (focusedIndex >= 0 && focusedIndex < links.length - 1) { 230 + e.preventDefault(); 231 + links[focusedIndex + 1].focus(); 232 + } 233 + } 234 + 235 + if (key === "ArrowUp") { 236 + if (focusedIndex === 0) { 237 + e.preventDefault(); 238 + input.focus(); 239 + } else if (focusedIndex > 0) { 240 + e.preventDefault(); 241 + links[focusedIndex - 1].focus(); 242 + } 243 + } 244 + }); 171 245 })();