fix: restore inspector template hot reload
This commit is contained in:
parent
cbbb87cd2e
commit
ffba038b35
7 changed files with 232 additions and 35 deletions
|
|
@ -36,6 +36,14 @@
|
|||
: [];
|
||||
}
|
||||
|
||||
function contextIdFor(element, templateId) {
|
||||
try {
|
||||
const ids = JSON.parse(element.dataset.blissTemplateContextIds || "{}");
|
||||
if (Object.hasOwn(ids, String(templateId))) return ids[String(templateId)];
|
||||
} catch (_) {}
|
||||
return element.dataset.blissTemplateContextId || null;
|
||||
}
|
||||
|
||||
function inventory() {
|
||||
const items = [];
|
||||
for (const owner of routeElements()) {
|
||||
|
|
@ -89,44 +97,107 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function replaceBody(html) {
|
||||
const ui = [...document.body.querySelectorAll(":scope > [data-bliss-ui]")];
|
||||
const next = new DOMParser().parseFromString(html, "text/html");
|
||||
for (const attribute of [...document.body.attributes]) {
|
||||
document.body.removeAttribute(attribute.name);
|
||||
}
|
||||
for (const attribute of [...next.body.attributes]) {
|
||||
document.body.setAttribute(attribute.name, attribute.value);
|
||||
}
|
||||
document.body.innerHTML = next.body.innerHTML;
|
||||
for (const node of ui) document.body.appendChild(node);
|
||||
htmx.process(document.body);
|
||||
}
|
||||
|
||||
async function refreshElement(element) {
|
||||
if (element.dataset.blissMethod !== "GET") return;
|
||||
const url = element.dataset.blissRequestUrl;
|
||||
if (!url) return;
|
||||
|
||||
if (element === document.body) {
|
||||
const ui = [...document.body.querySelectorAll(":scope > [data-bliss-ui]")];
|
||||
const response = await fetch(url, { headers: { "HX-Request": "true" } });
|
||||
// A page root must be fetched as a document. Asking for an HTMX
|
||||
// fragment here used to parse an empty <body> and erase the page.
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
|
||||
const next = new DOMParser().parseFromString(await response.text(), "text/html");
|
||||
for (const attribute of [...document.body.attributes]) {
|
||||
document.body.removeAttribute(attribute.name);
|
||||
}
|
||||
for (const attribute of [...next.body.attributes]) {
|
||||
document.body.setAttribute(attribute.name, attribute.value);
|
||||
}
|
||||
document.body.innerHTML = next.body.innerHTML;
|
||||
for (const node of ui) document.body.appendChild(node);
|
||||
htmx.process(document.body);
|
||||
await replaceBody(await response.text());
|
||||
return;
|
||||
}
|
||||
|
||||
await htmx.ajax("GET", url, { target: element, swap: "outerHTML" });
|
||||
}
|
||||
|
||||
function outermost(elements, selector) {
|
||||
const set = new Set(elements);
|
||||
return elements.filter((element) => {
|
||||
let parent = element.parentElement;
|
||||
while (parent) {
|
||||
if (set.has(parent) && parent.matches(selector)) return false;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshTemplateElement(element, templateId, previousRoots = []) {
|
||||
const response = await fetch("/_bliss/render-template", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
structureId: element.closest(ROUTE_SELECTOR)?.dataset.blissStructureId,
|
||||
templateId,
|
||||
contextId: contextIdFor(element, templateId),
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Template ${templateId} returned ${response.status}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
if (element === document.body) return replaceBody(html);
|
||||
const fragment = document.createElement("template");
|
||||
fragment.innerHTML = html;
|
||||
// A template may legally have several top-level nodes. They share one
|
||||
// captured context id, so replace that one render as a group instead of
|
||||
// rendering the same template once per root and duplicating its output.
|
||||
for (const root of previousRoots) {
|
||||
if (root !== element && root.isConnected) root.remove();
|
||||
}
|
||||
element.replaceWith(fragment.content);
|
||||
htmx.process(document.body);
|
||||
}
|
||||
|
||||
function templateRefreshGroups(templateId) {
|
||||
const elements = outermost(
|
||||
templateElements().filter((element) =>
|
||||
matches(element, { kind: "template", id: templateId }),
|
||||
),
|
||||
TEMPLATE_SELECTOR,
|
||||
);
|
||||
const groups = new Map();
|
||||
for (const element of elements) {
|
||||
const contextId = contextIdFor(element, templateId);
|
||||
const key = contextId || `element:${Math.random()}`;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(element);
|
||||
}
|
||||
return [...groups.values()];
|
||||
}
|
||||
|
||||
async function refreshSources({ kind, id, method }) {
|
||||
if (method && method !== "GET") return;
|
||||
const elements = routeElements().filter((element) => {
|
||||
if (kind === "template") {
|
||||
return templateElements().some(
|
||||
(template) =>
|
||||
matches(template, { kind: "template", id }) &&
|
||||
template.closest(ROUTE_SELECTOR) === element,
|
||||
);
|
||||
}
|
||||
return matches(element, { kind, id });
|
||||
});
|
||||
const settled = await Promise.allSettled(elements.map(refreshElement));
|
||||
const elements =
|
||||
kind === "template"
|
||||
? templateRefreshGroups(id)
|
||||
: outermost(
|
||||
routeElements().filter((element) => matches(element, { kind, id })),
|
||||
ROUTE_SELECTOR,
|
||||
);
|
||||
const refresh =
|
||||
kind === "template"
|
||||
? (group) => refreshTemplateElement(group[0], id, group)
|
||||
: refreshElement;
|
||||
const settled = await Promise.allSettled(elements.map(refresh));
|
||||
const failure = settled.find((result) => result.status === "rejected");
|
||||
if (failure) console.error("Bliss live refresh failed", failure.reason);
|
||||
publishInventory();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue