Changeset 156baa3 in Klonkt for src/assets/js/mod/chrome.js
- Timestamp:
- 08/07/2026 01:38:51 PM (5 weeks ago)
- Branches:
- main
- Children:
- 6ee289a
- Parents:
- 52fc278
- git-author:
- Robin <roboburr@…> (08/07/2026 01:26:06 PM)
- git-committer:
- roboburr <roboburr@…> (08/07/2026 01:38:51 PM)
- File:
-
- 1 edited
-
src/assets/js/mod/chrome.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/chrome.js
r52fc278 r156baa3 100 100 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); 101 101 })(); 102 103 // ── de topnav ────────────────────────────────────────────────── 104 // Zat inline in partials/topnav.ejs. De zoekteksten komen nu van het 105 // overlay-element (data-i18n) in plaats van uit interpolatie. 106 (function() { 107 // Wired ONCE. This chrome (topnav) is re-inserted out-of-band during htmx navigation 108 // → without this guard the script would stack EXTRA listeners on every navigation, 109 // causing the theme toggle to fire 2× (or more) = no net change ("toggle stops working"). 110 // Everything below uses event delegation on body/document, so it also works for 111 // buttons that appear after this run (OOB). 112 if (window.__pcmsChromeWired) return; 113 window.__pcmsChromeWired = true; 114 115 function toggleTheme() { 116 var cur = document.documentElement.getAttribute('data-theme') || 'dark'; 117 var next = cur === 'dark' ? 'light' : 'dark'; 118 document.documentElement.setAttribute('data-theme', next); 119 try { localStorage.setItem('pcms-theme', next); } catch (e) {} 120 } 121 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 = ''; } } 124 125 document.body.addEventListener('click', function(e) { 126 if (e.target.closest('#theme-toggle, #theme-toggle-mobile, #theme-toggle-footer')) { toggleTheme(); return; } 127 if (e.target.closest('#search-toggle')) { openSearch(); return; } 128 if (e.target.closest('#search-close')) { closeSearch(); return; } 129 // Close open dropdowns (user menu + language picker) on click outside. 130 document.querySelectorAll('.user-menu[open], .lang-menu[open]').forEach(function(d) { 131 if (!d.contains(e.target)) d.removeAttribute('open'); 132 }); 133 }); 134 135 document.addEventListener('keydown', function(e) { 136 if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) closeSearch(); } 137 }); 138 139 // ── Live results while typing ─────────────────────────────────── 140 // De teksten komen van de overlay zelf, elke keer opnieuw: bij een 141 // 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 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 var _sTimer; 164 document.addEventListener('input', function(e) { 165 var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]'); 166 if (!inp) return; 167 var box = document.getElementById('search-suggest'); 168 if (!box) return; 169 var q = inp.value.trim(); 170 clearTimeout(_sTimer); 171 if (q.length < 2) { box.innerHTML = ''; return; } 172 _sTimer = setTimeout(function() { 173 var ov = document.getElementById('search-overlay'); 174 var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest'; 175 fetch(url + '?q=' + encodeURIComponent(q)) 176 .then(function(r){ return r.ok ? r.json() : null; }) 177 .then(function(d){ if (d) renderSuggest(box, d, q, ov); }) 178 .catch(function(){}); 179 }, 200); 180 }); 181 // Click on a suggestion → close the overlay (the link/boost handles navigation). 182 document.addEventListener('click', function(e) { 183 if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; } 184 }); 185 })(); 186 187 // ── het profielblad ──────────────────────────────────────────── 188 // Zat inline in partials/profile-sheet.ejs, dat de shell opneemt. 189 (function() { 190 const sheet = document.getElementById('profile-sheet'); 191 if (!sheet) return; 192 193 const backdrop = document.getElementById('profile-sheet-backdrop'); 194 const panel = sheet.querySelector('.profile-sheet-panel'); 195 const closeBtn = document.getElementById('profile-sheet-close'); 196 const dragZone = document.getElementById('profile-sheet-drag-zone'); 197 const themeBtn = document.getElementById('profile-sheet-theme'); 198 const themeLbl = document.getElementById('profile-sheet-theme-state'); 199 200 function openSheet() { 201 sheet.classList.add('is-open'); 202 sheet.setAttribute('aria-hidden', 'false'); 203 document.body.classList.add('profile-sheet-locked'); 204 syncTheme(); 205 } 206 function closeSheet() { 207 sheet.classList.remove('is-open'); 208 sheet.setAttribute('aria-hidden', 'true'); 209 document.body.classList.remove('profile-sheet-locked'); 210 panel.style.removeProperty('--pcms-drag-y'); 211 } 212 213 // Open: any element with [data-profile-sheet-toggle] 214 document.addEventListener('click', function(e) { 215 const trigger = e.target.closest('[data-profile-sheet-toggle]'); 216 if (trigger) { 217 e.preventDefault(); 218 openSheet(); 219 } 220 }); 221 222 // Close: backdrop tap, handle tap, ESC, or any [data-close-sheet] item 223 if (backdrop) backdrop.addEventListener('click', closeSheet); 224 if (closeBtn) closeBtn.addEventListener('click', closeSheet); 225 document.addEventListener('keydown', (e) => { 226 if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet(); 227 }); 228 229 // Menu items that navigate: close synchronously before nav fires 230 sheet.querySelectorAll('[data-close-sheet]').forEach((el) => { 231 el.addEventListener('click', closeSheet); 232 }); 233 234 // Drag-down-to-close on touch. 235 // Uses --pcms-drag-y custom property rather than overwriting the panel's 236 // transform string. This composes correctly with any horizontal centering 237 // (currently none here, but the pattern matches audio-sheet for safety). 238 let startY = 0, lastY = 0, dragging = false; 239 function onDown(e) { 240 if (e.pointerType !== 'touch') return; 241 if (panel.scrollTop > 0) return; 242 startY = lastY = e.clientY; 243 dragging = true; 244 panel.classList.add('is-dragging'); 245 } 246 function onMove(e) { 247 if (!dragging) return; 248 lastY = e.clientY; 249 const dy = Math.max(0, lastY - startY); 250 panel.style.setProperty('--pcms-drag-y', dy + 'px'); 251 } 252 function onUp() { 253 if (!dragging) return; 254 dragging = false; 255 panel.classList.remove('is-dragging'); 256 const dy = lastY - startY; 257 if (dy > 80) closeSheet(); 258 else panel.style.removeProperty('--pcms-drag-y'); 259 } 260 if (window.PointerEvent && dragZone) { 261 dragZone.addEventListener('pointerdown', onDown); 262 document.addEventListener('pointermove', onMove); 263 document.addEventListener('pointerup', onUp); 264 document.addEventListener('pointercancel', onUp); 265 } 266 267 // Theme toggle inside sheet — syncs label 268 function syncTheme() { 269 if (!themeLbl) return; 270 const t = document.documentElement.getAttribute('data-theme') || 'dark'; 271 // De twee labels staan op het element zelf: een module kan geen vertaling 272 // interpoleren, en zo hoort de tekst bij het ding dat hem toont. 273 themeLbl.textContent = themeLbl.getAttribute(t === 'dark' ? 'data-dark' : 'data-light') || themeLbl.textContent; 274 } 275 if (themeBtn) { 276 themeBtn.addEventListener('click', function() { 277 const cur = document.documentElement.getAttribute('data-theme') || 'dark'; 278 const next = cur === 'dark' ? 'light' : 'dark'; 279 document.documentElement.setAttribute('data-theme', next); 280 try { localStorage.setItem('pcms-theme', next); } catch (e) {} 281 syncTheme(); 282 }); 283 } 284 })();
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)