326 lines
11 KiB
JavaScript
326 lines
11 KiB
JavaScript
/* global AbortController, DOMParser, HTMLFormElement, HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, 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);
|
|
|
|
function updateActiveNavigation() {
|
|
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");
|
|
} else {
|
|
link.removeAttribute("aria-current");
|
|
}
|
|
}
|
|
}
|
|
|
|
updateActiveNavigation();
|
|
window.addEventListener("wrnexus:navigated", updateActiveNavigation);
|
|
|
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
if (design.mode === "system") applyDesign(design);
|
|
});
|
|
|
|
function sourceAttribute(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll('"', """)
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|
|
|
|
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") {
|
|
if (field.checked) attributes.push(name);
|
|
} 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);
|
|
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");
|
|
}
|
|
}
|
|
|
|
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 label = link.querySelector("[data-docs-component-name]")?.textContent || "";
|
|
const matches = !query || label.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();
|
|
}
|
|
});
|
|
})();
|