Ignore:
File:
1 edited

Legend:

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

    r1e172f3 r156baa3  
    4141  });
    4242})();
     43
     44// ── de profielkop ──────────────────────────────────────────────
     45// Zat inline in partials/profile-header.ejs, en die partial zit IN de
     46// OOB-chrome -- dus hij kwam bij elke navigatie opnieuw binnen.
     47(function () {
     48  if (window.__pcmsFediFollowWired) return;
     49  window.__pcmsFediFollowWired = true;
     50  function modal() { return document.querySelector('.pf-follow'); }
     51  function closeM() { var m = modal(); if (m) m.classList.remove('is-open'); }
     52  function go() {
     53    var m = modal(); if (!m) return;
     54    var inp = m.querySelector('.pf-follow-input'), raw = inp ? inp.value : '';
     55    // Strip a full @user@host handle (and any scheme/path) down to the server host.
     56    var server = String(raw || '').trim().replace(/^@?[^@\s]*@/, '').replace(/^https?:\/\//i, '').replace(/\/.*$/, '').trim();
     57    if (!server) { if (inp) inp.focus(); return; }
     58    try { localStorage.setItem('pcmsFediServer', server); } catch (e) {}
     59    var actor = location.origin + '/ap/users/' + encodeURIComponent(m.getAttribute('data-actor-slug') || '');
     60    location.href = 'https://' + server + '/authorize_interaction?uri=' + encodeURIComponent(actor);
     61  }
     62  document.addEventListener('click', function (e) {
     63    var b = e.target.closest && e.target.closest('.profile-fedi-link');
     64    if (b) { e.preventDefault();
     65      var m = modal(); if (!m) return;
     66      m.setAttribute('data-actor-slug', b.getAttribute('data-fedi-actor-slug') || '');
     67      var inp = m.querySelector('.pf-follow-input');
     68      try { if (inp && !inp.value) inp.value = localStorage.getItem('pcmsFediServer') || ''; } catch (e2) {}
     69      m.classList.add('is-open');
     70      setTimeout(function () { if (inp) inp.focus(); }, 30);
     71      return;
     72    }
     73    if (e.target.closest && e.target.closest('.pf-follow-go')) { e.preventDefault(); go(); return; }
     74    if (e.target.closest && e.target.closest('.pf-follow-cancel')) { e.preventDefault(); closeM(); return; }
     75    var open = document.querySelector('.pf-follow.is-open');
     76    if (open && e.target === open) closeM(); /* click on backdrop */
     77  });
     78  document.addEventListener('keydown', function (e) {
     79    var m = document.querySelector('.pf-follow.is-open'); if (!m) return;
     80    if (e.key === 'Escape') { closeM(); }
     81    else if (e.key === 'Enter' && e.target.closest && e.target.closest('.pf-follow')) { e.preventDefault(); go(); }
     82  });
     83})();
     84
     85/* Profile summary: click the avatar to open a modal with photo + details. */
     86(function () {
     87  if (window.__pcmsLightboxWired) return; window.__pcmsLightboxWired = true;
     88  function close() { var m = document.querySelector('.pf-summary.is-open'); if (m) m.classList.remove('is-open'); }
     89  document.addEventListener('click', function (e) {
     90    if (e.target.closest && e.target.closest('.pf-summary-close')) { e.preventDefault(); close(); return; }
     91    var open = document.querySelector('.pf-summary.is-open');
     92    if (open && e.target === open) { close(); return; } /* click on backdrop */
     93    var t = e.target.closest && e.target.closest('.profile-photo[data-pf-summary]');
     94    if (!t) return;
     95    e.preventDefault();
     96    var root = (t.closest && t.closest('#pcms-chrome')) || document;
     97    var modal = root.querySelector('.pf-summary') || document.querySelector('.pf-summary');
     98    if (modal) modal.classList.add('is-open');
     99  });
     100  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
     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 {'&':'&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.