bliss/public/js/bliss_inspector.js

196 lines
6.6 KiB
JavaScript
Raw Normal View History

2026-08-03 01:24:54 -04:00
(function () {
if (window.__blissInspectorLoaded) return;
window.__blissInspectorLoaded = true;
2024-06-04 03:01:19 -04:00
2026-08-03 01:24:54 -04:00
const ROUTE_SELECTOR = "[data-bliss-structure-id][data-bliss-route-id]";
const TEMPLATE_SELECTOR = "[data-bliss-template-id]";
let root = null;
let open = false;
function visibleElements(selector) {
return [...document.querySelectorAll(selector)].filter(
(element) => !element.closest("[data-bliss-ui]"),
);
}
function routeElements() {
return visibleElements(ROUTE_SELECTOR);
}
function templateElements() {
return visibleElements(TEMPLATE_SELECTOR);
}
2024-06-04 03:01:19 -04:00
2026-08-03 01:24:54 -04:00
function templatesFor(element) {
try {
const templates = JSON.parse(element.dataset.blissTemplates || "[]");
if (templates.length) return templates;
} catch (_) {}
return element.dataset.blissTemplateId
? [
{
id: element.dataset.blissTemplateId,
name: element.dataset.blissTemplateName,
},
]
: [];
}
function inventory() {
const items = [];
for (const owner of routeElements()) {
const templates = templateElements().filter(
(element) => element.closest(ROUTE_SELECTOR) === owner,
);
if (!templates.length) templates.push(null);
for (const templateElement of templates) {
const renderedTemplates = templateElement
? templatesFor(templateElement)
: [null];
for (const template of renderedTemplates) {
items.push({
structureId: owner.dataset.blissStructureId,
structureName: owner.dataset.blissStructureName,
routeId: owner.dataset.blissRouteId,
routeName: owner.dataset.blissRouteName,
templateId: template?.id || null,
templateName: template?.name || null,
});
}
2024-06-02 00:39:30 -04:00
}
2026-08-03 01:24:54 -04:00
}
return items;
2024-06-02 00:39:30 -04:00
}
2026-08-03 01:24:54 -04:00
function publishInventory() {
document.dispatchEvent(
new CustomEvent("bliss:inventory", { detail: inventory() }),
2024-06-04 03:01:19 -04:00
);
2026-08-03 01:24:54 -04:00
}
function matches(element, { kind, id }) {
if (kind === "template") {
return templatesFor(element).some(
(template) => String(template.id) === String(id),
2024-06-04 03:01:19 -04:00
);
2026-08-03 01:24:54 -04:00
}
const field = {
structure: "blissStructureId",
route: "blissRouteId",
}[kind];
return field && element.dataset[field] === String(id);
}
function highlight(detail) {
const candidates =
detail.kind === "template" ? templateElements() : routeElements();
for (const element of candidates.filter((el) => matches(el, detail))) {
element.classList.toggle("bliss-source-highlight", detail.on);
}
}
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" } });
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);
return;
}
await htmx.ajax("GET", url, { target: element, swap: "outerHTML" });
}
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 });
2024-06-02 00:39:30 -04:00
});
2026-08-03 01:24:54 -04:00
const settled = await Promise.allSettled(elements.map(refreshElement));
const failure = settled.find((result) => result.status === "rejected");
if (failure) console.error("Bliss live refresh failed", failure.reason);
publishInventory();
2024-06-02 00:39:30 -04:00
}
2026-08-03 01:24:54 -04:00
function show() {
if (open) return;
open = true;
document.documentElement.classList.add("bliss-inspector-open");
root = document.createElement("div");
root.id = "bliss-inspector-root";
root.dataset.blissUi = "";
root.setAttribute("hx-get", "/_bliss/inspector");
root.setAttribute("hx-trigger", "load");
root.setAttribute("hx-swap", "innerHTML");
document.body.appendChild(root);
htmx.process(root);
}
function hide() {
open = false;
document.documentElement.classList.remove("bliss-inspector-open");
root?.remove();
root = null;
for (const element of [...routeElements(), ...templateElements()]) {
element.classList.remove("bliss-source-highlight");
2024-06-02 00:39:30 -04:00
}
2026-08-03 01:24:54 -04:00
}
document.addEventListener("DOMContentLoaded", () => {
const style = document.createElement("style");
style.dataset.blissUi = "";
style.textContent = `
html.bliss-inspector-open body > :not([data-bliss-ui]) {
transform: translateX(min(28rem, 90vw));
max-width: calc(100vw - min(28rem, 90vw));
}
[data-bliss-structure-id], [data-bliss-template-id] {
transition: outline-color 80ms linear, background-color 80ms linear;
}
.bliss-source-highlight {
outline: 4px solid #ff00a8 !important;
outline-offset: 3px !important;
background-color: rgba(255, 0, 168, .08) !important;
}
`;
document.head.appendChild(style);
const trigger = document.createElement("button");
trigger.type = "button";
trigger.textContent = "🔍";
trigger.title = "Open Bliss live editor";
trigger.dataset.blissUi = "";
trigger.style.cssText =
"position:fixed;right:.5rem;bottom:.5rem;z-index:2147483647;border:0;background:white;border-radius:999px;padding:.45rem;font-size:1.25rem;cursor:pointer;box-shadow:0 2px 8px #0004";
trigger.onclick = () => (open ? hide() : show());
document.body.appendChild(trigger);
2024-06-02 00:39:30 -04:00
});
2026-08-03 01:24:54 -04:00
document.addEventListener("bliss:highlight", (event) => highlight(event.detail));
document.addEventListener("bliss:source-saved", (event) => refreshSources(event.detail));
document.addEventListener("bliss:inventory-request", publishInventory);
document.addEventListener("bliss:inspector-close", hide);
document.addEventListener("htmx:afterSwap", () => {
if (open) publishInventory();
});
})();