Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/partials/topnav.ejs

    r156baa3 r72ec6a4  
    176176
    177177<!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
    178 <%# De teksten staan op het element dat ze gebruikt, niet in het script: een
    179     module is een statisch bestand en kan geen vertaling interpoleren. %>
    180 <div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search"
    181      data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty') }) %>">
     178<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search">
    182179  <div class="container">
    183180    <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
     
    194191</div>
    195192
    196 <%# Het script van de topnav staat in assets/js/mod/chrome.js (shaer-bqr). %>
     193<script>
     194(function() {
     195  // Wired ONCE. This chrome (topnav) is re-inserted out-of-band during htmx navigation
     196  // → without this guard the script would stack EXTRA listeners on every navigation,
     197  // causing the theme toggle to fire 2× (or more) = no net change ("toggle stops working").
     198  // Everything below uses event delegation on body/document, so it also works for
     199  // buttons that appear after this run (OOB).
     200  if (window.__pcmsChromeWired) return;
     201  window.__pcmsChromeWired = true;
     202
     203  function toggleTheme() {
     204    var cur = document.documentElement.getAttribute('data-theme') || 'dark';
     205    var next = cur === 'dark' ? 'light' : 'dark';
     206    document.documentElement.setAttribute('data-theme', next);
     207    try { localStorage.setItem('pcms-theme', next); } catch (e) {}
     208  }
     209  function overlay() { return document.getElementById('search-overlay'); }
     210  function openSearch() { var o = overlay(); if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } }
     211  function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } }
     212
     213  document.body.addEventListener('click', function(e) {
     214    if (e.target.closest('#theme-toggle, #theme-toggle-mobile, #theme-toggle-footer')) { toggleTheme(); return; }
     215    if (e.target.closest('#search-toggle')) { openSearch(); return; }
     216    if (e.target.closest('#search-close')) { closeSearch(); return; }
     217    // Close open dropdowns (user menu + language picker) on click outside.
     218    document.querySelectorAll('.user-menu[open], .lang-menu[open]').forEach(function(d) {
     219      if (!d.contains(e.target)) d.removeAttribute('open');
     220    });
     221  });
     222
     223  document.addEventListener('keydown', function(e) {
     224    if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) closeSearch(); }
     225  });
     226
     227  // ── Live results while typing ───────────────────────────────────
     228  var _ST = {
     229    posts: '<%= t('search.section_posts') %>', tracks: '<%= t('search.section_tracks') %>',
     230    events: '<%= t('search.section_events') %>', pages: '<%= t('search.section_pages') %>',
     231    all: '<%= t('search.suggest_all') %>', none: '<%= t('search.suggest_empty') %>'
     232  };
     233  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
     234  function renderSuggest(box, d, q, ov) {
     235    var html = '';
     236    function grp(title, items, fmt) {
     237      if (!items || !items.length) return;
     238      html += '<div class="ss-group"><div class="ss-title">' + esc(title) + '</div>' + items.map(fmt).join('') + '</div>';
     239    }
     240    grp(_ST.posts, d.posts, function(p){ return '<a class="ss-item" href="' + esc(p.url) + '">' + esc(p.title) + '</a>'; });
     241    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>'; });
     242    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>'; });
     243    grp(_ST.pages, d.pages, function(pg){ return '<a class="ss-item" href="' + esc(pg.url) + '">' + esc(pg.label) + '</a>'; });
     244    var hasAny = (d.posts && d.posts.length) || (d.tracks && d.tracks.length) || (d.events && d.events.length) || (d.pages && d.pages.length);
     245    if (!hasAny) { box.innerHTML = '<div class="ss-empty">' + esc(_ST.none) + '</div>'; return; }
     246    var action = (ov && ov.getAttribute('data-action')) || '/search';
     247    html += '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_ST.all) + '</a>';
     248    box.innerHTML = html;
     249  }
     250  var _sTimer;
     251  document.addEventListener('input', function(e) {
     252    var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]');
     253    if (!inp) return;
     254    var box = document.getElementById('search-suggest');
     255    if (!box) return;
     256    var q = inp.value.trim();
     257    clearTimeout(_sTimer);
     258    if (q.length < 2) { box.innerHTML = ''; return; }
     259    _sTimer = setTimeout(function() {
     260      var ov = document.getElementById('search-overlay');
     261      var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
     262      fetch(url + '?q=' + encodeURIComponent(q))
     263        .then(function(r){ return r.ok ? r.json() : null; })
     264        .then(function(d){ if (d) renderSuggest(box, d, q, ov); })
     265        .catch(function(){});
     266    }, 200);
     267  });
     268  // Click on a suggestion → close the overlay (the link/boost handles navigation).
     269  document.addEventListener('click', function(e) {
     270    if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
     271  });
     272})();
     273</script>
    197274
    198275<style>
Note: See TracChangeset for help on using the changeset viewer.