feat: slideout editor new new new!
This commit is contained in:
parent
c175a610da
commit
48ac1249c8
10 changed files with 482 additions and 112 deletions
|
|
@ -1,120 +1,195 @@
|
|||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Create the magnifying glass emoji button
|
||||
const magnifyingGlass = document.createElement("div");
|
||||
magnifyingGlass.innerHTML = "🔍";
|
||||
magnifyingGlass.className =
|
||||
"fixed bottom-2 right-2 cursor-pointer text-2xl z-50";
|
||||
document.body.appendChild(magnifyingGlass);
|
||||
(function () {
|
||||
if (window.__blissInspectorLoaded) return;
|
||||
window.__blissInspectorLoaded = true;
|
||||
|
||||
let highlighted = false;
|
||||
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 to highlight elements
|
||||
function highlightElements() {
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "inspector-modal";
|
||||
document.body.appendChild(modal);
|
||||
function visibleElements(selector) {
|
||||
return [...document.querySelectorAll(selector)].filter(
|
||||
(element) => !element.closest("[data-bliss-ui]"),
|
||||
);
|
||||
}
|
||||
|
||||
const elements = document.querySelectorAll("[data-bliss-route]");
|
||||
elements.forEach((el) => {
|
||||
if (!el.classList.contains("highlighted")) {
|
||||
el.classList.add(
|
||||
"border-2",
|
||||
"border-yellow-500",
|
||||
"highlighted",
|
||||
"p-2",
|
||||
);
|
||||
let controls = document.createElement("div");
|
||||
controls.className =
|
||||
"flex absolute gap-1 -bottom-3 right-0 inspector-controls";
|
||||
function routeElements() {
|
||||
return visibleElements(ROUTE_SELECTOR);
|
||||
}
|
||||
|
||||
// Create the edit icon
|
||||
const editIcon = document.createElement("a");
|
||||
editIcon.href = el.getAttribute("data-bliss-route");
|
||||
editIcon.innerHTML = "✏️";
|
||||
editIcon.className =
|
||||
"text-lg bg-white rounded-full p-1 shadow edit-icon";
|
||||
controls.appendChild(editIcon);
|
||||
function templateElements() {
|
||||
return visibleElements(TEMPLATE_SELECTOR);
|
||||
}
|
||||
|
||||
// Create the copy icon
|
||||
if (el.getAttribute("data-bliss-copy")) {
|
||||
const copyIcon = document.createElement("button");
|
||||
copyIcon.innerHTML = "📋";
|
||||
copyIcon.className =
|
||||
"text-lg bg-white rounded-full p-1 shadow copy-icon";
|
||||
controls.appendChild(copyIcon);
|
||||
copyIcon.addEventListener("click", () => {
|
||||
navigator.clipboard
|
||||
.writeText(el.getAttribute("data-bliss-copy"))
|
||||
.then(() => {
|
||||
copyIcon.innerHTML = "✅";
|
||||
})
|
||||
.catch((err) => {
|
||||
copyIcon.innerHTML = "❌";
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// Create the clone icon
|
||||
const cloneIcon = document.createElement("div");
|
||||
cloneIcon.innerHTML = "👯♀️";
|
||||
cloneIcon.className =
|
||||
"text-lg bg-white rounded-full cursor-pointer p-1 shadow clone-icon";
|
||||
cloneIcon.setAttribute("hx-get", el.getAttribute("data-bliss-clone"));
|
||||
cloneIcon.setAttribute("hx-target", "#inspector-modal");
|
||||
|
||||
controls.setAttribute(
|
||||
"_",
|
||||
`on mouseover set my.style.zIndex to 10000
|
||||
on mouseout set my.style.zIndex to "initial"
|
||||
`,
|
||||
);
|
||||
controls.appendChild(cloneIcon);
|
||||
|
||||
el.appendChild(controls);
|
||||
htmx.process(controls);
|
||||
_hyperscript.processNode(controls);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to remove highlights
|
||||
function removeHighlights() {
|
||||
const elements = document.querySelectorAll(
|
||||
"[data-bliss-route].highlighted",
|
||||
);
|
||||
elements.forEach((el) => {
|
||||
el.classList.remove(
|
||||
"border-2",
|
||||
"border-yellow-500",
|
||||
"relative",
|
||||
"highlighted",
|
||||
"p-2",
|
||||
);
|
||||
el.querySelectorAll(".inspector-controls").forEach((el) => el.remove());
|
||||
});
|
||||
}
|
||||
|
||||
magnifyingGlass.addEventListener("click", function () {
|
||||
if (!highlighted) {
|
||||
highlightElements();
|
||||
magnifyingGlass.innerHTML = "❌";
|
||||
} else {
|
||||
removeHighlights();
|
||||
magnifyingGlass.innerHTML = "🔍";
|
||||
}
|
||||
highlighted = !highlighted;
|
||||
return items;
|
||||
}
|
||||
|
||||
function publishInventory() {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("bliss:inventory", { detail: inventory() }),
|
||||
);
|
||||
}
|
||||
|
||||
function matches(element, { kind, id }) {
|
||||
if (kind === "template") {
|
||||
return templatesFor(element).some(
|
||||
(template) => String(template.id) === String(id),
|
||||
);
|
||||
}
|
||||
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 });
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// Observe the document for changes
|
||||
// const observer = new MutationObserver(function (mutations) {
|
||||
// if (highlighted) {
|
||||
// highlightElements();
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Configure the observer
|
||||
// observer.observe(document.body, {
|
||||
// childList: true,
|
||||
// subtree: true,
|
||||
// });
|
||||
});
|
||||
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();
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue