source: Klonkt/src/assets/js/mod/chrome.js@ 54ee51c

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

De profielkop, de track-editor en het data-kanaal (shaer-bqr, stap 3c)

PROFIELKOP naar mod/chrome.js: chrome.ejs neemt die partial op, dus hij zit in
de OOB-swap en kwam bij elke navigatie opnieuw binnen. Hoort bij de chrome, niet
bij een pagina.

TRACK-EDITOR (475 regels, geen interpolatie) naar een eigen module. admin-audio
neemt hem op, dus die route vraagt om 'admin-audio track-editor' -- een pagina
mag meer dan een module willen.

HET DATA-KANAAL, want de rest heeft het nodig. Een module is een statisch
bestand, dus er kan geen EJS in; alles wat een script van de server nodig heeft
gaat via partials/page-data.ejs en wordt gelezen met pageData() uit mod/lib.js.

Dat is een script-element en toch geen CSP-probleem: een script met een ander
type dan JavaScript wordt niet uitgevoerd en niet opgehaald, dus script-src heeft
er niets over te zeggen. Het werkt daarmee ook in inhoud die via htmx binnenkomt.

Twee dingen die daar misgaan als je ze niet doet, en allebei nagemeten:

De kleiner-dan wordt ontsnapt. Zonder dat sluit een sluitende script-tag IN de
gegevens het blok voortijdig af en staat de rest als HTML op de pagina.
Getoetst met precies zo'n waarde.

pageData() leest ELKE KEER opnieuw en onthoudt niets. Bij een htmx-navigatie
wisselt de inhoud terwijl de module blijft leven, dus een blok dat je bij het
laden vasthoudt is een pagina later verouderd.

Alle templates compileren.

  • Property mode set to 100644
File size: 5.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})();
Note: See TracBrowser for help on using the repository browser.