130 lines
4.1 KiB
JavaScript
130 lines
4.1 KiB
JavaScript
/**
|
|
* 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 });
|
|
}
|
|
})();
|