Files
WRNexusJS/examples/component-showcase/public/playground.js
T
2026-08-01 01:09:58 +05:30

386 lines
14 KiB
JavaScript

/* global AbortController, DOMParser, HTMLFormElement, HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, MutationObserver, Node, URL, URLSearchParams, clearTimeout, document, fetch, navigator, setTimeout, window */
(() => {
const SELECTOR = "[data-playground]";
const DESIGN_DEFAULTS = {
style: "default",
palette: "violet",
mode: "system",
font: "jakarta",
scale: "default",
};
const htmlPolicy = window.trustedTypes?.createPolicy("wrnexus-playground", {
createHTML: (value) => value,
});
let request;
let timer;
function storedDesign() {
const root = document.documentElement.dataset;
return {
style: root.uiStyle || DESIGN_DEFAULTS.style,
palette: root.uiPalette || DESIGN_DEFAULTS.palette,
mode: root.uiMode || DESIGN_DEFAULTS.mode,
font: root.uiFont || DESIGN_DEFAULTS.font,
scale: root.uiScale || DESIGN_DEFAULTS.scale,
};
}
function setCookie(name, value) {
document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=31536000;samesite=lax`;
}
function applyDesign(settings, persist = true) {
const root = document.documentElement;
root.dataset.uiStyle = settings.style;
root.dataset.uiPalette = settings.palette;
root.dataset.uiMode = settings.mode;
root.dataset.uiFont = settings.font;
root.dataset.uiScale = settings.scale;
const dark =
settings.mode === "dark" ||
(settings.mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
root.dataset.theme = dark ? "dark" : "light";
for (const control of document.querySelectorAll("[data-design-setting]")) {
control.value = settings[control.dataset.designSetting] || "";
}
if (persist) {
setCookie("wrn-ui-style", settings.style);
setCookie("wrn-ui-palette", settings.palette);
setCookie("wrn-ui-mode", settings.mode);
setCookie("wrn-ui-font", settings.font);
setCookie("wrn-ui-scale", settings.scale);
setCookie("wire-theme", dark ? "dark" : "light");
}
}
let design = storedDesign();
applyDesign(design, false);
for (const link of document.querySelectorAll(".docs-directory a[href]")) {
const target = new URL(link.href, window.location.origin);
if (target.pathname === window.location.pathname) link.setAttribute("aria-current", "page");
}
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if (design.mode === "system") applyDesign(design);
});
function sourceAttribute(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll('"', """)
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function updateCode(playground) {
const form = playground.querySelector("[data-playground-form]");
const code = playground.querySelector("[data-playground-code]");
if (!(form instanceof HTMLFormElement) || !code) return;
const component = playground.getAttribute("data-playground-component") || "Component";
const publicTag = playground.getAttribute("data-playground-public-tag") === "true";
const hasSlot = playground.getAttribute("data-playground-has-slot") === "true";
const attributes = [];
for (const field of form.elements) {
if (
!(
field instanceof HTMLInputElement ||
field instanceof HTMLTextAreaElement ||
field instanceof HTMLSelectElement
) ||
!field.name.startsWith("pg_")
)
continue;
const name = field.name.slice(3);
if (field instanceof HTMLInputElement && field.type === "checkbox") {
attributes.push(`${name}="${field.checked}"`);
} else if (field.value !== "") {
const value = field.hasAttribute("data-playground-json")
? `'${field.value.replaceAll("'", "\\'")}'`
: `"${field.value.replaceAll('"', '\\"')}"`;
attributes.push(`${name}=${value}`);
}
}
const tag = publicTag ? component : "div";
const mount = publicTag ? [] : [`data-component="${sourceAttribute(component)}"`];
const lines = [...mount, ...attributes].map((attribute) => ` ${attribute}`).join("\n");
const opening = lines ? `<${tag}\n${lines}` : `<${tag}`;
code.textContent = hasSlot
? `${opening}>\n <!-- Add slot content here -->\n</${tag}>`
: `${opening}\n/>`;
}
function valuesFrom(form) {
const params = new URLSearchParams();
let valid = true;
for (const field of form.elements) {
if (!(
field instanceof HTMLInputElement ||
field instanceof HTMLTextAreaElement ||
field instanceof HTMLSelectElement
))
continue;
if (!field.name) continue;
const error = field.closest(".playground-field")?.querySelector("[data-playground-error]");
if (field.hasAttribute("data-playground-json")) {
try {
JSON.parse(field.value);
field.removeAttribute("aria-invalid");
if (error) error.textContent = "";
} catch {
field.setAttribute("aria-invalid", "true");
if (error) error.textContent = "Enter valid JSON";
valid = false;
continue;
}
}
params.set(field.name, field.type === "checkbox" ? String(field.checked) : field.value);
}
return valid ? params : null;
}
async function update(playground) {
const form = playground.querySelector("[data-playground-form]");
const preview = playground.querySelector("[data-playground-preview]");
const status = playground.querySelector("[data-playground-status]");
if (!(form instanceof HTMLFormElement) || !preview || !status) return;
const params = valuesFrom(form);
if (!params) {
status.textContent = "Fix the highlighted JSON value.";
return;
}
request?.abort();
request = new AbortController();
const url = new URL(window.location.href);
for (const key of [...url.searchParams.keys()]) {
if (key.startsWith("pg_")) url.searchParams.delete(key);
}
for (const [key, value] of params) url.searchParams.set(key, value);
playground.setAttribute("data-updating", "true");
status.textContent = "Updating preview…";
try {
const response = await fetch(url, {
headers: { "x-wrnexus-playground": "1" },
signal: request.signal,
});
if (!response.ok) throw new Error(`Preview request failed (${response.status})`);
const responseHtml = await response.text();
const parsed = new DOMParser().parseFromString(
htmlPolicy ? htmlPolicy.createHTML(responseHtml) : responseHtml,
"text/html",
);
const next = parsed.querySelector("[data-playground-preview]");
if (!next) throw new Error("Preview markup was not returned");
window.__wrnexusDisposeBehaviors?.(preview);
preview.replaceChildren(
...Array.from(next.childNodes, (node) => document.importNode(node, true)),
);
window.__wrnexusHydrateScopes?.(preview);
window.__wrnexusHydrateCsrFetches?.(preview);
window.wireTheme?.bind?.(document);
bindPlaygroundEvents(document);
status.textContent = "Preview updated";
} catch (error) {
if (error?.name !== "AbortError") {
status.textContent = error instanceof Error ? error.message : "Unable to update preview";
}
} finally {
playground.removeAttribute("data-updating");
}
}
const outputBindings = new WeakMap();
const legacyObservedOutputs = new Set();
function formatOutputPayload(payload) {
if (payload === undefined) return "No output payload";
if (typeof payload === "string") return payload;
try {
return JSON.stringify(payload, null, 2);
} catch {
return String(payload);
}
}
function renderOutput(owner, outputName, payload) {
const log = owner.querySelector("[data-playground-event-log]");
if (!log) return;
log.replaceChildren();
const heading = document.createElement("strong");
heading.textContent = `@${outputName}`;
const output = document.createElement("pre");
output.textContent = formatOutputPayload(payload);
log.append(heading, output);
}
function bindDirectOutput(host, owner, outputName) {
let byName = outputBindings.get(host);
if (!byName) {
byName = new Map();
outputBindings.set(host, byName);
}
if (byName.has(outputName)) return;
const registry = (host.__wrnexusOutputHandlers ??= new Map());
const handlers = registry.get(outputName) ?? new Set();
const handler = (payload) => renderOutput(owner, outputName, payload);
handlers.add(handler);
registry.set(outputName, handlers);
byName.set(outputName, handler);
}
function bindPlaygroundEvents(root = document) {
for (const playground of root.querySelectorAll?.(SELECTOR) ?? []) {
const outputNames = (playground.getAttribute("data-playground-events") || "")
.split(",")
.map((name) => name.trim())
.filter(Boolean);
const host = playground.querySelector(
"[data-playground-preview] [data-wrn-scope], [data-playground-preview] [data-ui-component], [data-playground-preview] [data-component]",
);
for (const outputName of outputNames) {
if (host) bindDirectOutput(host, playground, outputName);
// Legacy CustomEvent observation is retained only for compatibility-mode components.
if (legacyObservedOutputs.has(outputName)) continue;
legacyObservedOutputs.add(outputName);
document.addEventListener(outputName, (legacyEvent) => {
const owner = legacyEvent.target?.closest?.(SELECTOR);
if (!owner) return;
const declaredOutputs = (owner.getAttribute("data-playground-events") || "").split(",");
if (!declaredOutputs.includes(outputName)) return;
renderOutput(owner, outputName, legacyEvent.detail);
});
}
}
}
bindPlaygroundEvents();
new MutationObserver(() => bindPlaygroundEvents(document)).observe(
document.getElementById("app") || document.body,
{ childList: true, subtree: true },
);
function schedule(playground) {
updateCode(playground);
clearTimeout(timer);
timer = setTimeout(() => update(playground), 180);
}
document.addEventListener("input", (event) => {
const playground = event.target.closest?.(SELECTOR);
if (playground) schedule(playground);
});
document.addEventListener("change", (event) => {
const playground = event.target.closest?.(SELECTOR);
if (playground) schedule(playground);
});
document.addEventListener("reset", (event) => {
const playground = event.target.closest?.(SELECTOR);
if (playground)
setTimeout(() => {
updateCode(playground);
update(playground);
}, 0);
});
document.addEventListener("click", async (event) => {
const designToggle = event.target.closest?.("[data-design-panel-toggle]");
if (designToggle) {
document.documentElement.toggleAttribute("data-design-panel-open");
return;
}
if (event.target.closest?.("[data-design-reset]")) {
design = { ...DESIGN_DEFAULTS };
applyDesign(design);
return;
}
const copyText = event.target.closest?.("[data-copy-text]");
if (copyText) {
try {
await navigator.clipboard.writeText(copyText.dataset.copyText || "");
copyText.setAttribute("data-copied", "true");
setTimeout(() => copyText.removeAttribute("data-copied"), 1200);
} catch {
// Clipboard access may be blocked in an insecure local context.
}
return;
}
const menuToggle = event.target.closest?.("[data-docs-menu-toggle]");
if (menuToggle) {
document.documentElement.toggleAttribute("data-docs-menu-open");
return;
}
if (event.target.closest?.("[data-docs-component-link]")) {
document.documentElement.removeAttribute("data-docs-menu-open");
}
const copy = event.target.closest?.("[data-playground-copy]");
if (!copy) return;
const playground = copy.closest(SELECTOR);
const code = playground?.querySelector("[data-playground-code]");
const status = playground?.querySelector("[data-playground-status]");
if (!code || !status) return;
try {
await navigator.clipboard.writeText(code.textContent || "");
status.textContent = "Component code copied";
const label = copy.lastChild;
if (label?.nodeType === Node.TEXT_NODE) label.textContent = "Copied";
setTimeout(() => {
if (label?.nodeType === Node.TEXT_NODE) label.textContent = "Copy";
}, 1400);
} catch {
status.textContent = "Copy failed. Select the code and copy it manually.";
}
});
document.addEventListener("input", (event) => {
const setting = event.target.closest?.("[data-design-setting]");
if (setting) {
design = { ...design, [setting.dataset.designSetting]: setting.value };
applyDesign(design);
return;
}
if (!event.target.matches?.("[data-docs-search]")) return;
const query = event.target.value.trim().toLowerCase();
const links = [...document.querySelectorAll("[data-docs-component-link]")];
let visible = 0;
for (const link of links) {
const matches = !query || link.textContent.toLowerCase().includes(query);
link.hidden = !matches;
if (matches) visible++;
}
for (const group of document.querySelectorAll(".docs-directory-group")) {
group.hidden = !group.querySelector("[data-docs-component-link]:not([hidden])");
}
const empty = document.querySelector("[data-docs-empty]");
if (empty) empty.hidden = visible > 0;
});
document.addEventListener("keydown", (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
document.querySelector("[data-docs-search]")?.focus();
}
});
})();