/** * Browser reactive runtime (Point 2: reactive directives). * * Served verbatim at `/__wrnexus/reactive.js` for any page that contains a * `data-scope`. It is plain browser JS (no build step) and self-contained: it * inlines a tiny `signal()` so it has no imports to resolve. * * Supported directives (this is exactly what the `.wrn` compiler emits): * data-scope="count: 0, name: 'x'" declare reactive state on a subtree * data-on-="count++" run a statement in scope on an event * data-text="expr" element textContent follows an expression * data-wrn-loop-locals="base64-json" preserves SSR {#each} item/index values * data-wrnexus-csr="id" target for generated CSR fetch bindings * {{expr}} or {expr} interpolation inside text nodes * * Expressions are evaluated by a tiny parser instead of `eval`/`new Function`, * so production can use a strong CSP without `unsafe-eval`. */ export const REACTIVE_RUNTIME = String.raw` (function () { var behaviorInstances = new WeakMap(); var mountedBehaviorRoots = new Set(); var pendingUpdateHooks = new Map(); var updateHooksScheduled = false; var behaviorObserver; var clientModuleCache = new Map(); /* * Globals the expression engine resolves for client code. Kept as explicit * tables rather than falling through to window[name]: an implicit fallback * would let any expression reach every global on the page (and would make a * typo silently resolve to some unrelated window property) -- these lists * say exactly what client code may reach. * * dialogGlobals must be bound to window or the browser throws * "Illegal invocation" when they are called detached. */ var dialogGlobals = { alert: 1, confirm: 1, prompt: 1, fetch: 1, print: 1, open: 1, scrollTo: 1, scrollBy: 1, matchMedia: 1, getComputedStyle: 1, structuredClone: 1, queueMicrotask: 1, btoa: 1, atob: 1, }; // Language builtins. Wrapped in thunks so referencing one that a given // engine lacks cannot throw at table-definition time. var jsGlobals = { Object: function () { return Object; }, Boolean: function () { return Boolean; }, RegExp: function () { return RegExp; }, Promise: function () { return typeof Promise === "undefined" ? undefined : Promise; }, Set: function () { return typeof Set === "undefined" ? undefined : Set; }, Map: function () { return typeof Map === "undefined" ? undefined : Map; }, Error: function () { return Error; }, Symbol: function () { return typeof Symbol === "undefined" ? undefined : Symbol; }, BigInt: function () { return typeof BigInt === "undefined" ? undefined : BigInt; }, Intl: function () { return typeof Intl === "undefined" ? undefined : Intl; }, parseInt: function () { return parseInt; }, parseFloat: function () { return parseFloat; }, isNaN: function () { return isNaN; }, isFinite: function () { return isFinite; }, encodeURIComponent: function () { return encodeURIComponent; }, decodeURIComponent: function () { return decodeURIComponent; }, encodeURI: function () { return encodeURI; }, decodeURI: function () { return decodeURI; }, NaN: function () { return NaN; }, Infinity: function () { return Infinity; }, undefined: function () { return undefined; }, }; /* * toast(...) -- raise a notification from any client expression. * * The core runtime deliberately knows nothing about the Toaster component: * it only dispatches a window event, and whatever toaster is mounted picks * it up. That keeps @wrnexus/ui out of the runtime's dependencies and lets * an app swap in its own toaster by listening for the same event. * * When nothing is listening the message would otherwise vanish silently, * which is a miserable thing to debug -- so fall back to the console and * say why once. */ var toastFallbackWarned = false; // Construct from the page window rather than the ambient global: in a // non-browser DOM the two are different classes, and a listener added on // the page window then never matches an event built from the other one. function dispatchWindowEvent(name, detail) { var Ctor = window.CustomEvent || CustomEvent; window.dispatchEvent(new Ctor(name, { detail: detail || {} })); } function emitToast(message, options, tone) { var detail = message && typeof message === "object" && !(message instanceof Error) ? Object.assign({}, message) : { message: message instanceof Error ? message.message : String(message) }; if (options && typeof options === "object") { Object.keys(options).forEach(function (key) { detail[key] = options[key]; }); } if (tone && !detail.tone) detail.tone = tone; // Detected from the DOM rather than a flag the host has to set, so any // element marked data-toaster counts -- including a custom one. if (!document.querySelector("[data-toaster]")) { if (!toastFallbackWarned) { toastFallbackWarned = true; console.info( "[wrnexus] toast() was called but no is mounted on this page; " + "falling back to the console. Add to your layout to show toasts.", ); } console.info("[wrnexus:toast]", detail.tone || "info", detail.title || "", detail.message); return detail; } dispatchWindowEvent("wrnexus:toast", detail); return detail; } var toastApi = function (message, options) { return emitToast(message, options); }; toastApi.success = function (message, options) { return emitToast(message, options, "success"); }; toastApi.error = function (message, options) { return emitToast(message, options, "danger"); }; toastApi.danger = toastApi.error; toastApi.warning = function (message, options) { return emitToast(message, options, "warning"); }; toastApi.info = function (message, options) { return emitToast(message, options, "info"); }; toastApi.dismiss = function (id) { dispatchWindowEvent("wrnexus:toast:dismiss", { id: id }); }; toastApi.clear = function () { dispatchWindowEvent("wrnexus:toast:clear", {}); }; /* * Published as a real global, not just an identifier the expression engine * knows about. Client functions are compiled to an ES module and run as * ordinary JavaScript, so a bare toast(...) there is a global lookup that * the engine never sees -- without this it throws ReferenceError while the * identical call in an inline handler works, which is a maddening * inconsistency. * * wireToast is the stable name. toast is the ergonomic alias and defers to * an application that already defines one. */ window.wireToast = toastApi; if (!window.toast) window.toast = toastApi; // Read straight off window, no binding needed (objects, not functions). var windowGlobals = { localStorage: 1, sessionStorage: 1, screen: 1, performance: 1, crypto: 1, CustomEvent: 1, Event: 1, FormData: 1, URLSearchParams: 1, AbortController: 1, Notification: 1, IntersectionObserver: 1, ResizeObserver: 1, MutationObserver: 1, devicePixelRatio: 1, innerWidth: 1, innerHeight: 1, scrollX: 1, scrollY: 1, }; function reportDiagnostic(code, message, element, detail) { var payload = { code: code, message: message, hydrationId: element && element.getAttribute ? element.getAttribute("data-wrn-hydration") : null, detail: detail || null, }; console.error("[wrnexus:" + code + "] " + message, detail || ""); try { window.dispatchEvent(new CustomEvent("wrnexus:diagnostic", { detail: payload })); } catch (_) { // CustomEvent can be unavailable in minimal DOM test environments. } } function scheduleUpdateHook(element, callback) { pendingUpdateHooks.set(element, callback); if (updateHooksScheduled) return; updateHooksScheduled = true; queueMicrotask(function () { updateHooksScheduled = false; var pending = Array.from(pendingUpdateHooks.entries()); pendingUpdateHooks.clear(); pending.forEach(function (entry) { var root = entry[0]; var hook = entry[1]; if (!root.isConnected) return; try { hook(); } catch (error) { console.error("[wrnexus] component update hook failed", error); } }); }); } function decodeScopePayload(value) { var binary = window.atob(value); var bytes = new Uint8Array(binary.length); for ( var index = 0; index < binary.length; index++ ) { bytes[index] = binary.charCodeAt(index); } var decoded = new TextDecoder( "utf-8", ).decode(bytes); var parsed = JSON.parse(decoded); return parsed && typeof parsed === "object" ? parsed : {}; } function decodeBehaviorPayload(value) { var binary = window.atob(value); var bytes = new Uint8Array(binary.length); for (var index = 0; index < binary.length; index++) { bytes[index] = binary.charCodeAt(index); } return new TextDecoder("utf-8").decode(bytes); } function parseBehavior(element) { var raw = element.getAttribute( "data-wrn-behavior", ); if (!raw) { return null; } try { var decoded = decodeBehaviorPayload(raw); var parsed = JSON.parse(decoded); if ( !parsed || typeof parsed !== "object" ) { return null; } return parsed; } catch (error) { console.error( "[wrnexus] failed to parse component behavior", error, ); return null; } } function extractBehaviorFunctions(source) { var functions = []; var re = /(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g; var match; while ((match = re.exec(source || ""))) { var start = re.lastIndex; var depth = 1; var quote = ""; var i = start; for (; i < source.length; i++) { var ch = source[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'" || ch === "\`") { quote = ch; continue; } if (ch === "{") depth++; else if (ch === "}" && --depth === 0) break; } if (depth !== 0) break; functions.push({ name: match[1], args: match[2].split(",").map(function (arg) { return arg.trim(); }).filter(Boolean), body: source.slice(start, i), }); re.lastIndex = i + 1; } return functions; } /* * Remove comments before anything tries to read the code. * * Statement bodies reaching the interpreter -- a lifecycle mount hook, a * multi-line inline handler -- are ordinary authored code. A comment line * became its own "statement" and blew up the whole body, so the hook simply * never ran. Comments are replaced by a space rather than deleted so they * cannot glue two tokens together. */ function stripComments(input) { var out = ""; var quote = ""; var index = 0; while (index < input.length) { var ch = input[index]; if (quote) { out += ch; if (ch === "\\") { out += input[index + 1] || ""; index += 2; continue; } if (ch === quote) quote = ""; index++; continue; } if (ch === '"' || ch === "'" || ch === "\`") { quote = ch; out += ch; index++; continue; } if (ch === "/" && input[index + 1] === "/") { var lineEnd = input.indexOf("\n", index + 2); if (lineEnd === -1) break; out += "\n"; index = lineEnd + 1; continue; } if (ch === "/" && input[index + 1] === "*") { var blockEnd = input.indexOf("*/", index + 2); out += " "; if (blockEnd === -1) break; index = blockEnd + 2; continue; } out += ch; index++; } return out; } function splitStatements(rawInput) { var input = stripComments(rawInput); var parts = []; var start = 0; var depth = 0; var quote = ""; function continuesOnNextLine(index) { var previousIndex = index - 1; var nextIndex = index + 1; while ( previousIndex >= start && /\s/.test(input[previousIndex]) ) { previousIndex--; } while ( nextIndex < input.length && /\s/.test(input[nextIndex]) ) { nextIndex++; } var previous = previousIndex >= start ? input[previousIndex] : ""; var next = nextIndex < input.length ? input[nextIndex] : ""; var currentLine = input .slice(start, previousIndex + 1) .trim(); // Postfix updates are complete statements. Treating their trailing + or // - as a multiline operator merges the following assignment into the // same statement (for example count++ followed by message = ...). if (/\+\+$|--$/.test(currentLine)) { return false; } // Newlines are formatting whitespace while either side is an // incomplete expression. This covers multiline assignments and // ternaries emitted by formatted .wrn component functions. return ( /[=+\-*/%?:,.!&|<>]/.test( previous, ) || /[.?+:*/%&|<>=]/.test(next) ); } for (var i = 0; i < input.length; i++) { var ch = input[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'" || ch === "\`") { quote = ch; continue; } if (ch === "(" || ch === "[" || ch === "{") depth++; else if (ch === ")" || ch === "]" || ch === "}") depth--; else if ( ( ch === ";" || ch === "\n" || ch === "\r" ) && depth === 0 ) { if ( ch !== ";" && continuesOnNextLine(i) ) { continue; } var nextIndex = i + 1; while ( nextIndex < input.length && /\s/.test(input[nextIndex]) ) { nextIndex++; } if ( input.slice( nextIndex, nextIndex + 4, ) === "else" ) { continue; } var part = input .slice(start, i) .trim(); if (part) { parts.push(part); } start = i + 1; } } var tail = input.slice(start).trim(); if (tail) parts.push(tail); return parts; } function signal(initial) { var value = initial; var subscribers = new Set(); var notifying = false; return { get: function () { return value; }, set: function (nextValue) { if ( Object.is( nextValue, value, ) ) { return; } var previous = value; value = nextValue; /* * Prevent nested synchronous notification of the * same signal. */ if (notifying) { return; } notifying = true; try { /* * Always iterate a snapshot. A renderer can read * this signal and subscribe again while it runs. */ var snapshot = Array.from(subscribers); for ( var index = 0; index < snapshot.length; index++ ) { var subscriber = snapshot[index]; if ( subscribers.has( subscriber, ) ) { subscriber( value, previous, ); } } } finally { notifying = false; } }, subscribe: function ( subscriber, ) { subscribers.add( subscriber, ); return function () { subscribers.delete( subscriber, ); }; }, }; } function routeSnapshot() { var url = new URL(window.location.href); var params = window.__wrnexusRouteParams || {}; var marker = document.querySelector("[data-wrn-route-params]"); if (marker) { try { params = JSON.parse(marker.getAttribute("data-wrn-route-params") || "{}"); } catch (_) { params = {}; } window.__wrnexusRouteParams = params; marker.remove(); } return { url: url.href, pathname: url.pathname, search: url.search, hash: url.hash, searchParams: url.searchParams, params: params, }; } function sanitizeReactiveUrl(value) { var raw = String(value == null ? "" : value); var compact = raw.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase(); if (/^(?:javascript|vbscript|file):/.test(compact)) return "about:blank"; if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(compact)) return "about:blank"; return raw; } function applyReactiveAttribute(node, name, value) { var lowerName = String(name || "").toLowerCase(); if ( value != null && ["href", "src", "action", "formaction", "poster", "cite", "background", "xlink:href"].indexOf(lowerName) !== -1 ) { value = sanitizeReactiveUrl(value); } if ( lowerName === "value" && ( node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement ) ) { var nextValue = value == null ? "" : String(value); if (node.value !== nextValue) { node.value = nextValue; } if (value == null) { node.removeAttribute("value"); } else { node.setAttribute( "value", nextValue, ); } return; } if ( lowerName === "checked" && node instanceof HTMLInputElement ) { var checked = !!value; if (node.checked !== checked) { node.checked = checked; } if (checked) { node.setAttribute( "checked", "", ); } else { node.removeAttribute( "checked", ); } return; } if ( lowerName === "selected" && node instanceof HTMLOptionElement ) { var selected = !!value; node.selected = selected; if (selected) { node.setAttribute( "selected", "", ); } else { node.removeAttribute( "selected", ); } return; } if ( lowerName === "disabled" || lowerName === "required" || lowerName === "readonly" || lowerName === "multiple" ) { var enabled = !!value; if (lowerName in node) { try { node[lowerName] = enabled; } catch (_) { // Fall through to attribute handling. } } if (enabled) { node.setAttribute( name, "", ); } else { node.removeAttribute(name); } return; } if ( lowerName.startsWith("data-") || lowerName.startsWith("aria-") ) { if (value == null) { node.removeAttribute(name); } else { node.setAttribute(name, String(value)); } return; } if (value === false || value == null) { node.removeAttribute(name); } else { node.setAttribute( name, value === true ? "" : String(value), ); } } function loadClientModule(element) { var url = element.getAttribute("data-wrn-client-module"); if (!url) return Promise.resolve(null); var promise = clientModuleCache.get(url); if (!promise) { promise = import(url).catch(function (error) { clientModuleCache.delete(url); reportDiagnostic("WRN-CLIENT-MODULE", "Failed to load browser function module '" + url + "'.", element, error); return null; }); clientModuleCache.set(url, promise); } return promise; } function setupScope(el) { if (el.__wrnexusScope) return; el.__wrnexusScope = true; el.__wrnexusHydrated = true; var encodedScope = el.getAttribute( "data-wrn-scope", ); var initial; if (encodedScope) { try { initial = decodeScopePayload( encodedScope, ); } catch (error) { reportDiagnostic( "WRN-HYDRATE-SCOPE", "Failed to decode the server-rendered scope payload.", el, error, ); initial = {}; } } else { var declaration = el.getAttribute( "data-scope", ) || ""; initial = parseScopeDecl( declaration, ); } var signals = {}; Object.keys(initial).forEach(function (k) { signals[k] = signal(initial[k]); }); var behavior = parseBehavior(el); var computedDefinitions = {}; var computing = new Set(); if (behavior && Array.isArray(behavior.computed)) { behavior.computed.forEach(function (entry) { if (entry && typeof entry.name === "string" && typeof entry.expr === "string") { computedDefinitions[entry.name] = entry.expr; } }); } var renderers = []; // Dependency-tracked rendering: while a renderer runs, every signal it reads // subscribes THAT renderer (not a blanket "re-render everything"). A signal // change then re-runs only the renderers that actually read it. The Set in // signal.subscribe dedupes, so re-subscribing each run is cheap and bounded. var currentRenderer = null; var pendingRenderers = new Set(); var batchDepth = 0; var flushingRenderers = false; function flushRenderers() { if (flushingRenderers || batchDepth > 0) return; flushingRenderers = true; try { while (pendingRenderers.size > 0) { var queue = Array.from(pendingRenderers); pendingRenderers.clear(); queue.forEach(function (run) { run(); }); } } finally { flushingRenderers = false; } } function scheduleRenderer(renderer) { pendingRenderers.add(renderer); flushRenderers(); } function batchUpdates(callback) { batchDepth++; try { return callback(); } finally { batchDepth--; flushRenderers(); } } function reactive(fn) { var running = false; function run() { if (running) { return; } running = true; var previousRenderer = currentRenderer; currentRenderer = schedule; try { fn(); } finally { currentRenderer = previousRenderer; running = false; } } function schedule() { scheduleRenderer(run); } renderers.push(run); return run; } /* * An index loop, deliberately, not forEach. * * forEach fixes its range before the first callback, so an effect * registered WHILE the sweep is running was never executed -- and an * effect that never runs never subscribes, so it stayed dead for the life * of the page. Every binding inside a data-for row registers exactly then, * from inside its loop render, which is why a row attribute could not * react to anything its list expression did not already read. Re-reading * length each step picks those up in the same sweep. * * The cap is a guard against an effect that registers another effect on * every run; it is far above any real component. */ function renderAll() { for (var index = 0; index < renderers.length && index < 10000; index++) { renderers[index](); } } function decodeLoopLocals(node) { if (!node) { return {}; } var element = node.nodeType === 1 ? node : node.parentElement; var owner = element && element.closest && element.closest( "[data-wrn-loop-locals]", ); if (!owner) { return {}; } var encoded = owner.getAttribute( "data-wrn-loop-locals", ); if (!encoded) { return {}; } try { var binary = window.atob(encoded); var bytes = new Uint8Array(binary.length); for ( var index = 0; index < binary.length; index++ ) { bytes[index] = binary.charCodeAt(index); } var decoded = new TextDecoder("utf-8").decode( bytes, ); var parsed = JSON.parse(decoded); return parsed && typeof parsed === "object" ? parsed : {}; } catch (error) { console.error( "[wrnexus] failed to decode loop locals", error, ); return {}; } } var behaviorFunctions = {}; var moduleBindings = {}; var componentEventTarget = el.hasAttribute("data-wrn-events") ? el : el.querySelector("[data-wrn-events]") || el; var declaredEvents = new Set( String(componentEventTarget.getAttribute("data-wrn-events") || "") .split(",") .map(function (name) { return name.trim(); }) .filter(Boolean), ); var outputHandlers = componentEventTarget.__wrnexusOutputHandlers || (componentEventTarget.__wrnexusOutputHandlers = {}); var outputProxy = new Proxy({}, { get: function (_target, property) { return function (payload) { return invokeComponentOutput(componentEventTarget, String(property), payload); }; }, }); var componentRpcName = componentEventTarget.getAttribute("data-wrn-component") || componentEventTarget.getAttribute("data-wrn-hydration") || "component"; var serverProxy = new Proxy({}, { get: function (_target, property) { return function () { return callServerFunction(componentRpcName, String(property), Array.prototype.slice.call(arguments)); }; }, }); var propsProxy = new Proxy({}, { get: function (_target, property) { return peekScope(String(property)); }, set: function () { throw new TypeError("WRN-PROP-READONLY: props are readonly"); }, }); var refsProxy = new Proxy({}, { get: function (_target, property) { return el.querySelector('[data-ref="' + String(property).replace(/"/g, '\"') + '"]'); }, }); var stateWatchers = {}; var anyStateListeners = new Set(); var cleanupCallbacks = []; var disposed = false; var routeValue = signal(routeSnapshot()); function updateRouteValue() { routeValue.set(routeSnapshot()); } window.addEventListener("wrnexus:navigated", updateRouteValue); window.addEventListener("popstate", updateRouteValue); window.addEventListener("hashchange", updateRouteValue); cleanupCallbacks.push(function () { window.removeEventListener("wrnexus:navigated", updateRouteValue); window.removeEventListener("popstate", updateRouteValue); window.removeEventListener("hashchange", updateRouteValue); }); function readGlobal(name) { if (declaredEvents.has(name)) { return function (detail) { return dispatchComponentEvent(componentEventTarget, name, detail); }; } if (name === "output") return outputProxy; if (name === "server") return serverProxy; if (name === "props") return propsProxy; if (name === "refs") return refsProxy; if (Object.prototype.hasOwnProperty.call(moduleBindings, name)) return moduleBindings[name]; if (name === "$emit") { return function (eventName, detail) { return dispatchComponentEvent(componentEventTarget, eventName, detail); }; } if (name === "window") return window; if (name === "document") return document; if (name === "console") return console; if (name === "Array") return Array; if (name === "Number") return Number; if (name === "String") return String; if (name === "Math") return Math; if (name === "JSON") return JSON; if (name === "Date") return Date; if (name === "URL") return URL; if (name === "location") return window.location; if (name === "history") return window.history; if (name === "navigator") return window.navigator; if (name === "$route" || name === "route") { if (currentRenderer) routeValue.subscribe(currentRenderer); return routeValue.get(); } if (name === "setTimeout") return window.setTimeout.bind(window); if (name === "clearTimeout") return window.clearTimeout.bind(window); if (name === "setInterval") return window.setInterval.bind(window); if (name === "clearInterval") return window.clearInterval.bind(window); if (name === "requestAnimationFrame") return window.requestAnimationFrame.bind(window); if (name === "cancelAnimationFrame") return window.cancelAnimationFrame.bind(window); /* * Ordinary browser and language globals. * * This engine resolves identifiers itself rather than handing the * expression to eval/new Function, which is what lets a page run under * a strict CSP with no unsafe-eval. The cost is that an unknown * identifier quietly evaluates to undefined -- so an author who writes * alert(...) or Object.keys(...) in a client function gets * "undefined is not a function" and no clue why. Resolving the normal * globals here makes client code behave the way its author expects. * * Bound where the callee needs a window receiver (illegal-invocation * otherwise), and read lazily so a server-side or minimal DOM that * lacks one of these does not break the rest. */ if (name === "toast") return toastApi; if (dialogGlobals[name] && typeof window[name] === "function") { return window[name].bind(window); } if (jsGlobals[name]) { var builtin = jsGlobals[name](); if (builtin !== undefined) return builtin; } if (windowGlobals[name]) { try { return window[name]; } catch (error) { return undefined; } } return undefined; } function readScope(name) { if (Object.prototype.hasOwnProperty.call(computedDefinitions, name)) { if (computing.has(name)) { reportDiagnostic("WRN-COMPUTED-CYCLE", "Computed value '" + name + "' has a dependency cycle.", el); return undefined; } computing.add(name); try { return evalExpr(computedDefinitions[name]); } finally { computing.delete(name); } } var sig = signals[name]; if (sig) { if (currentRenderer) sig.subscribe(currentRenderer); return sig.get(); } if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) { return behaviorFunctions[name]; } return readGlobal(name); } function peekScope(name) { if (Object.prototype.hasOwnProperty.call(computedDefinitions, name)) { return readScope(name); } if (signals[name]) return signals[name].get(); if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) { return behaviorFunctions[name]; } return readGlobal(name); } function notifyState(name, value, previous) { var watchers = stateWatchers[name]; if (watchers) { watchers.slice().forEach(function (watcher) { try { watcher(value, previous); } catch (error) { console.error("[wrnexus] watcher for '" + name + "' failed", error); } }); } anyStateListeners.forEach(function (listener) { try { listener(name, value, previous); } catch (error) { console.error("[wrnexus] state subscriber failed", error); } }); } function writeScope( name, value, ) { if (!signals[name]) { signals[name] = signal(value); notifyState( name, value, undefined, ); renderAll(); return; } var previous = signals[name].get(); if ( Object.is( previous, value, ) ) { return; } signals[name].set(value); notifyState( name, value, previous, ); } function evalExpr(expr, locals) { return evaluateExpression(expr, function (name) { if (locals && Object.prototype.hasOwnProperty.call(locals, name)) { return locals[name]; } return readScope(name); }); } function runStmt( source, locals, ) { return batchUpdates(function () { var statements = splitStatements(source); for ( var statementIndex = 0; statementIndex < statements.length; statementIndex++ ) { var result = runStatement( statements[statementIndex], function (expression) { return evalExpr( expression, locals, ); }, function (name) { if ( locals && Object.prototype .hasOwnProperty.call( locals, name, ) ) { return locals[name]; } return peekScope(name); }, function (name, value) { if ( locals && Object.prototype .hasOwnProperty.call( locals, name, ) ) { locals[name] = value; } else { writeScope(name, value); } }, function (body) { return runStmt( body, locals, ); }, ); if (result.returned) { return result; } } return { returned: false, value: undefined, }; }); } function installBehaviorFunctions(source) { extractBehaviorFunctions(source || "").forEach(function (definition) { // A generated browser module contains the real JavaScript implementation // of client/shared functions. Keep that implementation when it has already // been installed instead of replacing it with the small CSP-safe fallback // interpreter. The fallback intentionally supports only a bounded subset of // JavaScript and must never shadow the compiled function. if (typeof behaviorFunctions[definition.name] === "function") return; behaviorFunctions[ definition.name ] = function () { var locals = {}; for ( var i = 0; i < definition.args.length; i++ ) { locals[definition.args[i]] = arguments[i]; } var result = runStmt( definition.body, locals, ); return result.returned ? result.value : undefined; }; }); } function installClientModule(module) { if (!module) return; var stateProxy = new Proxy({}, { get: function (_target, property) { return readScope(String(property)); }, set: function (_target, property, value) { writeScope(String(property), value); return true; }, ownKeys: function () { return Object.keys(signals); }, getOwnPropertyDescriptor: function () { return { enumerable: true, configurable: true }; }, }); var context = { state: stateProxy, output: outputProxy, server: serverProxy, props: propsProxy, refs: refsProxy, }; var importedBindings = module.__wrnexusImportedBindings; if (importedBindings && typeof importedBindings === "object") { Object.keys(importedBindings).forEach(function (name) { var binding = importedBindings[name]; moduleBindings[name] = binding; if (binding && typeof binding.subscribe === "function") { cleanupCallbacks.push(binding.subscribe(function () { renderAll(); })); } }); } var functions = typeof module.bindClientScope === "function" ? module.bindClientScope(context) : module.__wrnexusClientFunctions; if (!functions || typeof functions !== "object") return; Object.keys(functions).forEach(function (name) { if (typeof functions[name] === "function") behaviorFunctions[name] = functions[name]; }); } installClientModule(el.__wrnexusClientModule); // A binding belongs to THIS scope only when el is the node's *owning* // scope. Normally that is the nearest [data-scope] ancestor, so an outer // scope never clobbers an inner one's values. // // Slot content is the exception. Children written between a component's // open and close tags are authored // in the PARENT's source but SSR splices them inside the child // component's [data-scope] root, so the nearest-ancestor rule would hand // them to the child -- where the page's state and functions do not exist, // and the child's same-named state silently shadows them. The server // wraps spliced slot content in [data-wrn-slot], which acts as an // ownership boundary: crossing it means stepping *out* of the component // that received the slot and continuing the search from its mount point. // The loop repeats so slots nested through several components resolve to // the scope that actually wrote the markup. function ownerScope(node) { var cursor = node.nodeType === 1 ? node : node.parentNode; for (var guard = 0; cursor && cursor.closest && guard < 32; guard++) { var found = cursor.closest( "[data-scope], [data-wrn-scope], [data-wrn-slot]", ); if (!found) return null; if (!found.hasAttribute("data-wrn-slot")) return found; // Step out of the component whose slot this content filled, then keep // looking from just above that component's root. var componentRoot = found.parentNode && found.parentNode.closest ? found.parentNode.closest("[data-scope], [data-wrn-scope]") : null; cursor = componentRoot ? componentRoot.parentNode : found.parentNode; } return null; } function owns(node) { return ownerScope(node) === el; } // --- data-for list rendering ------------------------------------------- // Each [data-for="item in list"] element is a per-item template. Add // data-key="item.id" or a key item.id suffix preserves DOM nodes when // a list is reordered. Unkeyed loops retain the legacy full-rerender path. function parseFor(value) { var m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)(?:\s+key\s+([\s\S]+?))?\s*$/.exec( value || "", ); return m ? { item: m[1], index: m[2], list: m[3].trim(), key: m[4] && m[4].trim() } : null; } function unwrapForKey(value) { var expression = String(value || "").trim(); if (expression.charAt(0) === "{" && expression.charAt(expression.length - 1) === "}") { expression = expression.slice(1, -1).trim(); } return expression; } function stableForKey(value) { if (value === null) return "null:"; var type = typeof value; if (type === "object") { try { return "object:" + JSON.stringify(value); } catch (_) { return "object:" + String(value); } } return type + ":" + String(value); } function fillMustache(str, itemEval) { return str.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, d, s) { var e = (d || s).trim(); try { return String(itemEval(e)); } catch (err) { return ""; } }); } function hydrateItem( root, locals, ) { function localRead(name) { if ( Object.prototype.hasOwnProperty.call( locals, name, ) ) { return locals[name]; } return readScope(name); } function itemEval(expression) { return evaluateExpression( expression, localRead, ); } function applyItemTemplate( template, ) { return String(template).replace( /\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function ( _, doubleExpression, singleExpression, ) { var expression = ( doubleExpression || singleExpression ).trim(); try { var value = itemEval(expression); return value == null ? "" : String(value); } catch (_) { return ""; } }, ); } /* * A nested [data-for] inside this item is a TEMPLATE for its own loop, * not content of this one. Interpolating it here would bake the inner * mustaches against the outer locals -- where the inner item name is * undefined -- and the nested rows would render blank. Skip those * subtrees; setupForLoop below takes them over with this item's locals * in scope. */ function insideNestedLoop(node) { var cursor = node; while (cursor && cursor !== root) { if (cursor.nodeType === 1 && cursor.hasAttribute("data-for")) return true; cursor = cursor.parentNode; } return false; } var elements = [root]; if (root.querySelectorAll) { Array.prototype.push.apply( elements, root.querySelectorAll("*"), ); } elements.forEach(function (node) { if ( !node || node.nodeType !== 1 ) { return; } if (node !== root && insideNestedLoop(node)) return; var attributes = Array.prototype.slice.call( node.attributes, ); attributes.forEach(function ( attribute, ) { /* * data-text must be handled using itemEval(), * because item/index only exist in loop locals. */ if ( attribute.name === "data-text" ) { try { var textValue = itemEval(attribute.value); node.textContent = textValue == null ? "" : String(textValue); } catch (error) { console.error( "[wrnexus] data-for text binding failed for '" + attribute.value + "'", error, ); node.textContent = ""; } return; } /* * Conditional classes: * data-wrn-class-*='["class","expression"]' */ if ( attribute.name.indexOf( "data-wrn-class-", ) === 0 ) { var classBinding; try { classBinding = JSON.parse( attribute.value, ); } catch (_) { return; } if ( !classBinding || classBinding.length !== 2 ) { return; } var className = classBinding[0]; var classExpression = classBinding[1]; var classEnabled = false; try { classEnabled = !!itemEval( classExpression, ); } catch (_) { classEnabled = false; } node.classList.toggle( className, classEnabled, ); return; } /* * Reactive ordinary attributes: * data-wrn-bind-*='["aria-expanded","{isOpen(index)}"]' */ if ( attribute.name.indexOf( "data-wrn-bind-", ) === 0 ) { var binding; try { binding = JSON.parse( attribute.value, ); } catch (_) { return; } if ( !binding || binding.length !== 2 ) { return; } var attributeName = binding[0]; var attributeTemplate = binding[1]; /* * Reactive, not resolved once. The expression can read component * state the row never mentions -- a selection set, the current sort * -- and the row must stay in step with it after it is built. * Resolving at clone time froze every such attribute at whatever it * was on first render. */ (function (bindNode, bindName, bindTemplate) { var exact = /^\{([^{}]+)\}$/.exec(bindTemplate); // Run immediately for the same reason as data-show above. var runBind = reactive(function () { var rawValue; if (exact) { try { rawValue = itemEval(exact[1].trim()); } catch (_) { return; } } else { rawValue = bindTemplate.replace(/\{([^{}]+)\}/g, function (_, expression) { try { var value = itemEval(expression.trim()); return value == null ? "" : String(value); } catch (_) { return ""; } }); } applyReactiveAttribute(bindNode, bindName, rawValue); }); runBind(); })(node, attributeName, attributeTemplate); return; } /* * Event handlers inside data-for. */ if ( attribute.name.indexOf( "data-on-", ) === 0 ) { var eventName = attribute.name.slice( "data-on-".length, ); var statement = attribute.value; node.addEventListener( eventName, function (event) { var eventLocals = {}; Object.keys( locals, ).forEach(function (key) { eventLocals[key] = locals[key]; }); eventLocals.event = event; eventLocals.$event = event; eventLocals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined; try { runStmt( statement, eventLocals, ); } catch (error) { console.error( "[wrnexus] data-for handler error in '" + statement + "'", error, ); } }, ); return; } /* * data-show inside data-for. * * The page-level data-show pass runs once over the DOM at hydration, * long before these clones exist, so a row's data-show was never * evaluated: the attribute kept its literal expression text and the * element stayed visible no matter what. That silently rendered * every conditional part of every row -- an empty action button drawn * as a stray outline, blank headings taking up space. * * Evaluated here against the row's own locals. A row is rebuilt when * its item changes, so evaluating once per clone is correct and * needs no separate subscription. */ /* * data-html inside a loop row. The page-level scan happens once at * hydration, long before these clones exist, so a row that renders * markup would otherwise never have it applied. */ if (attribute.name === "data-html") { (function (htmlNode, htmlExpr) { var runHtml = reactive(function () { var markup; try { markup = itemEval(htmlExpr); } catch (_) { return; } var next = markup == null ? "" : String(markup); if (htmlNode.innerHTML !== next) { htmlNode.innerHTML = next; } }); runHtml(); })(node, attribute.value); return; } if (attribute.name === "data-show") { // Reactive, not resolved once: the expression can read component // state the row itself knows nothing about (a selection set, a // sort key), and those must keep the row in step after it is // built. (function (showNode, showExpr) { /* * Run it now, do not wait to be swept. reactive() only * registers; the initial renderAll picks registrations up, but a * row rebuilt LATER (a re-sort, a filter) registers outside any * sweep, and nothing would ever run it. The attribute then stays * as the raw expression and the element defaults to visible -- * which is why sorting a column made a second copy of every * header label appear. */ var runShow = reactive(function () { var shown = true; try { shown = !!itemEval(showExpr); } catch (_) { // Unresolvable expression: leave the element visible rather // than hiding content because of an authoring slip. } showNode.setAttribute( "data-show", shown ? "true" : "false", ); showNode.style.display = shown ? "" : "none"; }); runShow(); })(node, attribute.value); return; } /* * Static attributes containing interpolation. */ if ( attribute.value && attribute.value.indexOf("{") !== -1 ) { node.setAttribute( attribute.name, applyItemTemplate( attribute.value, ), ); } }); }); /* * Text interpolation inside loop content: * * {item.title} */ var walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, null, ); var textNode; while ( (textNode = walker.nextNode()) ) { var template = textNode.nodeValue || ""; if ( template.indexOf("{") === -1 ) { continue; } if (insideNestedLoop(textNode)) continue; textNode.nodeValue = applyItemTemplate(template); } // Hand every nested loop its own renderer, with this item in scope. if (root.querySelectorAll) { Array.prototype.slice .call(root.querySelectorAll("[data-for]")) .forEach(function (nested) { // Only the outermost nested templates: deeper ones are set up by // their own parent when it renders. var parentTemplate = nested.parentNode; while (parentTemplate && parentTemplate !== root) { if ( parentTemplate.nodeType === 1 && parentTemplate.hasAttribute("data-for") ) { return; } parentTemplate = parentTemplate.parentNode; } setupForLoop(nested, locals); }); } } /* * Set up one [data-for] template. Extracted from an inline forEach so it * can recurse: hydrateItem calls it for every loop nested inside a rendered * item, passing that item as outerLocals. Without that a table -- rows * containing cells -- could not render at all, because the inner loop was * never given a renderer and its template was left in the DOM unexpanded. * * outerLocals are the enclosing item bindings; they are visible to the * list expression, the key, and everything the nested rows interpolate. */ function setupForLoop(tpl, outerLocals) { var inherited = outerLocals || {}; function withInherited(extra) { var merged = {}; Object.keys(inherited).forEach(function (name) { merged[name] = inherited[name]; }); if (extra) { Object.keys(extra).forEach(function (name) { merged[name] = extra[name]; }); } return merged; } function loopRead(name) { if (Object.prototype.hasOwnProperty.call(inherited, name)) { return inherited[name]; } return readScope(name); } function loopEval(expression) { return evaluateExpression(expression, loopRead); } /* * A nested template is hydrated while its row is still detached from * the document, so owns() cannot walk up to a [data-scope] ancestor * and would reject it. Ownership is already settled in that case: the * enclosing loop only renders rows it owns. */ if (!tpl.parentNode || (!outerLocals && !owns(tpl))) { return; } var spec = parseFor( tpl.getAttribute( "data-for", ), ); if (!spec) { return; } var template = tpl.cloneNode(true); template.removeAttribute( "data-for", ); var keyExpression = spec.key || unwrapForKey( tpl.getAttribute( "data-key", ), ); template.removeAttribute( "data-key", ); var parent = tpl.parentNode; var marker = document.createComment( "wrn-for", ); parent.insertBefore( marker, tpl, ); parent.removeChild(tpl); var clones = []; var keyedRecords = new Map(); /* * reactive() only REGISTERS the effect; the initial renderAll() sweep * runs the renderers array, and forEach fixes its range before the * first callback -- so an effect registered while that sweep is * already running is never executed. A nested loop registers at * exactly that moment, from inside its parent row's render, which is * why nested rows came out empty. Run it once by hand. */ var runLoop = reactive(function () { var list = loopEval(spec.list); if (!Array.isArray(list)) { console.error( "[wrnexus] data-for expected an array for '" + spec.list + "', received", list, ); list = []; } if (!keyExpression) { for ( var cloneIndex = 0; cloneIndex < clones.length; cloneIndex++ ) { var existingClone = clones[cloneIndex]; if ( existingClone.parentNode ) { existingClone.parentNode .removeChild( existingClone, ); } } clones = []; var fragment = document.createDocumentFragment(); for ( var itemIndex = 0; itemIndex < list.length; itemIndex++ ) { var clone = template.cloneNode(true); var locals = withInherited(null); locals[spec.item] = list[itemIndex]; if (spec.index) { locals[spec.index] = itemIndex; } hydrateItem( clone, locals, ); fragment.appendChild( clone, ); clones.push(clone); } parent.insertBefore( fragment, marker.nextSibling, ); return; } var nextRecords = new Map(); var orderedNodes = []; for ( var keyedIndex = 0; keyedIndex < list.length; keyedIndex++ ) { var keyedItem = list[keyedIndex]; var keyedLocals = withInherited(null); keyedLocals[spec.item] = keyedItem; if (spec.index) keyedLocals[spec.index] = keyedIndex; var rawKey; try { rawKey = evaluateExpression( keyExpression, function (name) { return Object.prototype.hasOwnProperty.call(keyedLocals, name) ? keyedLocals[name] : loopRead(name); }, ); } catch (error) { reportDiagnostic( "WRN-HYDRATE-KEY-001", "Unable to evaluate data-for key '" + keyExpression + "'.", el, error, ); rawKey = keyedIndex; } var normalizedKey = stableForKey(rawKey); if (nextRecords.has(normalizedKey)) { reportDiagnostic( "WRN-HYDRATE-KEY-002", "Duplicate data-for key '" + String(rawKey) + "'; falling back to its index.", el, { key: rawKey, index: keyedIndex }, ); normalizedKey += ":index:" + keyedIndex; } var record = keyedRecords.get(normalizedKey); if ( !record || record.item !== keyedItem || (spec.index && record.index !== keyedIndex) ) { if (record && record.node.parentNode) { record.node.parentNode.removeChild(record.node); } var keyedClone = template.cloneNode(true); hydrateItem(keyedClone, keyedLocals); record = { node: keyedClone, item: keyedItem, index: keyedIndex, }; } nextRecords.set(normalizedKey, record); orderedNodes.push(record.node); } keyedRecords.forEach(function (record, key) { if (!nextRecords.has(key) && record.node.parentNode) { record.node.parentNode.removeChild(record.node); } }); var keyedFragment = document.createDocumentFragment(); orderedNodes.forEach(function (node) { keyedFragment.appendChild(node); }); parent.insertBefore( keyedFragment, marker.nextSibling, ); keyedRecords = nextRecords; clones = orderedNodes; }); if (outerLocals) runLoop(); } Array.prototype.slice .call(el.querySelectorAll("[data-for]")) .forEach(function (tpl) { // Only top-level templates here; nested ones are wired by the item // that contains them, once it has values to give them. if (tpl.parentNode && tpl.parentNode.closest && tpl.parentNode.closest("[data-for]")) { return; } setupForLoop(tpl, null); }); // data-text bindings. Server-rendered {#each} nodes recover their item/index // values from the nearest data-wrn-loop-locals marker. /* * data-html="expr" -- render the value as markup instead of text. * * Deliberately a separate directive from data-text rather than an option * on it: this is the one place the runtime stops escaping, so it should be * impossible to reach by accident. Only ever point it at markup your own * application produced. Anything derived from user input has to be * sanitised first -- data-text is the safe default and stays that way. */ el.querySelectorAll("[data-html]").forEach(function (node) { if (!owns(node)) return; var htmlExpr = node.getAttribute("data-html"); reactive(function () { var value; try { value = evalExpr(htmlExpr, decodeLoopLocals(node)); } catch (error) { reportDiagnostic("WRN-HTML-EXPR", "data-html expression failed.", node, error); return; } var markup = value == null ? "" : String(value); if (node.innerHTML !== markup) { node.innerHTML = markup; } }); }); el.querySelectorAll("[data-text]").forEach(function (node) { if (!owns(node)) return; var expr = node.getAttribute("data-text"); reactive(function () { try { var value = evalExpr( expr, decodeLoopLocals(node), ); node.textContent = value == null ? "" : String(value); } catch (_) { // Keep the server-rendered value when evaluation is unavailable. } }); }); // data-show="expr" — toggle visibility on truthiness. This directive is // intentionally non-destructive because popovers, selects and remote data // controls keep event wiring and state while closed. Use a compiled {#if} // block when the inactive branch must not be rendered. el.querySelectorAll("[data-show]").forEach(function (node) { if (!owns(node)) return; var showExpr = node.getAttribute("data-show"); reactive(function () { var visible = true; try { visible = !!evalExpr( showExpr, decodeLoopLocals(node), ); } catch (_) { // Keep visible when evaluation is unavailable. } node.setAttribute( "data-show", visible ? "true" : "false", ); node.style.display = visible ? "" : "none"; }); }); // Conditional class bindings emitted as: // data-wrn-class-*='["class-name","expression"]' var classBindNodes = [el].concat( Array.prototype.slice.call(el.querySelectorAll("*")), ); classBindNodes.forEach(function (node) { if (!owns(node)) return; Array.prototype.slice.call(node.attributes).forEach(function (marker) { if (marker.name.indexOf("data-wrn-class-") !== 0) return; var binding; try { binding = JSON.parse(marker.value); } catch (e) { return; } if (!binding || binding.length !== 2) return; var className = binding[0]; var expression = binding[1]; reactive(function () { var enabled = false; try { enabled = !!evalExpr( expression, decodeLoopLocals(node), ); } catch (error) { enabled = false; } node.classList.toggle( className, enabled, ); }); }); }); // Reactive ordinary attributes emitted by the compiler. Each marker stores // [attributeName, originalTemplate], preserving an SSR value while allowing // state changes to update type, aria-*, class, href, and other attributes. var bindNodes = [el].concat( Array.prototype.slice.call( el.querySelectorAll("*"), ), ); bindNodes.forEach(function (node) { if (!owns(node)) return; Array.prototype.slice .call(node.attributes) .forEach(function (marker) { if ( marker.name.indexOf( "data-wrn-bind-", ) !== 0 ) { return; } var binding; try { binding = JSON.parse( marker.value, ); } catch (error) { return; } if ( !binding || binding.length !== 2 ) { return; } var name = binding[0]; var template = binding[1]; reactive(function () { var locals = decodeLoopLocals(node); var exact = /^\{([^{}]+)\}$/.exec( template, ); var raw; if (exact) { try { raw = evalExpr( exact[1].trim(), locals, ); } catch (error) { return; } } else { raw = template.replace( /\{([^{}]+)\}/g, function ( _, expression, ) { try { var value = evalExpr( expression.trim(), locals, ); return value == null ? "" : String(value); } catch (error) { return ""; } }, ); } applyReactiveAttribute( node, name, raw, ); }); }); }); // {{expr}} / {expr} interpolation in text nodes var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); var textNode; while ((textNode = walker.nextNode())) { var template = textNode.nodeValue; if (template.indexOf("{") === -1) continue; if (!owns(textNode)) continue; (function (node, tpl) { reactive(function () { node.nodeValue = tpl.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, doubleExpr, singleExpr) { var expr = doubleExpr || singleExpr; try { var value = evalExpr( expr.trim(), decodeLoopLocals( node.parentNode, ), ); return value == null ? "" : String(value); } catch (err) { return ""; } }); }); })(textNode, template); } // Event handlers on elements, window, and document. var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*"))); nodes.forEach(function (node) { if (!owns(node)) return; Array.prototype.slice.call(node.attributes).forEach(function (attr) { if (attr.name.indexOf("data-on-") !== 0) return; var rawName = attr.name.slice("data-on-".length); var target = node; var evt = rawName; if (rawName.indexOf("window-") === 0) { target = window; evt = rawName.slice("window-".length); } else if (rawName.indexOf("document-") === 0) { target = document; evt = rawName.slice("document-".length); } var stmt = attr.value; var listener = function (event) { var locals = decodeLoopLocals(node); locals.event = event; locals.$event = event; locals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined; try { runStmt( stmt, locals, ); } catch (error) { console.error( "[wrnexus] handler error in '" + stmt + "'", error, ); } }; var options = target === window && (evt === "scroll" || evt === "touchstart" || evt === "touchmove") ? { passive: true } : undefined; if (target === componentEventTarget && declaredEvents.has(evt)) { var directHandler = function (payload) { var locals = decodeLoopLocals(node); locals.payload = payload; locals.event = undefined; locals.$event = undefined; return runStmt(stmt, locals); }; (outputHandlers[evt] || (outputHandlers[evt] = new Set())).add(directHandler); cleanupCallbacks.push(function () { if (outputHandlers[evt]) outputHandlers[evt].delete(directHandler); }); } target.addEventListener(evt, listener, options); cleanupCallbacks.push(function () { target.removeEventListener(evt, listener, options); }); }); }); // Handlers a parent wrote on a component tag, e.g. a Modal tag carrying // an @confirm="save()" attribute. // The statement belongs to the PARENT's scope but the attribute rides // through to the child's view root, so the child must not bind it: that // function only exists out here. The compiler emits these as data-wrn-out-* so the // two cases stay distinguishable, and this scope claims every one that // sits on a component it directly mounts. Array.prototype.slice .call(el.querySelectorAll("[data-wrn-events]")) .forEach(function (node) { var componentRoot = node.closest("[data-scope], [data-wrn-scope]"); if (!componentRoot || componentRoot === el) return; // Only the scope that mounted this component owns its outputs. if ( !componentRoot.parentNode || !componentRoot.parentNode.closest || ownerScope(componentRoot.parentNode) !== el ) { return; } var registry = node.__wrnexusOutputHandlers || (node.__wrnexusOutputHandlers = {}); Array.prototype.slice.call(node.attributes).forEach(function (attr) { if (attr.name.indexOf("data-wrn-out-") !== 0) return; var outName = attr.name.slice("data-wrn-out-".length); var outStmt = attr.value; var outHandler = function (payload) { var locals = decodeLoopLocals(componentRoot); locals.payload = payload; locals.event = undefined; locals.$event = undefined; try { return runStmt(outStmt, locals); } catch (error) { console.error( "[wrnexus] component output handler error in '" + outStmt + "'", error, ); } }; (registry[outName] || (registry[outName] = new Set())).add(outHandler); cleanupCallbacks.push(function () { if (registry[outName]) registry[outName].delete(outHandler); }); // A component tag can also carry plain DOM events such as click. Those // are not declared outputs, so nothing calls the registry entry -- // listen for the bubbling event too. Declared outputs never reach // here: invokeComponentOutput short-circuits once a handler exists. var domListener = function (event) { var locals = decodeLoopLocals(componentRoot); locals.event = event; locals.$event = event; locals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined; try { runStmt(outStmt, locals); } catch (error) { console.error( "[wrnexus] component handler error in '" + outStmt + "'", error, ); } }; node.addEventListener(outName, domListener); cleanupCallbacks.push(function () { node.removeEventListener(outName, domListener); }); }); }); if (behavior) { installBehaviorFunctions(behavior.functions); var publicScopeApi = { get: peekScope, set: writeScope, call: function (name) { var fn = behaviorFunctions[name]; if (typeof fn !== "function") return undefined; return fn.apply(null, Array.prototype.slice.call(arguments, 1)); }, }; el.__wrnexusScopeApi = publicScopeApi; el.querySelectorAll("[data-wrn-select]").forEach(function (select) { select.__wrnexusScopeApi = publicScopeApi; }); (behavior.effects || []).forEach(function (source) { if (typeof source !== "string" || !source.trim()) return; reactive(function () { try { runStmt(source); } catch (error) { reportDiagnostic("WRN-EFFECT-ERROR", "Reactive effect failed.", el, error); } }); }); (behavior.watches || []).forEach(function (watch) { if (!watch || typeof watch.state !== "string" || typeof watch.body !== "string") return; if (!stateWatchers[watch.state]) stateWatchers[watch.state] = []; stateWatchers[watch.state].push(function (value, previous) { runStmt(watch.body, { value: value, previous: previous }); }); }); var updateSource = behavior.lifecycle && typeof behavior.lifecycle.update === "string" ? behavior.lifecycle.update.trim() : ""; if (updateSource) { anyStateListeners.add(function () { scheduleUpdateHook(el, function () { if (!disposed) runStmt(updateSource); }); }); } var mountSource = behavior.lifecycle && typeof behavior.lifecycle.mount === "string" ? behavior.lifecycle.mount.trim() : ""; var unmountSource = behavior.lifecycle && typeof behavior.lifecycle.unmount === "string" ? behavior.lifecycle.unmount.trim() : ""; var instance = { dispose: function () { if (disposed) return; disposed = true; cleanupCallbacks.splice(0).reverse().forEach(function (cleanup) { try { cleanup(); } catch (_) { /* ignore cleanup errors */ } }); pendingUpdateHooks.delete(el); if (unmountSource) { try { runStmt(unmountSource); } catch (error) { console.error("[wrnexus] component unmount hook failed", error); } } mountedBehaviorRoots.delete(el); behaviorInstances.delete(el); delete el.__wrnexusScopeApi; el.querySelectorAll("[data-wrn-select]").forEach(function (select) { delete select.__wrnexusScopeApi; }); el.__wrnexusScope = false; }, }; behaviorInstances.set(el, instance); mountedBehaviorRoots.add(el); if (mountSource) { queueMicrotask(function () { if (!disposed && el.isConnected) { try { runStmt(mountSource); } catch (error) { console.error("[wrnexus] component mount hook failed", error); } } }); } } renderAll(); } function disposeBehaviors(root) { var elements = []; if (root && root.nodeType === 1 && root.hasAttribute("data-wrn-behavior")) { elements.push(root); } if (root && root.querySelectorAll) { Array.prototype.push.apply(elements, root.querySelectorAll("[data-wrn-behavior]")); } elements.reverse().forEach(function (element) { var instance = behaviorInstances.get(element); if (instance) instance.dispose(); }); } function ensureBehaviorObserver() { if (behaviorObserver || typeof MutationObserver === "undefined") return; behaviorObserver = new MutationObserver(function (records) { records.forEach(function (record) { Array.prototype.forEach.call(record.removedNodes, function (node) { if (node && node.nodeType === 1) disposeBehaviors(node); }); }); }); behaviorObserver.observe(document.documentElement, { childList: true, subtree: true }); } function queueScopeHydration(element) { if (!element || element.__wrnexusScope || element.__wrnexusHydrationQueued) return; var runtime = element.getAttribute("data-wrn-runtime") || "universal"; var strategy = element.getAttribute("data-wrn-hydrate") || "load"; if (runtime === "server" || strategy === "none") return; element.__wrnexusHydrationQueued = true; function hydrate() { if (element.__wrnexusScope || !element.isConnected) return; var started = performance.now ? performance.now() : Date.now(); function complete(module) { if (element.__wrnexusScope || !element.isConnected) return; if (module) element.__wrnexusClientModule = module; setupScope(element); var ended = performance.now ? performance.now() : Date.now(); try { element.dispatchEvent(new CustomEvent("wrnexus:hydrated", { bubbles: true, detail: { id: element.getAttribute("data-wrn-hydration") || null, strategy: strategy, durationMs: Math.max(0, ended - started), }, })); } catch (_) {} } var moduleUrl = element.getAttribute("data-wrn-client-module"); if (!moduleUrl || moduleUrl === "__WRNEXUS_CLIENT_MODULE__") { complete(null); return; } loadClientModule(element).then(complete); } if (strategy === "load") { hydrate(); return; } if (strategy === "idle") { var idle = window.requestIdleCallback || function (callback) { return window.setTimeout(callback, 1); }; idle(hydrate); return; } if (strategy === "visible" && typeof IntersectionObserver !== "undefined") { var observer = new IntersectionObserver(function (entries) { if (!entries.some(function (entry) { return entry.isIntersecting; })) return; observer.disconnect(); hydrate(); }); observer.observe(element); return; } if (strategy === "interaction") { var activate = function () { element.removeEventListener("pointerdown", activate, true); element.removeEventListener("keydown", activate, true); element.removeEventListener("focusin", activate, true); hydrate(); }; element.addEventListener("pointerdown", activate, true); element.addEventListener("keydown", activate, true); element.addEventListener("focusin", activate, true); return; } if (strategy.indexOf("media:") === 0 && typeof window.matchMedia === "function") { var query = strategy.slice(6); var media = window.matchMedia(query); if (media.matches) hydrate(); else { var onChange = function (event) { if (!event.matches) return; if (media.removeEventListener) media.removeEventListener("change", onChange); else media.removeListener(onChange); hydrate(); }; if (media.addEventListener) media.addEventListener("change", onChange); else media.addListener(onChange); } return; } reportDiagnostic( "WRN-HYDRATE-STRATEGY", "Unknown hydration strategy '" + strategy + "'. Falling back to load.", element, ); hydrate(); } /* * Keep anchored overlays inside the viewport. * * A popover, tooltip or menu is positioned purely in CSS, relative to its * trigger. That is correct until the trigger sits near an edge: the panel * then hangs off the side of the screen and is unreadable and unclickable. * Every one of these components had that problem, so the nudge lives here * once rather than five times. * * The offset is applied through the standalone translate property, NOT * transform: several placements already use transform to centre themselves * with translateX(-50%), and the two properties compose instead of * overwriting each other. Measurement always happens with the offset * cleared, so repositioning is idempotent -- re-running it on a panel that * is already on screen changes nothing and cannot drift. */ var ANCHORED_SELECTOR = "[data-wrn-anchored]"; var ANCHOR_MARGIN = 8; var anchoredScheduled = false; // True while the clamp is writing, so its own style writes are not mistaken // for a change that needs repositioning. Without this the observer and the // clamp chase each other forever and the offset ends up cleared as often as // it is applied. var anchoredWriting = false; function isRenderedElement(element) { if (!element || !element.getBoundingClientRect) return false; var rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } function clampAnchored(element) { // Measure the untouched position; the offset is recomputed from scratch. element.style.translate = ""; var arrow = element.querySelector("[data-wrn-anchor-arrow]"); if (arrow) arrow.style.translate = ""; if (!isRenderedElement(element)) return; var rect = element.getBoundingClientRect(); var viewportWidth = window.innerWidth || document.documentElement.clientWidth; var viewportHeight = window.innerHeight || document.documentElement.clientHeight; /* * Clamp only while the overlay's own trigger is on screen. * * Judging by the PANEL alone gets both cases wrong. An overlay whose * trigger has scrolled away sits far outside the viewport quite * legitimately, and dragging it back tore it off its trigger. But a menu * that has only just opened can also start fully outside -- a top-placed * dropdown near the top of a phone screen renders above y=0 -- and that * one very much does need moving; skipping it left the menu invisible * with no way to reach it. The trigger tells the two apart. */ var host = element.closest("[data-ui-component]") || element.parentElement; var anchor = host ? host.getBoundingClientRect() : rect; var anchorVisible = anchor.bottom > 0 && anchor.top < viewportHeight && anchor.right > 0 && anchor.left < viewportWidth; if (!anchorVisible) { return; } var shiftX = 0; var shiftY = 0; if (rect.width < viewportWidth - ANCHOR_MARGIN * 2) { if (rect.left < ANCHOR_MARGIN) shiftX = ANCHOR_MARGIN - rect.left; else if (rect.right > viewportWidth - ANCHOR_MARGIN) { shiftX = viewportWidth - ANCHOR_MARGIN - rect.right; } } if (rect.height < viewportHeight - ANCHOR_MARGIN * 2) { if (rect.top < ANCHOR_MARGIN) shiftY = ANCHOR_MARGIN - rect.top; else if (rect.bottom > viewportHeight - ANCHOR_MARGIN) { shiftY = viewportHeight - ANCHOR_MARGIN - rect.bottom; } } if (!shiftX && !shiftY) return; element.style.translate = shiftX + "px " + shiftY + "px"; /* * The arrow lives inside the panel, so it travels with it and stops * pointing at the trigger. Move it back by the same amount -- but keep it * inside the panel, an arrow detached from its own bubble looks worse * than one slightly off-centre. */ if (!arrow) return; var arrowRect = arrow.getBoundingClientRect(); var limitX = Math.max(0, rect.width / 2 - arrowRect.width); var limitY = Math.max(0, rect.height / 2 - arrowRect.height); var arrowX = Math.max(-limitX, Math.min(limitX, -shiftX)); var arrowY = Math.max(-limitY, Math.min(limitY, -shiftY)); if (arrowX || arrowY) { arrow.style.translate = arrowX + "px " + arrowY + "px"; } } function repositionAnchored(root) { var host = root && root.querySelectorAll ? root : document; anchoredWriting = true; try { Array.prototype.slice .call(host.querySelectorAll(ANCHORED_SELECTOR)) .forEach(clampAnchored); } finally { // Released on a timer, not requestAnimationFrame. rAF does not fire in // a background or non-compositing tab, and this flag gates every future // reposition -- releasing it from rAF would latch it on and silently // disable the whole feature for the life of the page. window.setTimeout(function () { anchoredWriting = false; }, 0); } } function scheduleAnchoredReposition() { if (anchoredWriting || anchoredScheduled) return; anchoredScheduled = true; // setTimeout rather than requestAnimationFrame for the same reason: an // overlay opened while the tab is in the background must still be placed // correctly, and rAF simply never runs there. window.setTimeout(function () { anchoredScheduled = false; repositionAnchored(document); /* * A second pass shortly after. The mutation that opens an overlay is * usually the same one that makes it visible, so on the very next frame * it can still measure as hidden (zero-sized) -- the clamp then skips it * and, with no further mutation coming, nothing would ever reposition * it. Re-running once the panel has actually been laid out fixes that, * and because the clamp recomputes from a cleared offset the repeat is * free of side effects. */ window.setTimeout(function () { repositionAnchored(document); }, 60); }); } function setupAnchoredOverlays() { if (window.__wrnexusAnchoredBound) return; window.__wrnexusAnchoredBound = true; // Opening is expressed differently by each component (data-open on the // root, data-show on the panel, a class), so watch for any attribute or // structural change and re-measure on the next frame instead of trying to // enumerate every signal. if (typeof MutationObserver === "function") { new MutationObserver(scheduleAnchoredReposition).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-open", "data-show", "class", "data-placement"], }); } window.addEventListener("resize", scheduleAnchoredReposition); window.addEventListener("scroll", scheduleAnchoredReposition, true); } /* * Modal dialog behaviour: focus, focus restore, a Tab trap and a scroll lock. * * These live here rather than in Modal.wrn and Drawer.wrn because every one * of them is the same code, and because a client function cannot hold the * "element that had focus before we opened" across a close -- state written * after an await or inside a callback is dropped. * * Fixing focus also repairs Escape. Both components bind @keydown on their * own root, so the handler only ever runs when focus is inside the dialog; * until something moved focus there, closeOnEscape did nothing at all. */ var DIALOG_SELECTOR = '[role="dialog"][aria-modal="true"]'; var FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled])," + ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; var openDialogs = []; var dialogRestoreFocus = null; var dialogScrollLock = null; /* * Open/closed is expressed by the data-show marker these components already * emit, not by measured size. Measuring looks more thorough but made the * trap and the scroll lock impossible to cover: the test DOM reports every * element as zero-sized, so a dialog never counted as open and none of this * behaviour ran under test. */ function isDialogVisible(dialog) { if (!dialog || !dialog.isConnected) return false; if (dialog.hasAttribute("hidden")) return false; if (dialog.closest('[data-show="false"]')) return false; return true; } function focusableWithin(dialog) { var found = []; var candidates = dialog.querySelectorAll(FOCUSABLE_SELECTOR); for (var index = 0; index < candidates.length; index += 1) { var candidate = candidates[index]; // Same rule as the roving items: markers, not measurement. if (candidate.hasAttribute("hidden")) continue; if (candidate.closest('[data-show="false"]')) continue; found.push(candidate); } return found; } function lockDialogScroll() { if (dialogScrollLock) return; var body = document.body; if (!body) return; dialogScrollLock = { overflow: body.style.overflow }; body.style.overflow = "hidden"; } function unlockDialogScroll() { if (!dialogScrollLock || !document.body) return; document.body.style.overflow = dialogScrollLock.overflow; dialogScrollLock = null; } function onDialogOpened(dialog) { // Only the first dialog records the restore target; a dialog opened from // inside another must not overwrite the page element we came from. if (!openDialogs.length) { var active = document.activeElement; dialogRestoreFocus = active && active !== document.body ? active : null; lockDialogScroll(); } openDialogs.push(dialog); // Prefer a real control so keyboard users land somewhere useful, and fall // back to the panel itself, which carries tabindex="-1" for exactly this. var targets = focusableWithin(dialog); var target = targets.length ? targets[0] : dialog; if (target && target.focus) target.focus(); } function onDialogClosed(dialog) { var at = openDialogs.indexOf(dialog); if (at !== -1) openDialogs.splice(at, 1); if (openDialogs.length) return; unlockDialogScroll(); if (dialogRestoreFocus && dialogRestoreFocus.focus && dialogRestoreFocus.isConnected) { dialogRestoreFocus.focus(); } dialogRestoreFocus = null; } function syncDialogs() { var dialogs = document.querySelectorAll(DIALOG_SELECTOR); for (var index = 0; index < dialogs.length; index += 1) { var dialog = dialogs[index]; var visible = isDialogVisible(dialog); var tracked = openDialogs.indexOf(dialog) !== -1; if (visible && !tracked) onDialogOpened(dialog); else if (!visible && tracked) onDialogClosed(dialog); } // A dialog can be removed from the document outright rather than hidden. for (var open = openDialogs.length - 1; open >= 0; open -= 1) { if (!openDialogs[open].isConnected) onDialogClosed(openDialogs[open]); } } function trapDialogTab(event) { if (event.key !== "Tab" || !openDialogs.length) return; var dialog = openDialogs[openDialogs.length - 1]; var targets = focusableWithin(dialog); if (!targets.length) { // Nothing to cycle through; keep focus on the panel rather than letting // Tab walk out into the page sitting behind the backdrop. event.preventDefault(); if (dialog.focus) dialog.focus(); return; } var first = targets[0]; var last = targets[targets.length - 1]; var active = document.activeElement; if (event.shiftKey && (active === first || !dialog.contains(active))) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); } } function setupModalDialogs() { if (window.__wrnexusDialogsBound) return; window.__wrnexusDialogsBound = true; if (typeof MutationObserver === "function") { new MutationObserver(function () { // Same deferral as the anchored clamp: the mutation that opens a // dialog is the one that makes it visible, so it still measures as // hidden until the panel has been laid out. window.setTimeout(syncDialogs, 0); }).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-open", "data-show", "class", "style", "hidden"], }); } document.addEventListener("keydown", trapDialogTab, true); syncDialogs(); } /* * Roving arrow-key focus, the ARIA pattern shared by tabs, menus, sidebars * and steppers. A container marked data-wrn-roving owns its * [data-wrn-roving-item] descendants: exactly one carries tabindex="0" so * Tab reaches the group once, and the arrow keys move within it. * * This lives here rather than in each component because it is the same code * five times over, and because focus bookkeeping cannot be held in component * state -- a client function writing after it returns has that write dropped. */ var ROVING_SELECTOR = "[data-wrn-roving]"; var ROVING_ITEM_SELECTOR = "[data-wrn-roving-item]"; /* * Templates stringify these, so data-wrn-roving="" and * data-wrn-roving-item="false" mean "not this time". A bare [attr] selector * matches either, so the value must be checked. */ // A bare data-wrn-roving-item means yes; only an explicit "false" opts out. function rovingItemOff(value) { return value === null || value === "false"; } /* * The container is stricter: it must name an axis. An empty value is what a * template emits for {cond ? "horizontal" : ""}, so empty means off rather * than defaulting to horizontal. */ function rovingOrientation(container) { var value = container.getAttribute("data-wrn-roving"); if (value === null || value === "" || value === "false") return ""; return value; } function rovingItems(container) { var found = []; var candidates = container.querySelectorAll(ROVING_ITEM_SELECTOR); for (var index = 0; index < candidates.length; index += 1) { var candidate = candidates[index]; if (rovingItemOff(candidate.getAttribute("data-wrn-roving-item"))) continue; // A nested group owns its own items; do not steal them. if (candidate.closest(ROVING_SELECTOR) !== container) continue; if (candidate.hasAttribute("disabled")) continue; if (candidate.getAttribute("aria-disabled") === "true") continue; // Markers, not measurement: the test DOM reports every element as // zero-sized, which is what left the dialog trap uncovered. if (candidate.hasAttribute("hidden")) continue; if (candidate.closest('[data-show="false"]')) continue; found.push(candidate); } return found; } function rovingActiveIndex(items) { for (var index = 0; index < items.length; index += 1) { var item = items[index]; if (item.getAttribute("aria-selected") === "true") return index; var current = item.getAttribute("aria-current"); if (current === "page" || current === "step" || current === "true") return index; } return 0; } function applyRovingTabindex(items, activeIndex) { for (var index = 0; index < items.length; index += 1) { items[index].setAttribute("tabindex", index === activeIndex ? "0" : "-1"); } } function syncRovingGroup(container) { if (!rovingOrientation(container)) return; var items = rovingItems(container); if (!items.length) return; applyRovingTabindex(items, rovingActiveIndex(items)); } function syncRovingGroups() { var groups = document.querySelectorAll(ROVING_SELECTOR); for (var index = 0; index < groups.length; index += 1) syncRovingGroup(groups[index]); } function handleRovingKeydown(event) { var target = event.target; if (!target || !target.closest) return; var item = target.closest(ROVING_ITEM_SELECTOR); if (!item) return; var container = item.closest(ROVING_SELECTOR); if (!container) return; var items = rovingItems(container); var index = items.indexOf(item); if (index === -1) return; var orientation = rovingOrientation(container); if (!orientation) return; var horizontal = orientation === "horizontal" || orientation === "both"; var vertical = orientation === "vertical" || orientation === "both"; var key = event.key; var next = -1; if ((horizontal && key === "ArrowRight") || (vertical && key === "ArrowDown")) { next = (index + 1) % items.length; } else if ((horizontal && key === "ArrowLeft") || (vertical && key === "ArrowUp")) { next = (index - 1 + items.length) % items.length; } else if (key === "Home") { next = 0; } else if (key === "End") { next = items.length - 1; } else { return; } event.preventDefault(); applyRovingTabindex(items, next); if (items[next].focus) items[next].focus(); } function setupRovingFocus() { if (window.__wrnexusRovingBound) return; window.__wrnexusRovingBound = true; document.addEventListener("keydown", handleRovingKeydown, true); if (typeof MutationObserver === "function") { new MutationObserver(function () { window.setTimeout(syncRovingGroups, 0); }).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-selected", "aria-current", "disabled", "aria-disabled", "hidden"], }); } syncRovingGroups(); } function hydrateScopes(root) { var host = root || document; var scopeSelector = "[data-scope], [data-wrn-scope]"; if ( host.nodeType === 1 && host.matches && host.matches(scopeSelector) ) { queueScopeHydration(host); } if (host.querySelectorAll) { host .querySelectorAll(scopeSelector) .forEach(queueScopeHydration); } ensureBehaviorObserver(); hydrateNavbarControllers(host); hydratePreferenceControllers(host); hydrateSidebarControllers(host); hydrateDropdownControllers(host); hydrateSelectControllers(host); hydratePinInputControllers(host); } var navbarOutsideClickBound = false; var preferenceOutsideClickBound = false; var dropdownOutsideClickBound = false; function hydrateSidebarControllers(root) { var host = root || document; var sidebars = []; if (host.nodeType === 1 && host.matches && host.matches(".wire-sidebar-shell")) { sidebars.push(host); } if (host.querySelectorAll) { Array.prototype.push.apply(sidebars, host.querySelectorAll(".wire-sidebar-shell")); } sidebars.forEach(function (sidebar) { var current = window.location.pathname.replace(/\/+$/, "") || "/"; var best = null; var bestLength = -1; sidebar.querySelectorAll(".wire-sidebar-items a[href]").forEach(function (link) { var url; try { url = new URL(link.getAttribute("href"), window.location.origin); } catch (_) { return; } var path = url.pathname.replace(/\/+$/, "") || "/"; var matches = path === current || (path !== "/" && current.indexOf(path + "/") === 0); link.classList.remove("is-active"); if (!matches || path.length <= bestLength) return; best = link; bestLength = path.length; }); sidebar.querySelectorAll(".wire-sidebar-items a[aria-current='page']").forEach(function (link) { if (link !== best) link.removeAttribute("aria-current"); }); sidebar.querySelectorAll(".wire-sidebar-group.has-active").forEach(function (group) { group.classList.remove("has-active"); }); if (best) { best.classList.add("is-active"); best.setAttribute("aria-current", "page"); var group = best.closest(".wire-sidebar-group"); if (group) { group.setAttribute("open", ""); group.classList.add("has-active"); } } }); } function hydrateDropdownControllers(root) { var host = root || document; var dropdowns = []; if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-dropdown]")) { dropdowns.push(host); } if (host.querySelectorAll) { Array.prototype.push.apply(dropdowns, host.querySelectorAll("[data-wrn-dropdown]")); } dropdowns.forEach(function (dropdown) { if (dropdown.dataset.wrnDropdownBound === "true") return; dropdown.dataset.wrnDropdownBound = "true"; dropdown.addEventListener("toggle", function () { if (!dropdown.open) return; document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (candidate) { if (candidate !== dropdown) candidate.removeAttribute("open"); }); }); }); if (!dropdownOutsideClickBound) { dropdownOutsideClickBound = true; document.addEventListener("pointerdown", function (event) { document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (dropdown) { if (!dropdown.contains(event.target)) dropdown.removeAttribute("open"); }); }); document.addEventListener("keydown", function (event) { if (event.key !== "Escape") return; document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (dropdown) { dropdown.removeAttribute("open"); var summary = dropdown.querySelector(":scope > summary"); if (summary) summary.focus(); }); }); } } function hydratePreferenceControllers(root) { var host = root || document; var switchers = []; if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-preferences]")) { switchers.push(host); } if (host.querySelectorAll) { Array.prototype.push.apply( switchers, host.querySelectorAll("[data-wrn-preferences]"), ); } switchers.forEach(function (switcher) { if (switcher.dataset.wrnPreferencesBound === "true") return; switcher.dataset.wrnPreferencesBound = "true"; switcher.addEventListener("toggle", function (event) { var opened = event.target; if (!opened || opened.tagName !== "DETAILS" || !opened.open) return; switcher.querySelectorAll(".wire-preferences__menu[open]").forEach(function (menu) { if (menu !== opened) menu.removeAttribute("open"); }); }, true); }); if (!preferenceOutsideClickBound) { preferenceOutsideClickBound = true; document.addEventListener("pointerdown", function (event) { document.querySelectorAll("[data-wrn-preferences]").forEach(function (switcher) { if (switcher.contains(event.target)) return; switcher.querySelectorAll(".wire-preferences__menu[open]").forEach(function (menu) { menu.removeAttribute("open"); }); }); }); document.addEventListener("keydown", function (event) { if (event.key !== "Escape") return; document.querySelectorAll("[data-wrn-preferences] .wire-preferences__menu[open]").forEach(function (menu) { menu.removeAttribute("open"); var summary = menu.querySelector(":scope > summary"); if (summary) summary.focus(); }); }); } } function hydrateNavbarControllers(root) { var host = root || document; var navbars = []; if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-navbar]")) { navbars.push(host); } if (host.querySelectorAll) { Array.prototype.push.apply( navbars, host.querySelectorAll("[data-wrn-navbar]"), ); } navbars.forEach(function (navbar) { var current = window.location.pathname.replace(/\/+$/, "") || "/"; var best = null; var bestLength = -1; navbar.querySelectorAll(".wire-navbar__menus a[href]").forEach(function (link) { var url; try { url = new URL(link.getAttribute("href"), window.location.origin); } catch (_) { return; } if (url.origin !== window.location.origin) return; var path = url.pathname.replace(/\/+$/, "") || "/"; var matches = path === current || (path !== "/" && current.indexOf(path + "/") === 0); if (!matches || path.length <= bestLength) return; best = link; bestLength = path.length; }); navbar.querySelectorAll(".wire-navbar__menus [aria-current='page']").forEach(function (item) { item.removeAttribute("aria-current"); }); if (best) { best.setAttribute("aria-current", "page"); var parentMenu = best.closest(".wire-navbar__dropdown"); var parentSummary = parentMenu && parentMenu.querySelector(":scope > summary"); if (parentSummary) parentSummary.setAttribute("aria-current", "page"); } if (navbar.dataset.wrnNavbarBound === "true") return; navbar.dataset.wrnNavbarBound = "true"; var hoverEnabled = navbar.getAttribute("data-open-on-hover") === "true"; var hoverCapable = typeof window.matchMedia !== "function" || window.matchMedia("(hover: hover) and (pointer: fine)").matches; if (hoverEnabled && hoverCapable) { navbar.querySelectorAll(".wire-navbar__dropdown").forEach(function (menu) { menu.addEventListener("pointerenter", function () { if (menu.__wrnNavbarCloseTimer) { clearTimeout(menu.__wrnNavbarCloseTimer); menu.__wrnNavbarCloseTimer = null; } navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (candidate) { if (candidate !== menu) candidate.removeAttribute("open"); }); menu.setAttribute("open", ""); }); menu.addEventListener("pointerleave", function () { if (menu.__wrnNavbarCloseTimer) clearTimeout(menu.__wrnNavbarCloseTimer); menu.__wrnNavbarCloseTimer = setTimeout(function () { menu.removeAttribute("open"); menu.__wrnNavbarCloseTimer = null; }, 240); }); }); } navbar.addEventListener("toggle", function (event) { var opened = event.target; if (!opened || opened.tagName !== "DETAILS" || !opened.open) return; navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (menu) { if (menu !== opened) menu.removeAttribute("open"); }); }, true); }); if (!navbarOutsideClickBound) { navbarOutsideClickBound = true; document.addEventListener("pointerdown", function (event) { document.querySelectorAll("[data-wrn-navbar]").forEach(function (navbar) { if (navbar.contains(event.target)) return; navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (menu) { menu.removeAttribute("open"); }); }); }); document.addEventListener("keydown", function (event) { if (event.key !== "Escape") return; document.querySelectorAll("[data-wrn-navbar] .wire-navbar__dropdown[open]").forEach(function (menu) { menu.removeAttribute("open"); var summary = menu.querySelector(":scope > summary"); if (summary) summary.focus(); }); }); } } var selectOutsideClickBound = false; function selectScopeApi(root) { var scope = root; while (scope) { if (scope.__wrnexusScopeApi) return scope.__wrnexusScopeApi; scope = scope.parentElement; } return null; } function selectPayloadOptions(payload) { if (Array.isArray(payload)) return payload; if (!payload || typeof payload !== "object") return []; if (Array.isArray(payload.options)) return payload.options; if (Array.isArray(payload.items)) return payload.items; if (Array.isArray(payload.results)) return payload.results; if (Array.isArray(payload.data)) return payload.data; return []; } function syncComboboxInput(root) { if (!root || !root.hasAttribute("data-wrn-combobox")) return; var input = root.querySelector(".wire-next__combobox-input"); var api = selectScopeApi(root); if (!input || !api) return; var value = api.call("inputText") || ""; input.value = String(value); input.setAttribute("value", String(value)); } function selectEventDetail(root, extra) { var api = selectScopeApi(root); var multiple = !!(api && api.get("multiple")); var detail = { component: root.hasAttribute("data-wrn-combobox") ? "ComboBox" : "AdvancedSelect", query: api ? (api.get("query") || "") : "", value: api ? (api.get("selectedValue") || "") : "", values: api && Array.isArray(api.get("selectedValues")) ? api.get("selectedValues") : [], multiple: multiple, selectedOptions: api ? (api.call("selectedOptions") || []) : [], }; if (extra && typeof extra === "object") { Object.keys(extra).forEach(function (key) { detail[key] = extra[key]; }); } return detail; } function emitSelectEvent(root, name, extra) { try { dispatchComponentEvent(root, name, selectEventDetail(root, extra)); } catch (_) { // CustomEvent can be unavailable in minimal DOM environments. } } function renderRemoteSelectOptions(root, options) { var list = root.querySelector(".wire-next__select-list"); if (!list) return; list.querySelectorAll(".wire-next__option-group, .wire-next__select-option, .wire-next__select-message") .forEach(function (node) { node.remove(); }); options.forEach(function (option) { if (!option || option.value == null) return; var button = document.createElement("button"); button.type = "button"; button.className = "wire-next__select-option"; button.setAttribute("role", "option"); button.setAttribute("data-option-value", String(option.value)); button.disabled = !!option.disabled; if (option.icon) { var icon = document.createElement("i"); icon.className = String(option.icon); icon.setAttribute("aria-hidden", "true"); button.appendChild(icon); } else if (option.avatar) { var avatar = document.createElement("img"); avatar.src = String(option.avatar); avatar.alt = ""; button.appendChild(avatar); } else if (option.color) { var dot = document.createElement("i"); dot.className = "wire-next__color-dot"; dot.style.setProperty("--option-color", String(option.color)); button.appendChild(dot); } var copy = document.createElement("span"); var label = document.createElement("strong"); label.textContent = String(option.label == null ? option.value : option.label); copy.appendChild(label); if (option.description) { var description = document.createElement("small"); description.textContent = String(option.description); copy.appendChild(description); } button.appendChild(copy); var check = document.createElement("i"); check.className = "wire-next__check icon-[lucide--check]"; check.setAttribute("aria-hidden", "true"); check.style.display = "none"; button.appendChild(check); button.addEventListener("click", function () { var api = selectScopeApi(root); if (!api || button.disabled) return; api.call("chooseOption", option); window.setTimeout(function () { var selected = api.call("isSelected", option); button.classList.toggle("wire-next__select-option--selected", !!selected); button.setAttribute("aria-selected", selected ? "true" : "false"); check.style.display = selected ? "" : "none"; syncComboboxInput(root); }, 0); }); list.appendChild(button); }); } function setupSelectController(root) { if (!root || root.__wrnexusSelectController) return; root.__wrnexusSelectController = { attempts: 0 }; var remote = root.getAttribute("data-remote") === "true"; var remoteUrl = root.getAttribute("data-remote-url") || ""; var input = root.querySelector(".wire-next__select-search input, .wire-next__combobox-input"); var list = root.querySelector(".wire-next__select-list"); var loadMore = root.querySelector("[data-wrn-select-load-more]"); var timer = 0; var requestId = 0; var loadedOptions = []; var retainedSelections = []; var currentPage = Number(root.getAttribute("data-page") || 1); function setLoading(loading) { root.setAttribute("data-loading", loading ? "true" : "false"); if (list) list.setAttribute("aria-busy", loading ? "true" : "false"); var api = selectScopeApi(root); if (api) api.set("loading", loading); } function load(append, allowEmptyQuery) { root.__wrnexusSelectController.attempts++; if (!remote || !remoteUrl) return; var api = selectScopeApi(root); if (!api) return; var query = input ? input.value : ""; var minimum = Number(api.get("minSearchLength") || 0); if (!allowEmptyQuery && query.length < minimum) return; var url; try { url = new URL(remoteUrl, window.location.href); } catch (_) { return; } var parameter = root.getAttribute("data-remote-query-param") || "q"; if (query) url.searchParams.set(parameter, query); var nextPage = append ? currentPage + 1 : 1; url.searchParams.set("page", String(nextPage)); var thisRequest = ++requestId; setLoading(true); fetch(url.toString(), { headers: { accept: "application/json" } }) .then(function (response) { if (!response.ok) throw new Error("HTTP " + response.status); return response.json(); }) .then(function (payload) { if (thisRequest !== requestId) return; var nextOptions = selectPayloadOptions(payload); loadedOptions = append ? loadedOptions.concat(nextOptions) : nextOptions; currentPage = nextPage; var selectedValues = api.get("selectedValues"); var selectedValue = api.get("selectedValue"); var selectedKeys = api.get("multiple") ? (Array.isArray(selectedValues) ? selectedValues : []) : (selectedValue ? [selectedValue] : []); var previousOptions = api.call("allOptions") || []; retainedSelections = retainedSelections .concat(previousOptions) .concat(loadedOptions) .filter(function (option, index, list) { return option && selectedKeys.includes(option.value) && list.findIndex(function (item) { return item && item.value === option.value; }) === index; }); var stateOptions = retainedSelections.concat(loadedOptions).filter(function (option, index, list) { return option && list.findIndex(function (item) { return item && item.value === option.value; }) === index; }); api.set("options", stateOptions); api.set("groups", []); api.set("page", currentPage); api.set("hasMore", !!(payload && (payload.hasMore || payload.nextPage || payload.next))); renderRemoteSelectOptions(root, loadedOptions); emitSelectEvent(root, "load", { options: nextOptions, page: currentPage, hasMore: !!(payload && (payload.hasMore || payload.nextPage || payload.next)), append: !!append, url: url.toString(), payload: payload, }); }) .catch(function (error) { console.error("[wrnexus] advanced select remote request failed", error); emitSelectEvent(root, "error", { error: error, message: error && error.message ? error.message : String(error), url: url.toString(), }); }) .then(function () { if (thisRequest === requestId) setLoading(false); }); } if (input) { input.addEventListener("input", function () { emitSelectEvent(root, "search", { query: input.value }); if (!remote) return; window.clearTimeout(timer); var debounce = Number(root.getAttribute("data-remote-debounce") || 250); timer = window.setTimeout(function () { load(false); }, debounce); }); } if (loadMore) { loadMore.addEventListener("click", function () { load(true); }); } if (list && root.getAttribute("data-infinite") === "true") { list.addEventListener("scroll", function () { if (list.scrollTop + list.clientHeight < list.scrollHeight - 24) return; var api = selectScopeApi(root); if (api && api.get("hasMore") && root.getAttribute("data-loading") !== "true") load(true); }); } root.addEventListener("click", function (event) { if (!remote || !remoteUrl) return; if (!event.target.closest(".wire-next__select-trigger")) return; if (loadedOptions.length === 0) load(false); }); root.__wrnexusSelectController.load = load; var dropdown = root.querySelector(".wire-next__select-dropdown"); var lastOpen = !!(selectScopeApi(root) && selectScopeApi(root).get("open")); function emitOpenStateIfChanged() { var api = selectScopeApi(root); var nextOpen = !!(api && api.get("open")); if (nextOpen === lastOpen) return; lastOpen = nextOpen; emitSelectEvent(root, nextOpen ? "open" : "close"); } var ObserverConstructor = root.ownerDocument && root.ownerDocument.defaultView && root.ownerDocument.defaultView.MutationObserver ? root.ownerDocument.defaultView.MutationObserver : (typeof MutationObserver !== "undefined" ? MutationObserver : null); if (ObserverConstructor) { new ObserverConstructor(function () { emitOpenStateIfChanged(); }).observe(dropdown || root, { attributes: true, attributeFilter: dropdown ? ["data-show", "style"] : ["class"], }); } root.addEventListener("click", function (event) { var optionButton = event.target.closest(".wire-next__select-option"); if (optionButton) { window.setTimeout(function () { var api = selectScopeApi(root); var optionValue = optionButton.getAttribute("data-option-value"); var options = api ? (api.call("allOptions") || []) : []; var option = options.find(function (item) { return item && String(item.value) === String(optionValue); }); emitSelectEvent(root, "select", { option: option || null }); emitSelectEvent(root, "change", { option: option || null, reason: "select" }); emitOpenStateIfChanged(); }, 0); } if (event.target.closest(".wire-next__clear-select")) { window.setTimeout(function () { emitSelectEvent(root, "clear"); emitSelectEvent(root, "change", { reason: "clear" }); }, 0); } }, true); root.addEventListener("keydown", function (event) { if (event.key !== "Enter") return; var before = selectEventDetail(root); window.setTimeout(function () { var after = selectEventDetail(root); var beforeSelection = before.multiple ? JSON.stringify(before.values) : before.value; var afterSelection = after.multiple ? JSON.stringify(after.values) : after.value; if (afterSelection === beforeSelection) return; syncComboboxInput(root); emitSelectEvent(root, "select", { source: "keyboard" }); emitSelectEvent(root, "change", { reason: "select", source: "keyboard" }); emitOpenStateIfChanged(); }, 0); }, true); if (root.hasAttribute("data-wrn-combobox")) { root.wrnexusCombobox = { open: function () { var api = selectScopeApi(root); if (api) api.call("openDropdown"); emitOpenStateIfChanged(); }, close: function () { var api = selectScopeApi(root); if (api) api.call("closeDropdown"); emitOpenStateIfChanged(); }, clear: function () { var api = selectScopeApi(root); if (api) api.call("clearValue"); syncComboboxInput(root); emitSelectEvent(root, "clear", { source: "method" }); emitSelectEvent(root, "change", { reason: "clear", source: "method" }); }, setValue: function (value) { var api = selectScopeApi(root); if (api) api.call("setValue", value); syncComboboxInput(root); emitSelectEvent(root, "change", { reason: "setValue", source: "method" }); }, reload: function () { load(false, true); }, }; root.addEventListener("click", function (event) { if (!event.target.closest(".wire-next__select-option, .wire-next__clear-select")) return; window.setTimeout(function () { syncComboboxInput(root); }, 0); }); if (input) { input.addEventListener("keydown", function (event) { if (event.key === "Enter") { window.setTimeout(function () { syncComboboxInput(root); }, 0); } }); } } if (remote && remoteUrl && root.getAttribute("data-remote-auto-load") !== "false") { load(false, true); } } function hydrateSelectControllers(root) { var host = root || document; var selects = []; if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-select]")) selects.push(host); if (host.querySelectorAll) { Array.prototype.push.apply(selects, host.querySelectorAll("[data-wrn-select]")); } selects.forEach(setupSelectController); if (!selectOutsideClickBound) { selectOutsideClickBound = true; document.addEventListener("pointerdown", function (event) { document.querySelectorAll("[data-wrn-select].wire-next--open").forEach(function (select) { if (select.contains(event.target)) return; var api = selectScopeApi(select); if (api) api.set("open", false); }); }); } } function invokeComponentOutput(root, name, payload) { if (!root || !name) return undefined; var registry = root.__wrnexusOutputHandlers; var handlers = registry && registry[name]; if (handlers && handlers.size) { var values = []; handlers.forEach(function (handler) { values.push(handler(payload)); }); return values.some(function (value) { return value && typeof value.then === "function"; }) ? Promise.all(values) : values[values.length - 1]; } return dispatchComponentEvent(root, name, payload); } function callServerFunction(component, functionName, args) { var csrf = document.querySelector('meta[name="wrnexus-csrf"]'); return fetch("/__wrnexus/rpc", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", "x-csrf-token": csrf ? csrf.getAttribute("content") || "" : "", }, body: JSON.stringify({ component: component, function: functionName, args: args || [] }), }).then(function (response) { return response.json().catch(function () { return null; }).then(function (payload) { if (!response.ok || !payload || !payload.ok) { var error = new Error(payload && payload.error && payload.error.message || "Server function call failed"); error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED"; error.status = response.status; throw error; } return payload.value; }); }); } function dispatchComponentEvent(root, name, detail) { if (!root || !name) return null; var EventConstructor = root.ownerDocument && root.ownerDocument.defaultView && root.ownerDocument.defaultView.CustomEvent ? root.ownerDocument.defaultView.CustomEvent : CustomEvent; var event = new EventConstructor(String(name), { bubbles: true, detail: detail || {}, }); root.dispatchEvent(event); // Compatibility for applications using the former prefixed contract. root.dispatchEvent(new EventConstructor("wrnexus:" + String(name), { bubbles: true, detail: detail || {}, })); return event; } function emitPinInputEvent(root, name, extra) { var hidden = root.querySelector("[data-pin-value]"); var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]")); var value = hidden ? hidden.value : ""; var detail = { component: "PinInput", value: value, digits: cells.map(function (cell) { return cell.value; }), complete: value.length === cells.length, length: cells.length, }; if (extra && typeof extra === "object") { Object.keys(extra).forEach(function (key) { detail[key] = extra[key]; }); } try { dispatchComponentEvent(root, name, detail); } catch (_) { // CustomEvent can be unavailable in minimal DOM environments. } } function setupPinInputController(root) { if (!root || root.__wrnexusPinInputController) return; var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]")); var hidden = root.querySelector("[data-pin-value]"); var clearButton = root.querySelector("[data-pin-clear]"); var patternSource = root.getAttribute("data-pattern") || "[0-9]"; var allowPaste = root.getAttribute("data-allow-paste") !== "false"; var autoSubmit = root.getAttribute("data-auto-submit") === "true"; var matcher; var exactMatcher; var NativeEventConstructor = root.ownerDocument && root.ownerDocument.defaultView && root.ownerDocument.defaultView.Event ? root.ownerDocument.defaultView.Event : Event; try { exactMatcher = new RegExp("^(?:" + patternSource + ")$"); matcher = new RegExp("^(?:" + patternSource + ")$", "i"); } catch (error) { matcher = /$a/; emitPinInputEvent(root, "error", { error: error, message: "Invalid PIN pattern: " + patternSource, }); } function acceptedCharacters(value) { return Array.from(String(value || "")).map(function (character) { matcher.lastIndex = 0; if (!matcher.test(character)) return ""; exactMatcher.lastIndex = 0; if (exactMatcher.test(character)) return character; var upper = character.toUpperCase(); exactMatcher.lastIndex = 0; if (exactMatcher.test(upper)) return upper; var lower = character.toLowerCase(); exactMatcher.lastIndex = 0; return exactMatcher.test(lower) ? lower : character; }).filter(Boolean); } function currentValue() { return cells.map(function (cell) { return cell.value; }).join(""); } function updateClearButton(value) { if (!clearButton) return; clearButton.style.display = value ? "" : "none"; } function syncValue(reason, index) { var previous = hidden ? hidden.value : ""; var value = currentValue(); if (hidden) { hidden.value = value; hidden.setAttribute("value", value); hidden.dispatchEvent(new NativeEventConstructor("input", { bubbles: true })); hidden.dispatchEvent(new NativeEventConstructor("change", { bubbles: true })); } root.setAttribute("data-value", value); root.setAttribute("data-complete", value.length === cells.length ? "true" : "false"); updateClearButton(value); emitPinInputEvent(root, "input", { reason: reason, index: index }); if (value !== previous) { emitPinInputEvent(root, "change", { reason: reason, index: index }); } if (value.length === cells.length && previous.length !== cells.length) { emitPinInputEvent(root, "complete", { reason: reason, index: index }); if (autoSubmit) { var form = root.closest("form"); if (form && typeof form.requestSubmit === "function") form.requestSubmit(); } } } function setValue(value, reason) { var characters = acceptedCharacters(value).slice(0, cells.length); cells.forEach(function (cell, index) { cell.value = characters[index] || ""; cell.setAttribute("value", cell.value); }); syncValue(reason || "setValue", characters.length ? characters.length - 1 : -1); } function focusCell(index) { var cell = cells[Math.max(0, Math.min(cells.length - 1, index))]; if (cell && !cell.disabled) { cell.focus(); if (typeof cell.select === "function") cell.select(); } } cells.forEach(function (cell, index) { cell.addEventListener("focus", function () { if (typeof cell.select === "function") cell.select(); }); cell.addEventListener("input", function () { var characters = acceptedCharacters(cell.value); cell.value = characters.length ? characters[characters.length - 1] : ""; cell.setAttribute("value", cell.value); syncValue("input", index); if (cell.value && index < cells.length - 1) focusCell(index + 1); }); cell.addEventListener("keydown", function (event) { if (event.key === "Backspace" && !cell.value && index > 0) { event.preventDefault(); cells[index - 1].value = ""; cells[index - 1].setAttribute("value", ""); syncValue("backspace", index - 1); focusCell(index - 1); } else if (event.key === "ArrowLeft" && index > 0) { event.preventDefault(); focusCell(index - 1); } else if (event.key === "ArrowRight" && index < cells.length - 1) { event.preventDefault(); focusCell(index + 1); } else if (event.key === "Home") { event.preventDefault(); focusCell(0); } else if (event.key === "End") { event.preventDefault(); focusCell(cells.length - 1); } }); cell.addEventListener("paste", function (event) { if (!allowPaste) return; var clipboard = event.clipboardData; if (!clipboard) return; event.preventDefault(); var characters = acceptedCharacters(clipboard.getData("text")).slice(0, cells.length - index); characters.forEach(function (character, offset) { cells[index + offset].value = character; cells[index + offset].setAttribute("value", character); }); syncValue("paste", index); emitPinInputEvent(root, "paste", { index: index, pasted: characters.join("") }); focusCell(Math.min(index + characters.length, cells.length - 1)); }); }); if (clearButton) { clearButton.addEventListener("click", function () { setValue("", "clear"); emitPinInputEvent(root, "clear"); focusCell(0); }); } root.wrnexusPinInput = { focus: function (index) { focusCell(Number(index || 0)); }, clear: function () { setValue("", "clear"); emitPinInputEvent(root, "clear", { source: "method" }); }, setValue: function (value) { setValue(value, "setValue"); }, getValue: function () { return currentValue(); }, }; root.__wrnexusPinInputController = root.wrnexusPinInput; setValue(hidden ? hidden.value : root.getAttribute("data-value"), "initial"); } function hydratePinInputControllers(root) { var host = root || document; var inputs = []; if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-pin-input]")) { inputs.push(host); } if (host.querySelectorAll) { Array.prototype.push.apply(inputs, host.querySelectorAll("[data-wrn-pin-input]")); } inputs.forEach(setupPinInputController); } function parseScopeDecl(decl) { var initial = {}; splitTopLevel(decl, ",").forEach(function (part) { var idx = findTopLevel(part, ":"); if (idx < 0) return; var name = part.slice(0, idx).trim(); var expr = part.slice(idx + 1).trim(); if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) return; try { initial[name] = evaluateExpression(expr, function () { return undefined; }); } catch (e) { console.error("[wrnexus] invalid data-scope value for '" + name + "'", e); } }); return initial; } function skipStatementWhitespace( source, index, ) { while ( index < source.length && /\s/.test(source[index]) ) { index++; } return index; } function findClosingDelimiter( source, start, opening, closing, ) { if (source[start] !== opening) { return -1; } var depth = 1; var quote = ""; var escaped = false; for ( var index = start + 1; index < source.length; index++ ) { var character = source[index]; if (quote) { if (escaped) { escaped = false; continue; } if (character === "\\") { escaped = true; continue; } if (character === quote) { quote = ""; } continue; } if ( character === '"' || character === "'" || character === "\`" ) { quote = character; continue; } if (character === opening) { depth++; continue; } if (character === closing) { depth--; if (depth === 0) { return index; } } } return -1; } function parseIfStatement(source) { source = String(source || "").trim(); if ( source.slice(0, 2) !== "if" || /[A-Za-z0-9_$]/.test( source.charAt(2), ) ) { return null; } var branches = []; var index = 0; var firstBranch = true; while (index < source.length) { index = skipStatementWhitespace( source, index, ); var branchType; if ( firstBranch && source.slice(index, index + 2) === "if" && !/[A-Za-z0-9_$]/.test( source.charAt(index + 2), ) ) { branchType = "if"; index += 2; } else if ( source.slice(index, index + 7) === "else if" && !/[A-Za-z0-9_$]/.test( source.charAt(index + 7), ) ) { branchType = "else-if"; index += 7; } else if ( source.slice(index, index + 4) === "else" && !/[A-Za-z0-9_$]/.test( source.charAt(index + 4), ) ) { branchType = "else"; index += 4; } else { break; } firstBranch = false; index = skipStatementWhitespace( source, index, ); var condition = null; if (branchType !== "else") { if (source.charAt(index) !== "(") { throw new Error( "Expected '(' after " + ( branchType === "if" ? "if" : "else if" ), ); } var conditionEnd = findClosingDelimiter( source, index, "(", ")", ); if (conditionEnd < 0) { throw new Error( "Unclosed " + ( branchType === "if" ? "if" : "else if" ) + " condition", ); } condition = source .slice( index + 1, conditionEnd, ) .trim(); index = skipStatementWhitespace( source, conditionEnd + 1, ); } if (source.charAt(index) !== "{") { throw new Error( "Expected '{' after " + branchType, ); } var bodyEnd = findClosingDelimiter( source, index, "{", "}", ); if (bodyEnd < 0) { throw new Error( "Unclosed " + branchType + " body", ); } branches.push({ condition: condition, body: source.slice( index + 1, bodyEnd, ), }); index = skipStatementWhitespace( source, bodyEnd + 1, ); if ( source.slice(index, index + 7) === "else if" || source.slice(index, index + 4) === "else" ) { continue; } break; } return branches.length > 0 ? branches : null; } function runStatement( stmt, evalExpr, read, write, runBlock, ) { stmt = String(stmt || "").trim(); if (!stmt) { return { returned: false, value: undefined, }; } var returnMatch = /^return(?:\s+([\s\S]+))?$/.exec( stmt, ); if (returnMatch) { return { returned: true, value: returnMatch[1] ? evalExpr(returnMatch[1]) : undefined, }; } var ifBranches = parseIfStatement(stmt); if (ifBranches) { for ( var branchIndex = 0; branchIndex < ifBranches.length; branchIndex++ ) { var branch = ifBranches[branchIndex]; if ( branch.condition === null || !!evalExpr(branch.condition) ) { return runBlock(branch.body); } } return { returned: false, value: undefined, }; } var increment = stmt.match( /^([A-Za-z_$][A-Za-z0-9_$]*)\s*(\+\+|--)$/, ); if (increment) { write( increment[1], Number(read(increment[1]) || 0) + (increment[2] === "++" ? 1 : -1), ); return { returned: false, value: undefined, }; } var assignment = stmt.match( /^([A-Za-z_$][A-Za-z0-9_$]*)\s*(\+=|-=|\*=|\/=|%=|=)\s*([\s\S]+)$/, ); if (!assignment) { evalExpr(stmt); return { returned: false, value: undefined, }; } var name = assignment[1]; var operator = assignment[2]; var nextValue = evalExpr(assignment[3]); var currentValue = read(name); if (operator === "+=") { nextValue = currentValue + nextValue; } else if (operator === "-=") { nextValue = Number(currentValue || 0) - Number(nextValue || 0); } else if (operator === "*=") { nextValue = Number(currentValue || 0) * Number(nextValue || 0); } else if (operator === "/=") { nextValue = Number(currentValue || 0) / Number(nextValue || 0); } else if (operator === "%=") { nextValue = Number(currentValue || 0) % Number(nextValue || 0); } write(name, nextValue); return { returned: false, value: nextValue, }; } function evaluateSpecialExpression( expression, read, ) { var source = String( expression || "", ).trim(); // Remove an optional trailing semicolon. if ( source.charAt( source.length - 1, ) === ";" ) { source = source .slice(0, -1) .trim(); } /* * Array literals containing spreads: * * [...openIndexes, index] * [first, ...otherItems] */ if ( source.charAt(0) === "[" && source.charAt( source.length - 1, ) === "]" && source.indexOf("...") !== -1 ) { var arraySource = source.slice( 1, -1, ); var arrayParts = splitTopLevel( arraySource, ",", ); var arrayResult = []; arrayParts.forEach(function ( part, ) { var partSource = part.trim(); if (!partSource) { return; } if ( partSource.slice(0, 3) === "..." ) { var spreadResult = evaluateExpression( partSource .slice(3) .trim(), read, ); if ( Array.isArray( spreadResult, ) ) { Array.prototype.push.apply( arrayResult, spreadResult, ); } return; } arrayResult.push( evaluateExpression( partSource, read, ), ); }); return { matched: true, value: arrayResult, }; } /* * First recognize the array method call without * trying to parse the callback in the same regex. */ var methodMatch = /^([\s\S]+)\.(filter|map|flatMap|some|every|find|findIndex)\(\s*([\s\S]*)\s*\)$/.exec( source, ); if (!methodMatch) { return { matched: false, value: undefined, }; } var collectionSource = methodMatch[1].trim(); var method = methodMatch[2]; var callbackSource = methodMatch[3].trim(); /* * Supported callbacks: * * item => item.active * (item) => item.active * (item, index) => index > 0 */ var callbackMatch = /^(?:\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*)?\)|([A-Za-z_$][A-Za-z0-9_$]*))\s*=>\s*([\s\S]+)$/.exec( callbackSource, ); if (!callbackMatch) { return { matched: false, value: undefined, }; } var itemName = callbackMatch[1] || callbackMatch[3]; var indexName = callbackMatch[2]; var callbackBody = callbackMatch[4].trim(); /* * Support expression bodies wrapped in parentheses: * * item => ( * item.active * ) */ if ( callbackBody.charAt(0) === "(" && callbackBody.charAt( callbackBody.length - 1, ) === ")" ) { callbackBody = callbackBody .slice(1, -1) .trim(); } var collection = evaluateExpression( collectionSource, read, ); if (!Array.isArray(collection)) { collection = []; } var callback = function ( itemValue, itemIndex, ) { return evaluateExpression( callbackBody, function (name) { if (name === itemName) { return itemValue; } if ( indexName && name === indexName ) { return itemIndex; } return read(name); }, ); }; var result; if (method === "filter") { result = collection.filter(callback); } else if ( method === "map" ) { result = collection.map(callback); } else if ( method === "flatMap" ) { result = collection.flatMap(callback); } else if ( method === "some" ) { result = collection.some(callback); } else if ( method === "every" ) { result = collection.every(callback); } else if ( method === "find" ) { result = collection.find(callback); } else { result = collection.findIndex( callback, ); } return { matched: true, value: result, }; } // A small, eval-free expression evaluator (so a strict CSP needs no // 'unsafe-eval'). Supports: literals, identifiers, member access (a.b, a[b]), // function/method calls, arrays, objects, arithmetic, comparison, equality, // logical (&& ||), unary (! - +), and the ternary operator. function evaluateExpression( expr, read, ) { expr = String( expr || "", ).trim(); var special = evaluateSpecialExpression( expr, read, ); if (special.matched) { return special.value; } function evaluateTemplateLiteral(source) { var template = source.slice(1, -1); var rendered = ""; var cursor = 0; while (cursor < template.length) { if (template[cursor] === "\\") { cursor++; var escaped = template[cursor++]; rendered += escaped === "n" ? "\n" : escaped === "t" ? "\t" : escaped || ""; continue; } if (template[cursor] === "$" && template[cursor + 1] === "{") { var expressionStart = cursor + 2; var expressionEnd = expressionStart; var depth = 1; var quote = ""; for (; expressionEnd < template.length; expressionEnd++) { var character = template[expressionEnd]; if (quote) { if (character === "\\") expressionEnd++; else if (character === quote) quote = ""; continue; } if (character === '"' || character === "'") quote = character; else if (character === "{") depth++; else if (character === "}" && --depth === 0) break; } if (depth !== 0) throw new Error("Unclosed template interpolation in '" + source + "'"); rendered += String(evaluateExpression(template.slice(expressionStart, expressionEnd), read)); cursor = expressionEnd + 1; continue; } rendered += template[cursor++]; } return rendered; } // Template literals are evaluated without eval so they remain compatible // with a strict CSP. Each interpolation uses the same bounded expression // evaluator as every other reactive binding. They are accepted both as a // complete expression and as a primary inside a larger expression. if ( expr.length >= 2 && expr[0] === "\`" && expr[expr.length - 1] === "\`" ) { return evaluateTemplateLiteral(expr); } var tokens = tokenizeExpression(expr); var index = 0; function peek() { return tokens[index]; } function next() { return tokens[index++]; } /* * Punctuation only. is/match are asked exclusively about operators and * brackets, but they used to compare the token *value* alone -- so a * string literal that happens to spell an operator was mistaken for one. * That silently broke the most ordinary calls in the language: * join('-'), split(','), replace('.', '') all died on "Unexpected token". * Requiring an operator token keeps a quoted '-' a value. */ function is(v) { var t = peek(); return !!t && (t.type === "op" || t.type === "operator") && t.value === v; } function match(v) { if (is(v)) { index++; return true; } return false; } function expect(v) { if (!match(v)) throw new Error("Expected '" + v + "' in '" + expr + "'"); } function parsePrimary() { var t = next(); if (!t) throw new Error("Unexpected end of expression in '" + expr + "'"); if (t.type === "number" || t.type === "string") return { value: t.value }; // A fresh RegExp per evaluation: a shared instance carries lastIndex // between calls, so a /g pattern would return alternating results for // the same input. if (t.type === "regex") return { value: new RegExp(t.value.source, t.value.flags) }; if (t.type === "template") return { value: evaluateTemplateLiteral(t.value) }; if (t.type === "ident") { if (t.value === "true") return { value: true }; if (t.value === "false") return { value: false }; if (t.value === "null") return { value: null }; if (t.value === "undefined") return { value: undefined }; return { value: read(t.value) }; } if (t.value === "(") { var v = parseTernary(); expect(")"); return { value: v }; } if (t.value === "[") { var arr = []; if (!is("]")) { arr.push(parseTernary()); while (match(",")) arr.push(parseTernary()); } expect("]"); return { value: arr }; } if (t.value === "{") { var obj = {}; if (!is("}")) { do { var kt = next(); var key = kt.value; expect(":"); obj[key] = parseTernary(); } while (match(",")); } expect("}"); return { value: obj }; } throw new Error("Unexpected token '" + t.value + "'"); } function parsePostfix() { var node = parsePrimary(); for (;;) { if (match(".")) { var prop = next().value; node = { value: node.value == null ? undefined : node.value[prop], obj: node.value }; } else if (match("[")) { var key = parseTernary(); expect("]"); node = { value: node.value == null ? undefined : node.value[key], obj: node.value }; } else if (is("(")) { next(); var args = []; if (!is(")")) { args.push(parseTernary()); while (match(",")) args.push(parseTernary()); } expect(")"); var fn = node.value; node = { value: typeof fn === "function" ? fn.apply(node.obj, args) : undefined }; } else break; } return node; } function parseUnary() { if (match("!")) return !parseUnary(); if (match("-")) return -parseUnary(); if (match("+")) return +parseUnary(); return parsePostfix().value; } function parseMul() { var l = parseUnary(); while (peek() && (is("*") || is("/") || is("%"))) { var op = next().value, r = parseUnary(); l = op === "*" ? l * r : op === "/" ? l / r : l % r; } return l; } function parseAdd() { var l = parseMul(); while (peek() && (is("+") || is("-"))) { var op = next().value, r = parseMul(); l = op === "+" ? l + r : l - r; } return l; } function parseCmp() { var l = parseAdd(); while (peek() && (is("<") || is(">") || is("<=") || is(">="))) { var op = next().value, r = parseAdd(); l = op === "<" ? l < r : op === ">" ? l > r : op === "<=" ? l <= r : l >= r; } return l; } function parseEq() { var l = parseCmp(); while (peek() && (is("==") || is("!=") || is("===") || is("!=="))) { var op = next().value, r = parseCmp(); l = op === "==" ? l == r : op === "!=" ? l != r : op === "===" ? l === r : l !== r; } return l; } function parseAnd() { var l = parseEq(); while (match("&&")) { var r = parseEq(); l = l && r; } return l; } function parseOr() { var l = parseAnd(); while (match("||")) { var r = parseAnd(); l = l || r; } return l; } function parseTernary() { var c = parseOr(); if (match("?")) { var a = parseTernary(); expect(":"); var b = parseTernary(); return c ? a : b; } return c; } var value = parseTernary(); if (index < tokens.length) throw new Error("Unexpected token '" + tokens[index].value + "'"); return value; } function tokenizeExpression(input) { var tokens = []; var i = 0; while (i < input.length) { var ch = input[i]; if (/\s/.test(ch)) { i++; continue; } /* * Comments. Statement bodies handed to this engine -- a lifecycle mount * hook, an inline handler -- are ordinary authored code and routinely * contain them. Without this the leading slash fell through to the * regex-literal branch and the whole body died with "Unclosed regular * expression", naming nothing useful. */ if (ch === "/" && input[i + 1] === "/") { var lineEnd = input.indexOf("\n", i + 2); i = lineEnd === -1 ? input.length : lineEnd + 1; continue; } if (ch === "/" && input[i + 1] === "*") { var blockEnd = input.indexOf("*/", i + 2); i = blockEnd === -1 ? input.length : blockEnd + 2; continue; } if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(input[i + 1]))) { var start = i++; while (i < input.length && /[0-9.]/.test(input[i])) i++; tokens.push({ type: "number", value: Number(input.slice(start, i)) }); continue; } if (ch === '"' || ch === "'") { var quote = ch, value = ""; i++; while (i < input.length) { ch = input[i++]; if (ch === quote) break; if (ch === "\\") { var esc = input[i++]; value += esc === "n" ? "\n" : esc === "t" ? "\t" : esc || ""; } else value += ch; } tokens.push({ type: "string", value: value }); continue; } if (ch === "\`") { var templateStart = i++; var templateDepth = 0; var templateQuote = ""; while (i < input.length) { ch = input[i++]; if (templateQuote) { if (ch === "\\") i++; else if (ch === templateQuote) templateQuote = ""; continue; } if (ch === "\\") { i++; continue; } if (ch === "\`" && templateDepth === 0) break; if (ch === "$" && input[i] === "{") { templateDepth++; i++; continue; } if (templateDepth > 0) { if (ch === '"' || ch === "'") templateQuote = ch; else if (ch === "{") templateDepth++; else if (ch === "}") templateDepth--; } } if (input[i - 1] !== "\`") throw new Error("Unclosed template literal"); tokens.push({ type: "template", value: input.slice(templateStart, i) }); continue; } if (/[A-Za-z_$]/.test(ch)) { var s = i++; while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++; tokens.push({ type: "ident", value: input.slice(s, i) }); continue; } /* * Regex literal. Validating input is the single most common reason a * client function reaches for one, and without this the leading '/' fell * through to the division operator and the pattern body then died on * "Unexpected character '^'" -- taking the whole handler with it. * * '/' is ambiguous: it starts a regex only where a *value* may begin, * and is division after one. The preceding token decides -- after a * value-producing token (number, string, identifier, or a closing * bracket) it is division; anywhere else it opens a literal. This is * the standard lexer disambiguation and is why plain division keeps working. */ if (ch === "/") { var previous = tokens[tokens.length - 1]; var afterValue = previous && (previous.type === "number" || previous.type === "string" || previous.type === "template" || (previous.type === "ident" && previous.value !== "return" && previous.value !== "typeof" && previous.value !== "in" && previous.value !== "of") || ((previous.type === "op" || previous.type === "operator") && (previous.value === ")" || previous.value === "]" || previous.value === "}"))); if (!afterValue) { var regexStart = i++; var inClass = false; var closed = false; while (i < input.length) { var rc = input[i]; if (rc === "\\") { i += 2; continue; } if (rc === "[") inClass = true; else if (rc === "]") inClass = false; else if (rc === "/" && !inClass) { closed = true; i++; break; } else if (rc === "\n") break; i++; } if (!closed) throw new Error("Unclosed regular expression"); var body = input.slice(regexStart + 1, i - 1); var flagStart = i; while (i < input.length && /[a-z]/.test(input[i])) i++; tokens.push({ type: "regex", value: new RegExp(body, input.slice(flagStart, i)), }); continue; } } var three = input.slice(i, i + 3); if ( three === "===" || three === "!==" ) { tokens.push({ type: "operator", value: three, }); i += 3; continue; } var two = input.slice(i, i + 2); if ( two === "==" || two === "!=" || two === ">=" || two === "<=" || two === "&&" || two === "||" ) { tokens.push({ type: "operator", value: two, }); i += 2; continue; } if ("()+-*/%!<>.,?:[]{}".indexOf(ch) !== -1) { tokens.push({ type: "op", value: ch }); i++; continue; } throw new Error("Unexpected character '" + ch + "'"); } return tokens; } function splitTopLevel(input, separator) { var parts = []; var start = 0; var depth = 0; var quote = ""; for (var i = 0; i < input.length; i++) { var ch = input[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'") { quote = ch; continue; } if (ch === "(" || ch === "[" || ch === "{") depth++; else if (ch === ")" || ch === "]" || ch === "}") depth--; else if (ch === separator && depth === 0) { parts.push(input.slice(start, i).trim()); start = i + 1; } } parts.push(input.slice(start).trim()); return parts.filter(Boolean); } function findTopLevel(input, needle) { var depth = 0; var quote = ""; for (var i = 0; i < input.length; i++) { var ch = input[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'") quote = ch; else if (ch === "(" || ch === "[" || ch === "{") depth++; else if (ch === ")" || ch === "]" || ch === "}") depth--; else if (ch === needle && depth === 0) return i; } return -1; } function safeCsrUrl(id) { try { if (!/^[A-Za-z0-9_-]+$/.test(id)) return null; return "/__wrnexus/csr?route=" + encodeURIComponent(location.pathname) + "&id=" + encodeURIComponent(id); } catch (_) { return null; } } function setupCsrFetch(el) { if (el.__wrnexusCsrFetch) return; el.__wrnexusCsrFetch = true; var id = el.getAttribute("data-wrnexus-csr") || ""; var url = safeCsrUrl(id); if (!url) { console.error("[wrnexus] blocked unsafe CSR binding '" + id + "'"); return; } var headers = { "accept": "text/plain" }; var storage = localStorageSnapshotHeader(); if (storage) headers["x-wrnexus-local-storage"] = storage; fetch(url, { headers: headers }) .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.text(); }) .then(function (text) { el.textContent = text; }) .catch(function (err) { console.error("[wrnexus] CSR fetch failed for binding '" + id + "'", err); el.textContent = "Failed to load"; }); } function hydrateCsrFetches(root) { (root || document).querySelectorAll("[data-wrnexus-csr]").forEach(setupCsrFetch); } function localStorageSnapshotHeader() { try { if (!("localStorage" in window)) return ""; var values = {}; for (var i = 0; i < window.localStorage.length; i++) { var key = window.localStorage.key(i); if (!key) continue; var value = window.localStorage.getItem(key); if (typeof value === "string") values[key] = value; } var encoded = encodeURIComponent(JSON.stringify(values)); return encoded.length <= 12000 ? encoded : ""; } catch (_) { return ""; } } var asyncLoads = new Map(); function asyncValueAt(value, path) { var current = value; var parts = String(path || "").split(".").filter(Boolean); for (var i = 0; i < parts.length; i++) { if (current == null || typeof current !== "object") return ""; current = current[parts[i]]; } return current == null ? "" : current; } function interpolateAsync(root, model, prefix) { function visit(node) { if (node.nodeType === 3) { node.textContent = String(node.textContent || "").replace(/\{\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}/g, function (whole, path) { var parts = path.split("."); if (parts[0] === prefix) parts.shift(); if (prefix === "error" && parts[0] !== "message") return whole; var value = asyncValueAt(model, parts.join(".")); return typeof value === "object" ? JSON.stringify(value) : String(value); }); } Array.from(node.childNodes || []).forEach(visit); } visit(root); root.querySelectorAll("*").forEach(function (node) { Array.from(node.attributes || []).forEach(function (attribute) { if (attribute.name.indexOf("data-wrn-async-") === 0) return; if (attribute.value.indexOf("{") === -1) return; var nextValue = String(attribute.value).replace(/\{\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}/g, function (whole, path) { var parts = path.split("."); if (parts[0] === prefix) parts.shift(); if (prefix === "error" && parts[0] !== "message") return whole; var value = asyncValueAt(model, parts.join(".")); return typeof value === "object" ? JSON.stringify(value) : String(value); }); if (/^(?:href|src|action|formaction)$/i.test(attribute.name)) { nextValue = sanitizeReactiveUrl(nextValue); } node.setAttribute(attribute.name, nextValue); }); }); root.querySelectorAll("[data-wrn-async-value]").forEach(function (node) { var path = node.getAttribute("data-wrn-async-value") || ""; var value = asyncValueAt(model, path); node.textContent = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value); }); } function showAsync(boundary, state, model) { var template = boundary.querySelector("template[data-wrn-async-" + state + "]"); var content = boundary.querySelector("[data-wrn-async-content]"); if (!template || !content) return; var fragment = template.content.cloneNode(true); var alias = template.getAttribute("data-wrn-async-alias") || (state === "error" ? "error" : boundary.getAttribute("data-wrn-async")); interpolateAsync(fragment, model, alias); content.replaceChildren(fragment); boundary.setAttribute("aria-busy", state === "loading" ? "true" : "false"); boundary.setAttribute("data-wrn-async-state", state); } function setupAsyncBoundary(boundary) { if (boundary.__wrnexusAsync) return; boundary.__wrnexusAsync = true; if (boundary.getAttribute("data-wrn-async-resolved") === "true") { boundary.setAttribute("data-wrn-async-state", "success"); boundary.setAttribute("aria-busy", "false"); return; } var name = boundary.getAttribute("data-wrn-async") || ""; if (!/^[A-Za-z_$][\w$]{0,63}$/.test(name)) return showAsync(boundary, "error", { message: "Invalid data source" }); var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&search=" + encodeURIComponent(location.search) + "&name=" + encodeURIComponent(name); var attempts = Math.min(5, Math.max(0, Number(boundary.getAttribute("data-wrn-async-retries")) || 0)); var controller = new AbortController(); boundary.__wrnexusAsyncAbort = controller; window.addEventListener("wrnexus:navigation-start", function () { controller.abort(); }, { once: true }); function request(remaining) { var pending = asyncLoads.get(url); if (!pending) { pending = fetch(url, { headers: { accept: "application/json" }, signal: controller.signal }).then(function (response) { if (!response.ok) throw new Error("Client load returned " + response.status); return response.json(); }).finally(function () { asyncLoads.delete(url); }); asyncLoads.set(url, pending); } pending.then(function (result) { showAsync(boundary, "success", result.data); }).catch(function (error) { if (controller.signal.aborted) return; if (remaining > 0) return setTimeout(function () { request(remaining - 1); }, 100 * (attempts - remaining + 1)); showAsync(boundary, "error", { message: error && error.message ? error.message : "Loading failed" }); }); } showAsync(boundary, "loading", {}); request(attempts); } function hydrateAsyncBoundaries(root) { (root || document).querySelectorAll("[data-wrn-async]").forEach(setupAsyncBoundary); } var invalidationRefreshPending = false; function asyncBoundaryTags(boundary) { var raw = boundary.getAttribute("data-wrn-async-tags") || boundary.getAttribute("data-wrn-async") || ""; return raw.split(",").map(function (tag) { return tag.trim(); }).filter(Boolean); } function refreshAsyncBoundary(boundary) { if (boundary.__wrnexusAsyncAbort) { try { boundary.__wrnexusAsyncAbort.abort(); } catch (_) {} } boundary.__wrnexusAsync = false; boundary.removeAttribute("data-wrn-async-resolved"); setupAsyncBoundary(boundary); } function handleCacheInvalidation(event) { var detail = event && event.detail && typeof event.detail === "object" ? event.detail : {}; var tags = Array.isArray(detail.tags) ? detail.tags.map(String) : []; if (!tags.length) return; var matchedServerBoundary = false; document.querySelectorAll("[data-wrn-async]").forEach(function (boundary) { var matches = asyncBoundaryTags(boundary).some(function (tag) { return tags.indexOf(tag) !== -1; }); if (!matches) return; if (boundary.getAttribute("data-wrn-async-resolved") === "true") { matchedServerBoundary = true; return; } refreshAsyncBoundary(boundary); }); if (matchedServerBoundary && !invalidationRefreshPending) { invalidationRefreshPending = true; Promise.resolve().then(function () { invalidationRefreshPending = false; if (typeof window.__wrnexusRefresh === "function") window.__wrnexusRefresh(); else window.location.reload(); }); } } window.addEventListener("wrnexus:cache:invalidate", handleCacheInvalidation); function mountClientRoots(root) { (root || document).querySelectorAll("template[data-wrn-client-template]").forEach(function (template) { var id = template.getAttribute("data-wrn-client-template"); var mount = null; (root || document).querySelectorAll("[data-wrn-client-root]").forEach(function (candidate) { if (!mount && candidate.getAttribute("data-wrn-client-root") === id) mount = candidate; }); if (!mount || !template.content) return; var fragment = template.content.cloneNode(true); mount.replaceWith(fragment); template.remove(); window.dispatchEvent(new window.CustomEvent("wrnexus:client-mounted", { detail: { id: id } })); }); } function hydrateDeclarativeUi(root) { var host = root || document; host.querySelectorAll("[data-wrn-portal]").forEach(function (portal) { if (portal.__wrnPortalMounted) return; var selector = portal.getAttribute("data-wrn-portal") || "body"; var target = null; try { target = document.querySelector(selector); } catch (_) {} if (!target || target === portal || portal.contains(target)) return; portal.__wrnPortalMounted = true; var marker = document.createComment("wrnexus-portal"); portal.parentNode && portal.parentNode.insertBefore(marker, portal); target.appendChild(portal); }); host.querySelectorAll("[data-wrn-transition]").forEach(function (element) { if (element.__wrnTransitionMounted) return; element.__wrnTransitionMounted = true; var name = element.getAttribute("data-wrn-transition") || "wrn-transition"; element.classList.add(name + "-enter", name + "-enter-active"); requestAnimationFrame(function () { element.classList.remove(name + "-enter"); element.classList.add(name + "-enter-to"); }); element.addEventListener("transitionend", function () { element.classList.remove(name + "-enter-active", name + "-enter-to"); }, { once: true }); }); host.querySelectorAll("[data-wrn-dynamic-component]").forEach(function (element) { if (element.__wrnDynamicMounted) return; element.__wrnDynamicMounted = true; var cases = Array.prototype.slice.call(element.children).filter(function (candidate) { return candidate.hasAttribute("data-component-case"); }).map(function (candidate) { var marker = document.createComment("wrnexus-component-case:" + (candidate.getAttribute("data-component-case") || "")); element.insertBefore(marker, candidate); return { candidate: candidate, marker: marker }; }); var update = function () { var selected = element.getAttribute("data-wrn-dynamic-component") || ""; cases.forEach(function (record) { var candidate = record.candidate; var active = candidate.getAttribute("data-component-case") === selected; candidate.hidden = false; if (active && !candidate.isConnected && record.marker.parentNode) { record.marker.parentNode.insertBefore(candidate, record.marker.nextSibling); } else if (!active && candidate.parentNode) { candidate.parentNode.removeChild(candidate); } }); window.dispatchEvent(new window.CustomEvent("wrnexus:dynamic-component", { detail: { component: selected } })); }; update(); new MutationObserver(update).observe(element, { attributes: true, attributeFilter: ["data-wrn-dynamic-component"] }); }); } window.__wrnexusMountClientRoots = mountClientRoots; window.__wrnexusHydrateAsyncBoundaries = hydrateAsyncBoundaries; setupAnchoredOverlays(); setupModalDialogs(); setupRovingFocus(); window.__wrnexusRepositionAnchored = repositionAnchored; window.__wrnexusHydrateScopes = hydrateScopes; window.__wrnexusInvalidateClientModule = function (url) { clientModuleCache.delete(url); }; window.__wrnexusHydrateCsrFetches = hydrateCsrFetches; window.__wrnexusDisposeBehaviors = disposeBehaviors; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { mountClientRoots(document); hydrateScopes(document); hydrateCsrFetches(document); hydrateAsyncBoundaries(document); hydrateDeclarativeUi(document); }); } else { mountClientRoots(document); hydrateScopes(document); hydrateCsrFetches(document); hydrateAsyncBoundaries(document); hydrateDeclarativeUi(document); } })(); `.trim();