Changeset 4b5db37 in Klonkt


Ignore:
Timestamp:
08/07/2026 06:33:54 PM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
da84110
Parents:
c1df3e9
git-author:
Robin <roboburr@…> (08/07/2026 06:33:41 PM)
git-committer:
Robin <roboburr@…> (08/07/2026 06:33:54 PM)
Message:

Modules herstarten per paginawissel: de editor leeft weer na een terugkeer (shaer-5s1)

Drie klachten, een wortel. De oude inline scripts draaiden bij ELKE render;
een ES-module draait zijn top-level EEN keer per sessie, en de bootstrap
importeert een geladen module nooit opnieuw. Dus:

  • /posts/new na een htmx-terugkeer: geen toolbar, geen serialisatie -- het verborgen veld kreeg de chips nooit terug (shortcodes 'opgegeten')
  • /admin/audio na een terugkeer: de eigen kopieerhandler dood, en de klik viel door naar de GEDELEGEERDE handler van admin-media, die voor mediapaden terecht location.origin voorplakt -- vandaar https://site[[track:uuid]] op het klembord

De afspraak is nu: een module die per render moet draaien exporteert init(),
en de bootstrap roept die aan bij elke wissel waarop de module actief is.
Modules zonder init houden hun oude gedrag. Listeners op document/window
overleven de swap met closures naar dode elementen; makeSweeper in lib.js
veegt bij elke init de vorige lichting weg. Gedelegeerde handlers die
bewust blijven leven (media/videos) krijgen een paginawacht, want data-copy
betekent daar een PAD en elders een shortcode.

Omgezet: post-edit, admin-audio, admin-playlists. Bewaakt: admin-media,
admin-videos. playlist-editor en track-editor waren al swap-bestendig
(globale functie, gedelegeerd met eigen guards).

Co-Authored-By: Claude Opus 5 <noreply@…>

Location:
src
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/admin-audio.js

    rc1df3e9 r4b5db37  
    55// link BINNEN de site binnenkomt (shaer-0i6).
    66
    7 import { pageData } from './lib.js';
    8 
    9   const T = pageData();
     7import { pageData, makeSweeper } from './lib.js';
     8
     9// Zie post-edit.js: init() per paginawissel, de veger haalt de window-
     10// listeners van de vorige lichting weg (shaer-5s1).
     11const doc = makeSweeper();
     12let T = {};
     13
     14export function init() {
     15  doc.sweep();
     16  T = pageData();
     17  run();
     18}
     19
     20function run() {
    1021
    1122(function() {
     
    197208  // explicitly handles them.
    198209  ['dragover', 'drop'].forEach(ev => {
    199     window.addEventListener(ev, e => {
     210    doc.on(window, ev, e => {
    200211      // Allow drops INSIDE the dropzone — its own listener handles those.
    201212      if (dropzone.contains(e.target)) return;
     
    441452  });
    442453})();
     454}
  • src/assets/js/mod/admin-media.js

    rc1df3e9 r4b5db37  
    99(function () {
    1010  if (window.__mediaWired) return; window.__mediaWired = true;
    11   var T = pageData();
     11  // Gedelegeerd op document, dus dit overleeft elke paginawissel -- ook op
     12  // pagina's waar [data-copy] iets ANDERS betekent (audio en playlists: een
     13  // shortcode, geen pad). Zonder deze wacht kaapt dit daar de kopieerklik en
     14  // plakt er een origin voor (shaer-5s1). T per klik gelezen, want pageData
     15  // wisselt mee met de pagina.
     16  function active() { return (document.body.getAttribute('data-js') || '').split(/\s+/).indexOf('admin-media') !== -1; }
    1217  document.addEventListener('click', function (e) {
     18    if (!active()) return;
     19    var T = pageData();
    1320    var c = e.target.closest('[data-copy]');
    1421    if (c) {
  • src/assets/js/mod/admin-playlists.js

    rc1df3e9 r4b5db37  
    66import { pageData } from './lib.js';
    77
     8// Element-bedrading per render, dus init() per paginawissel (shaer-5s1).
     9export function init() { run(); }
     10
     11function run() {
    812(function() {
    913  const _d = pageData();
     
    7478  })();
    7579})();
     80}
  • src/assets/js/mod/admin-videos.js

    rc1df3e9 r4b5db37  
    99(function () {
    1010  if (window.__videosWired) return; window.__videosWired = true;
    11   var T = pageData();
     11  // Gedelegeerd op document, dus dit overleeft elke paginawissel -- ook op
     12  // pagina's waar [data-copy] iets ANDERS betekent (audio en playlists: een
     13  // shortcode, geen pad). Zonder deze wacht kaapt dit daar de kopieerklik en
     14  // plakt er een origin voor (shaer-5s1). T per klik gelezen, want pageData
     15  // wisselt mee met de pagina.
     16  function active() { return (document.body.getAttribute('data-js') || '').split(/\s+/).indexOf('admin-videos') !== -1; }
    1217  document.addEventListener('click', function (e) {
     18    if (!active()) return;
     19    var T = pageData();
    1320    var c = e.target.closest('[data-copy]');
    1421    if (c) {
  • src/assets/js/mod/lib.js

    rc1df3e9 r4b5db37  
    3535
    3636/**
     37 * Listeners op document en window overleven een paginawissel; de pagina zelf
     38 * niet. Een module die per navigatie opnieuw start (export init) registreert
     39 * zulke listeners hier, en veegt bij elke start de vorige lichting weg --
     40 * anders stapelen ze, elk met een closure naar elementen die al weg zijn.
     41 *
     42 * Listeners op ELEMENTEN hoeven hier niet doorheen: die sterven met hun
     43 * element bij de swap. Alleen wat het document overleeft, moet geveegd.
     44 */
     45export function makeSweeper() {
     46  let list = [];
     47  return {
     48    on(target, ev, fn, opts) { target.addEventListener(ev, fn, opts); list.push([target, ev, fn, opts]); },
     49    sweep() { for (const [t, ev, fn, opts] of list) t.removeEventListener(ev, fn, opts); list = []; },
     50  };
     51}
     52
     53/**
    3754 * Een tekst veilig in HTML zetten.
    3855 *
  • src/assets/js/mod/post-edit.js

    rc1df3e9 r4b5db37  
    55// het houdt een apostrof in een vertaling uit de HTML die hier geplakt wordt.
    66
    7 import { pageData, esc } from './lib.js';
    8 
    9 const T = pageData();
     7import { pageData, esc, makeSweeper } from './lib.js';
     8
     9// De oude inline scripts draaiden bij ELKE render; een module draait zijn
     10// top-level een keer per sessie. Vandaar init(): de bootstrap roept hem aan
     11// bij elke paginawissel waarop deze module actief is, en de veger haalt eerst
     12// de document/window-listeners van de vorige pagina weg -- die overleven de
     13// swap, met closures naar elementen die al verdwenen zijn (shaer-5s1).
     14const doc = makeSweeper();
     15let T = {};
     16let _barObserver = null;
     17
     18export function init() {
     19  doc.sweep();
     20  if (_barObserver) { _barObserver.disconnect(); _barObserver = null; }
     21  T = pageData();
     22  run();
     23}
     24
     25function run() {
    1026
    1127          (function () {
     
    631647  }
    632648  function toggleFullscreen() { if (isFs()) closeFs(); else openFs(); }
    633   window.addEventListener('popstate', function () { if (isFs()) applyFs(false); });
     649  doc.on(window, 'popstate', function () { if (isFs()) applyFs(false); });
    634650  if (fsBtn) fsBtn.addEventListener('click', toggleFullscreen);
    635651  var fsDoneBtn = document.getElementById('pe-fs-done');
    636652  if (fsDoneBtn) fsDoneBtn.addEventListener('click', closeFs);
    637   document.addEventListener('keydown', (e) => {
     653  doc.on(document, 'keydown', (e) => {
    638654    if (e.key === 'Escape' && isFs()) { e.preventDefault(); closeFs(); }
    639655  });
     
    663679    if (bqBtn) bqBtn.classList.toggle('is-active', !!blockquoteAncestor());
    664680  }
    665   document.addEventListener('selectionchange', () => {
     681  doc.on(document, 'selectionchange', () => {
    666682    if (document.activeElement === editor) updateToolbarState();
    667683  });
     
    884900      }
    885901    });
    886     document.addEventListener('keydown', (e) => {
     902    doc.on(document, 'keydown', (e) => {
    887903      if (!trackPicker.hidden && e.key === 'Escape') {
    888904        e.preventDefault();
     
    10921108  }
    10931109  position();
    1094   window.addEventListener('resize', position);
    1095   if (vv) { vv.addEventListener('resize', position); vv.addEventListener('scroll', position); }
    1096   // has-audio-player is toggled via a body class → observe it.
    1097   try { new MutationObserver(position).observe(document.body, { attributes: true, attributeFilter: ['class'] }); } catch (_) {}
     1110  doc.on(window, 'resize', position);
     1111  if (vv) { doc.on(vv, 'resize', position); doc.on(vv, 'scroll', position); }
     1112  // has-audio-player is toggled via a body class → observe it. De observer
     1113  // overleeft de swap net als de listeners; init() disconnect de vorige.
     1114  try { _barObserver = new MutationObserver(position); _barObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); } catch (_) {}
    10981115})();
     1116}
  • src/views/shell.ejs

    rc1df3e9 r4b5db37  
    490490  window.__modBoot = true;
    491491
     492  // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
     493  // die `init` exporteert draait die bij ELKE paginawissel waarop hij actief
     494  // is -- dat is het gedrag van de oude inline scripts, die per render
     495  // draaiden. Een module zonder init draait alleen zijn top-level, een keer.
    492496  var loaded = {};
     497  function start(name, m) {
     498    if (!m || typeof m.init !== 'function') return;
     499    try { m.init(); } catch (e) { console.warn('[mod] ' + name + ' init:', e && e.message); }
     500  }
    493501  function load() {
    494502    var names = (document.body.getAttribute('data-js') || '').trim().split(/\s+/);
     
    497505      // kleine letters, cijfers en streepjes; nooit een punt of een schuine
    498506      // streep.
    499       if (!name || loaded[name] || !/^[a-z0-9-]+$/.test(name)) return;
     507      if (!name || !/^[a-z0-9-]+$/.test(name)) return;
     508      if (loaded[name]) { start(name, loaded[name]); return; }
    500509      loaded[name] = 1;
    501       import('/assets/js/mod/' + name + '.js').catch(function (e) {
     510      import('/assets/js/mod/' + name + '.js').then(function (m) {
     511        loaded[name] = m;
     512        start(name, m);
     513      }).catch(function (e) {
    502514        console.warn('[mod] ' + name + ' laadde niet:', e && e.message);
    503515      });
Note: See TracChangeset for help on using the changeset viewer.