Changeset fd8faf5 in Klonkt for src/assets/js/mod
- Timestamp:
- 09/30/2026 07:27:05 AM (4 hours ago)
- Branches:
- main
- Children:
- 19e28ce
- Parents:
- 86cfa1a
- File:
-
- 1 edited
-
src/assets/js/mod/chrome.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/chrome.js
r86cfa1a rfd8faf5 119 119 try { localStorage.setItem('pcms-theme', next); } catch (e) {} 120 120 } 121 // ── Het zoeken: de balk bovenaan, het vak met resultaten eronder ───── 122 // Twee plekken, één vorm (Robins ontwerp, 30-9): de overlay die op elke 123 // pagina kan openen, en de pagina /search?q=... zelf. Alles hieronder werkt 124 // op elk .search-surface, zodat de twee zich ook hetzelfde gedragen. 121 125 function overlay() { return document.getElementById('search-overlay'); } 122 function openSearch() { var o = overlay(); if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } } 123 function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } } 126 function openSearch() { 127 var o = overlay(); if (!o) return; 128 var inp = o.querySelector('input[name="q"]'); 129 var box = o.querySelector('.search-box-inner'); 130 // Een verse start: tekst en resultaten van de vorige keer horen niet te 131 // blijven staan als je hem opnieuw opent. 132 if (inp) inp.value = ''; 133 if (box) box.innerHTML = ''; 134 o.hidden = false; 135 document.documentElement.classList.add('search-open'); 136 if (inp) inp.focus(); 137 } 138 function closeSearch() { 139 var o = overlay(); if (!o) return; 140 o.hidden = true; 141 document.documentElement.classList.remove('search-open'); 142 } 124 143 125 144 document.body.addEventListener('click', function(e) { … … 137 156 }); 138 157 139 // ── Live results while typing ───────────────────────────────────140 // De teksten komen van de overlay zelf, elke keer opnieuw: bij een141 // htmx-navigatie wordt die vervangen en kan de taal gewisseld zijn.142 function _st() {143 var o = document.getElementById('search-overlay');144 try { return JSON.parse((o && o.getAttribute('data-i18n')) || '{}'); } catch (e) { return {}; }145 }146 158 function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } 147 function renderSuggest(box, d, q, ov) { 148 var html = ''; 149 function grp(title, items, fmt) { 150 if (!items || !items.length) return; 151 html += '<div class="ss-group"><div class="ss-title">' + esc(title) + '</div>' + items.map(fmt).join('') + '</div>'; 152 } 153 grp(_st().posts, d.posts, function(p){ return '<a class="ss-item" href="' + esc(p.url) + '">' + esc(p.title) + '</a>'; }); 154 grp(_st().tracks, d.tracks, function(tk){ var sub = tk.artist ? ' <span class="ss-sub">' + esc(tk.artist) + '</span>' : ''; return tk.url ? '<a class="ss-item" href="' + esc(tk.url) + '">' + esc(tk.title) + sub + '</a>' : '<span class="ss-item ss-noclick">' + esc(tk.title) + sub + '</span>'; }); 155 grp(_st().events, d.events, function(ev){ return '<a class="ss-item" href="' + esc(ev.url) + '">' + esc(ev.where || ev.when) + ' <span class="ss-sub">' + esc(ev.when) + '</span></a>'; }); 156 grp(_st().pages, d.pages, function(pg){ return '<a class="ss-item" href="' + esc(pg.url) + '">' + esc(pg.label) + '</a>'; }); 157 var hasAny = (d.posts && d.posts.length) || (d.tracks && d.tracks.length) || (d.events && d.events.length) || (d.pages && d.pages.length); 158 if (!hasAny) { box.innerHTML = '<div class="ss-empty">' + esc(_st().none) + '</div>'; return; } 159 var action = (ov && ov.getAttribute('data-action')) || '/search'; 160 html += '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>'; 161 box.innerHTML = html; 162 } 163 // Een ADRES is geen zoekterm (shaer-utpi). Dezelfde regel als lookupUri in 164 // routes/search.js: een schema is verplicht, anders wordt zoeken naar 165 // "soundfabrics.nl" een netwerkverzoek. De server beslist daarna nog eens, 166 // met de rechten erbij; dit scheelt alleen een vraag die toch nee oplevert. 167 function lookupUri(q) { 168 if (!/^https?:\/\//i.test(q) || /\s/.test(q)) return null; 169 try { var u = new URL(q); return u.hostname.indexOf('.') < 0 ? null : u.href; } catch (e) { return null; } 170 } 171 function allLink(ov, q) { 172 var action = (ov && ov.getAttribute('data-action')) || '/search'; 173 return '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>'; 174 } 175 function fetchSuggest(box, q, ov, seq) { 176 var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest'; 177 fetch(url + '?q=' + encodeURIComponent(q)) 178 .then(function(r){ return r.ok ? r.json() : null; }) 179 .then(function(d){ if (d && seq === _sSeq) renderSuggest(box, d, q, ov); }) 180 .catch(function(){}); 181 } 182 // Elke toetsaanslag krijgt een volgnummer. Een ophaling van een andere 183 // server duurt soms seconden; zonder nummer overschrijft een traag antwoord 184 // op een oude vraag wat je inmiddels hebt getypt. 185 var _sTimer, _sSeq = 0; 159 function texts(surface) { try { return JSON.parse(surface.getAttribute('data-i18n') || '{}'); } catch (e) { return {}; } } 160 161 // Iets dat van een andere server moet komen: een adres of een handle. Dezelfde 162 // regels als lookupUri en lookupHandle in routes/search.js. Hier alleen om te 163 // weten of er meteen een wachtregel moet staan: de server beslist daarna 164 // zelf, met de rechten erbij. 165 function isRemoteLookup(q) { 166 if (/^https?:\/\//i.test(q) && !/\s/.test(q)) { 167 try { return new URL(q).hostname.indexOf('.') >= 0; } catch (e) { return false; } 168 } 169 return /^@?[a-z0-9_.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/i.test(q); 170 } 171 172 // Elke zoekopdracht krijgt een volgnummer per vlak. Een ophaling van een 173 // andere server duurt soms seconden; zonder nummer overschrijft een traag 174 // antwoord op een oude vraag wat je inmiddels hebt getypt. 175 function runSearch(surface, now) { 176 if (!surface) return; 177 var inp = surface.querySelector('input[name="q"]'); 178 var box = surface.querySelector('.search-box-inner'); 179 if (!inp || !box) return; 180 var q = inp.value.trim(); 181 var seq = (surface.__seq = (surface.__seq || 0) + 1); 182 clearTimeout(surface.__timer); 183 if (q.length < 2) { box.innerHTML = ''; return; } 184 var remote = isRemoteLookup(q); 185 if (remote) box.innerHTML = '<div class="sr-loading">' + esc(texts(surface).looking) + '</div>'; 186 // Op de pagina beweegt het adres mee, zodat wat je ziet te delen blijft. 187 if (surface.classList.contains('is-page') && history.replaceState) { 188 history.replaceState(history.state, '', (surface.getAttribute('data-action') || '/search') + '?q=' + encodeURIComponent(q)); 189 } 190 surface.__timer = setTimeout(function() { 191 var url = surface.getAttribute('data-results') || '/search/results'; 192 fetch(url + '?q=' + encodeURIComponent(q), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } }) 193 .then(function(r) { return r.status === 200 ? r.text() : ''; }) 194 .then(function(html) { 195 if (seq !== surface.__seq) return; 196 box.innerHTML = html; 197 // De links naar een post zijn htmx-links; wat via innerHTML binnenkomt 198 // kent htmx nog niet. 199 if (window.htmx && window.htmx.process) window.htmx.process(box); 200 }) 201 .catch(function() {}); 202 }, now ? 0 : (remote ? 300 : 200)); 203 } 186 204 document.addEventListener('input', function(e) { 187 var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]'); 188 if (!inp) return; 189 var box = document.getElementById('search-suggest'); 190 if (!box) return; 191 var q = inp.value.trim(); 192 var seq = ++_sSeq; 193 clearTimeout(_sTimer); 194 if (q.length < 2) { box.innerHTML = ''; return; } 195 var ov = document.getElementById('search-overlay'); 196 var address = lookupUri(q); 197 if (!address) { _sTimer = setTimeout(function() { fetchSuggest(box, q, ov, seq); }, 200); return; } 198 // Een geplakt adres: de post zelf, niet de tekst van de link doorzocht. 199 box.innerHTML = '<div class="ss-remote-loading">' + esc(_st().looking) + '</div>'; 200 _sTimer = setTimeout(function() { 201 var url = (ov && ov.getAttribute('data-remote')) || '/search/remote'; 202 fetch(url + '?q=' + encodeURIComponent(address), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } }) 203 .then(function(r) { 204 if (seq !== _sSeq) return; 205 // 204: geen recht om op te halen (of toch geen adres). Dan is het 206 // gewoon een zoekterm en krijg je de gewone suggesties. 207 if (r.status === 204 || !r.ok) { fetchSuggest(box, q, ov, seq); return; } 208 return r.text().then(function(html) { if (seq === _sSeq) box.innerHTML = html + allLink(ov, q); }); 209 }) 210 .catch(function() { if (seq === _sSeq) fetchSuggest(box, q, ov, seq); }); 211 }, 300); 212 }); 213 // Een klik op een suggestie sluit de overlay, want de link gaat ergens heen. 214 // Behalve wat in een nieuw tabblad opent (het origineel, een profiel): dan 215 // blijf je hier, en hoort de kaart er nog te staan als je terugkomt. 205 var inp = e.target.closest && e.target.closest('.search-surface input[name="q"]'); 206 if (inp) runSearch(inp.closest('.search-surface'), false); 207 }); 208 // Enter blijft waar hij is: het vak IS de resultaten. Zonder JS gaat het 209 // formulier naar /search, dat er precies zo uitziet. 210 document.addEventListener('submit', function(e) { 211 var f = e.target.closest && e.target.closest('.search-bar-form'); 212 if (!f) return; 213 e.preventDefault(); 214 runSearch(f.closest('.search-surface'), true); 215 }); 216 // Sluiten op de pagina is teruggaan, als je van deze site kwam; anders volgt 217 // de link naar het begin van de site. 216 218 document.addEventListener('click', function(e) { 217 var a = e.target.closest && e.target.closest('#search-suggest a'); 218 if (a && a.getAttribute('target') !== '_blank') { var o = overlay(); if (o) o.hidden = true; } 219 var leave = e.target.closest && e.target.closest('[data-search-leave]'); 220 if (!leave) return; 221 if (document.referrer.indexOf(location.origin + '/') === 0 && history.length > 1) { e.preventDefault(); history.back(); } 222 }); 223 // Een klik op een resultaat in de overlay sluit hem, want de link gaat ergens 224 // heen. Behalve wat in een nieuw tabblad opent: dan blijf je hier. 225 document.addEventListener('click', function(e) { 226 var a = e.target.closest && e.target.closest('#search-overlay .search-box a'); 227 if (a && a.getAttribute('target') !== '_blank') closeSearch(); 219 228 }); 220 229 })();
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)