release: WRNexusJS 0.3.5
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
/* 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);
|
||||
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("<", "<")
|
||||
.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 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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user