Ignore:
File:
1 edited

Legend:

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

    r156baa3 r1e172f3  
    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.