/** * htmx-reinit.js — re-initialise non-HTMX scripts after every HTMX swap. * * In the htmx-ssr rendering profile, scripts like highlight.js, cytoscape, * theme-init, and tag-filters run once at page load. When HTMX swaps a * fragment in, those scripts don't see the new DOM and the content goes * "dead" (unhighlighted code blocks, unwired filter pills, etc.). * * This module fixes that by exposing a registry — * `window.RusteloReinit.register(fn)` — and a single listener on * `htmx:afterSettle` that walks the registry and calls each handler with * the swapped element as scope. * * Handlers MUST be idempotent. We help by tracking processed nodes in a * WeakSet keyed by handler id, so double-decoration is impossible even if * a swap re-emits a region that contains an already-processed node. * * Usage from another script: * * (function () { * window.RusteloReinit.register('highlight', function (scope) { * scope.querySelectorAll('pre code:not(.hljs)').forEach(hljs.highlightElement); * }); * })(); * * The registered name is used to namespace the processed-nodes WeakSet so * each handler manages its own idempotency. */ (function () { 'use strict'; if (window.RusteloReinit) { return; // already loaded } /** @type {Array<{ name: string, fn: (scope: Element) => void, seen: WeakSet }>} */ const handlers = []; function findHandler(name) { for (let i = 0; i < handlers.length; i++) { if (handlers[i].name === name) { return handlers[i]; } } return null; } function runHandler(handler, scope) { if (handler.seen.has(scope)) { return; } handler.seen.add(scope); try { handler.fn(scope); } catch (err) { console.error('[RusteloReinit:' + handler.name + '] handler failed', err); } } const api = { /** * Register a re-init handler. Calling register twice with the same * name replaces the previous handler. */ register: function (name, fn) { if (typeof name !== 'string' || typeof fn !== 'function') { throw new TypeError('RusteloReinit.register(name, fn) — name must be string, fn must be function'); } const existing = findHandler(name); if (existing) { existing.fn = fn; // Reset the seen set so the replacement gets a fresh chance at the DOM. existing.seen = new WeakSet(); } else { handlers.push({ name: name, fn: fn, seen: new WeakSet() }); } // Run immediately on document.body so the initial page gets processed. if (document.body) { runHandler(findHandler(name), document.body); } else { document.addEventListener('DOMContentLoaded', function () { runHandler(findHandler(name), document.body); }, { once: true }); } }, /** * Force-run every handler against the given scope. Used by tests and by * the htmx:afterSettle listener. */ runAll: function (scope) { for (let i = 0; i < handlers.length; i++) { runHandler(handlers[i], scope); } }, /** Return registered handler names (debug). */ names: function () { return handlers.map(function (h) { return h.name; }); } }; window.RusteloReinit = api; // Hook into htmx lifecycle events. afterSettle fires after the DOM is in // its final state for the swap; we want this rather than afterSwap to // avoid running against elements that get replaced moments later. document.body && document.body.addEventListener('htmx:afterSettle', function (ev) { const target = ev.detail && ev.detail.target; if (target instanceof Element) { api.runAll(target); } else { api.runAll(document.body); } }); // If body wasn't ready yet, wire on DOMContentLoaded. if (!document.body) { document.addEventListener('DOMContentLoaded', function () { document.body.addEventListener('htmx:afterSettle', function (ev) { const target = ev.detail && ev.detail.target; if (target instanceof Element) { api.runAll(target); } else { api.runAll(document.body); } }); }, { once: true }); } })();