source: Klonkt/src/assets/js/mod/chrome.js@ 156baa3

main
Last change on this file since 156baa3 was 156baa3, checked in by roboburr <roboburr@…>, 5 weeks ago

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.

  • Property mode set to 100644
File size: 14.3 KB
Line 
1// De chrome: alles wat om elke pagina heen staat (shaer-bqr).
2//
3// Dit stond als inline <script> in de partials. Dat kan niet blijven: de
4// CSP-nonce rouleert per verzoek, en de chrome wordt bij ELKE htmx-navigatie
5// out-of-band opnieuw ingevoegd. Zo'n script draagt dan een nonce die het
6// document niet kent en wordt geweigerd -- dus viel de chrome-JS bij de eerste
7// klik binnen de site al weg (shaer-0i6).
8//
9// Nu een module, geladen door de bootstrap in shell.ejs. Die zit in het
10// document zelf, heeft dus wel de goede nonce, en een dynamische import vanuit
11// een vertrouwd script is precies waar 'strict-dynamic' voor is.
12//
13// TWEE REGELS voor alles wat hier bij komt:
14//
15// GEDELEGEERD luister op document, nooit op een element dat er nu staat. De
16// chrome wordt vervangen, dus een vastgehouden verwijzing is na
17// een navigatie een verwijzing naar iets dat weg is.
18// IDEMPOTENT de module kan een tweede keer geladen worden. Een slot op
19// window voorkomt dat er een tweede stel luisteraars bij komt --
20// dat is hoe de themaknop ooit twee keer vuurde en dus niets deed.
21//
22// En: GEEN servergegevens in deze code. Interpolatie hoort niet in een statisch
23// bestand; wat de server wil meegeven komt via een data-attribuut op een element.
24
25(function () {
26 'use strict';
27 if (window.__chromeMod) return;
28 window.__chromeMod = true;
29
30 // De zoekknop in de bottom-tab opent de overlay. De overlay zelf wordt bij een
31 // htmx-navigatie vervangen, dus hem hier vasthouden zou na een klik binnen de
32 // site niets meer opleveren: elke keer opnieuw opzoeken.
33 document.addEventListener('click', function (e) {
34 if (!e.target.closest || !e.target.closest('#bottom-tab-search-toggle')) return;
35 var o = document.getElementById('search-overlay');
36 if (o) {
37 o.hidden = false;
38 var i = o.querySelector('input');
39 if (i) i.focus();
40 }
41 });
42})();
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 TracBrowser for help on using the repository browser.