Changeset 156baa3 in Klonkt for src/assets/js/mod/chrome.js


Ignore:
Timestamp:
08/07/2026 01:38:51 PM (5 weeks ago)
Author:
roboburr <roboburr@…>
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)
Message:

De chrome en vier schermen met servergegevens (shaer-bqr, stap 3e)

topnav en profile-sheet naar mod/chrome.js; admin-media, admin-videos, paid-gate,
paid-passkey en admin-playlists naar eigen modules.

TWEE MANIEREN om servergegevens bij een module te krijgen, en de keuze is niet
willekeurig:

OP HET ELEMENT waar de tekst bij EEN ding hoort. De zoekteksten staan nu op

#search-overlay, de twee themalabels op het label zelf. Dat is
ook robuuster: bij een htmx-navigatie wordt de chrome
vervangen, en dan komt de tekst gewoon mee.

VIA pageData() waar het om paginabrede gegevens gaat (csrf, slug, een blob,

een setje meldingen).

De chrome kon niet via pageData: die leeft langer dan een pagina.

DRIE FOUTEN ONDERWEG, alle drie gevangen voordat er iets stuk ging:

  1. Ik zocht de eerste </script> in het HELE bestand in plaats van die na het openende tag. paid-gate en paid-passkey hebben een <script src> erboven, dus het "blok" werd leeg en er werd een lege module geschreven. Teruggezet uit git en de zoekfunctie kreeg een offset plus een controle dat het blok niet leeg is.
  2. Een zoekterm sloot te vroeg af: de tekst van delete_failed loopt door in dezelfde JS-string (": " + err.message), dus het patroon met de sluitquote matchte niet.
  3. Een eerdere poging verving nul regels en schreef toch. Nu telt de helper de vervangingen en weigert als het er niet exact zoveel zijn als opgegeven.

Die controle op "geen enkele module bevat nog EJS" draait mee, en er is er nu een
bij die een verdacht lege module afvangt -- precies wat fout 1 opleverde.

Templates compileren, suite 565/565.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/chrome.js

    r52fc278 r156baa3  
    100100  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
    101101})();
     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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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.