Files
WRNexusJS/packages/csr/src/reactive-runtime.ts
T
ClintchizandClaude Opus 5 b56cb8cba5
Quality / quality (ubuntu-latest) (push) Failing after 6m8s
Quality / quality (windows-latest) (push) Canceled after 0s
feat(ui): build LayoutSplitter and CustomScrollbar for real
Both advertised behaviour they did not have. LayoutSplitter declared
resizeStart, resize and resizeEnd with no pointer handling whatsoever, so a
caller wired up @resize and received nothing, for ever, with no error, and its
props were columns, gap and maxWidth copied from a grid scaffold.
CustomScrollbar was the same shape with a scroll output.

The splitter now resizes. Dragging lives in the reactive runtime behind
data-wrn-splitter, because a pointermove fires far too often to route through
a client function and a state write made in that callback is dropped; the
resolved size is held on the container as a --wrn-split custom property and
the component grids from it. The handle is a real separator: arrow keys step
it, Home and End go to the bounds rather than to nothing, and it carries
aria-valuenow, aria-valuemin and aria-valuemax. minSize fixes both bounds so
neither pane can be dragged away and left unrecoverable.

CustomScrollbar is CSS rather than script -- scrollbar-width and
scrollbar-color with webkit rules for the engines that still need them -- and
its fake scroll output is removed rather than left unimplemented, since a
caller can listen for a plain scroll event.

The test harness needed a fix too: mount did not bind the window CustomEvent,
so the runtime built events from the host global and happy-dom listeners never
matched them, which made anything dispatched look silently lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:22:45 +05:30

5537 lines
172 KiB
TypeScript

/**
* 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-<event>="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 <Toaster /> is mounted on this page; " +
"falling back to the console. Add <Toaster /> 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:
*
* <span>{item.title}</span>
*/
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;
// Text in these is data, not a template: a JSON sample sitting in a
// textarea would otherwise be read as mustaches and eaten.
var owner = textNode.parentNode;
var ownerName = owner ? owner.nodeName : "";
if (ownerName === "TEXTAREA" || ownerName === "SCRIPT" || ownerName === "STYLE") 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);
});
}
/*
* One document observer, several subscribers. The filter stays explicit:
* observing every attribute would see the tabindex the roving code writes
* and loop on its own output.
*/
var documentWatchers = [];
function watchDocument(handler) {
documentWatchers.push(handler);
}
function startDocumentWatch() {
if (window.__wrnexusDocWatchBound || typeof MutationObserver !== "function") return;
window.__wrnexusDocWatchBound = true;
new MutationObserver(function () {
for (var index = 0; index < documentWatchers.length; index += 1) documentWatchers[index]();
}).observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
// prettier-ignore
attributeFilter: ["data-open","data-show","class","style","hidden",
"data-placement","aria-selected","aria-current","disabled","aria-disabled"],
});
}
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.
watchDocument(scheduleAnchoredReposition);
window.addEventListener("resize", scheduleAnchoredReposition);
window.addEventListener("scroll", scheduleAnchoredReposition, true);
}
/*
* Modal dialog focus, focus restore, Tab trap and scroll lock. Shared by
* Modal and Drawer, and here rather than in them because a client function
* cannot hold the previously focused element across a close.
*
* Fixing focus also repairs Escape: both bind @keydown on their own root,
* so until focus moved inside, closeOnEscape did nothing.
*/
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;
// data-show, not measured size: the test DOM reports everything as
// zero-sized, which is what left the trap uncovered.
function isDialogVisible(dialog) {
if (!dialog || !dialog.isConnected) return false;
if (dialog.hasAttribute("hidden")) return false;
if (dialog.closest('[data-show="false"]')) return false;
/*
* data-open is the signal Modal and Drawer actually publish, and it is the
* only one Drawer can publish: its panel animates open, so it cannot be
* toggled with data-show, which sets display:none. Treating every dialog
* without a data-show="false" ancestor as open meant a closed Drawer held
* the body scroll lock forever and the page could not be scrolled.
*/
var owner = dialog.closest("[data-open]");
if (owner) return owner.getAttribute("data-open") === "true";
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];
// 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; the panel carries tabindex="-1" as a fallback.
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; keep focus on the panel rather than the page behind.
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;
// Deferred: the mutation that opens a dialog is the one that makes it
// visible, so it is not laid out yet when the record arrives.
watchDocument(function () {
window.setTimeout(syncDialogs, 0);
});
document.addEventListener("keydown", trapDialogTab, true);
syncDialogs();
}
/*
* Roving arrow-key focus. A container marked data-wrn-roving owns its
* [data-wrn-roving-item] descendants: one carries tabindex="0" so Tab
* reaches the group once, and the arrows move within it. Here rather than
* in five components because focus bookkeeping cannot live in component
* state.
*/
var ROVING_SELECTOR = "[data-wrn-roving]";
var ROVING_ITEM_SELECTOR = "[data-wrn-roving-item]";
// Templates stringify these: "" and "false" mean opted out, and a bare
// [attr] selector matches either, so the value must be checked.
// Bare means yes; only an explicit "false" opts out.
function rovingItemOff(value) {
return value === null || value === "false";
}
// The container must name an axis; empty is what {cond ? "x" : ""} emits.
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 (see isDialogVisible).
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);
watchDocument(function () {
window.setTimeout(syncRovingGroups, 0);
});
syncRovingGroups();
}
// Scrollspy. The marker goes onto the links, not into state: an observer
// callback fires after the client function returned, so that write is lost.
function applyScrollspyCurrent(nav, href) {
var links = nav.querySelectorAll('a[href^="#"]');
var changed = false;
var label = "";
for (var index = 0; index < links.length; index += 1) {
var link = links[index];
var current = link.getAttribute("href") === href;
if (current) label = (link.textContent || "").trim();
if ((link.getAttribute("data-active") === "true") !== current) changed = true;
link.setAttribute("data-active", current ? "true" : "false");
link.setAttribute("aria-current", current ? "location" : "false");
}
if (!changed) return;
// Named for the component output so a parent @change binding receives it.
nav.dispatchEvent(new CustomEvent("change", { detail: { href: href, label: label } }));
}
function wireScrollspy(nav) {
if (nav.__wrnScrollspyWired) return;
nav.__wrnScrollspyWired = true;
nav.addEventListener("click", function (event) {
var t = event.target;
var link = t && t.closest ? t.closest('a[href^="#"]') : null;
if (link && nav.contains(link)) applyScrollspyCurrent(nav, link.getAttribute("href"));
});
if (typeof IntersectionObserver === "undefined") return;
var links = nav.querySelectorAll('a[href^="#"]');
var targets = [];
for (var index = 0; index < links.length; index += 1) {
var href = links[index].getAttribute("href");
var section = document.getElementById(href.slice(1));
if (section) targets.push({ section: section, href: href });
}
if (!targets.length) return;
var visible = {};
var observer = new IntersectionObserver(
function (entries) {
for (var entryIndex = 0; entryIndex < entries.length; entryIndex += 1) {
visible[entries[entryIndex].target.id] = entries[entryIndex].isIntersecting;
}
// First visible section in document order wins, so up and down
// settle on the same link.
for (var pick = 0; pick < targets.length; pick += 1) {
if (visible[targets[pick].section.id]) {
applyScrollspyCurrent(nav, targets[pick].href);
return;
}
}
},
// Biased to the upper third: the current section is the one being read.
{ rootMargin: "-80px 0px -55% 0px" },
);
for (var watch = 0; watch < targets.length; watch += 1) observer.observe(targets[watch].section);
}
function setupScrollspy() {
if (window.__wrnexusScrollspyBound) return;
window.__wrnexusScrollspyBound = true;
var wireAll = function () {
var navs = document.querySelectorAll("[data-wrn-scrollspy]");
for (var index = 0; index < navs.length; index += 1) wireScrollspy(navs[index]);
};
wireAll();
watchDocument(function () {
window.setTimeout(wireAll, 0);
});
}
/*
* Resizable split panes.
*
* The resolved size is written onto the container as a --wrn-split custom
* property and the component styles from it. Pointer moves fire far too
* often to route through a client function, and a state write made inside a
* pointermove callback is dropped, so the DOM holds the answer.
*/
var SPLITTER_SELECTOR = "[data-wrn-splitter]";
function splitterNumber(element, name, fallback) {
var raw = Number(element.getAttribute(name));
return isFinite(raw) && raw !== 0 ? raw : fallback;
}
function splitterBounds(root) {
var min = splitterNumber(root, "data-wrn-splitter-min", 10);
return { min: min, max: 100 - min };
}
function applySplit(root, handle, size) {
var bounds = splitterBounds(root);
var next = Math.min(bounds.max, Math.max(bounds.min, size));
next = Math.round(next * 100) / 100;
root.style.setProperty("--wrn-split", next + "%");
if (handle) {
handle.setAttribute("aria-valuenow", String(next));
handle.setAttribute("aria-valuemin", String(bounds.min));
handle.setAttribute("aria-valuemax", String(bounds.max));
}
// Named for the component output so a parent @resize binding receives it.
root.dispatchEvent(new CustomEvent("resize", { detail: { size: next } }));
return next;
}
function currentSplit(root, handle) {
var fromHandle = Number(handle && handle.getAttribute("aria-valuenow"));
if (isFinite(fromHandle) && fromHandle) return fromHandle;
var raw = String(root.style.getPropertyValue("--wrn-split") || "").replace("%", "");
var parsed = Number(raw);
return isFinite(parsed) && parsed ? parsed : 50;
}
function handleSplitterKeydown(event) {
var target = event.target;
if (!target || !target.closest) return;
var handle = target.closest("[data-wrn-splitter-handle]");
if (!handle) return;
var root = handle.closest(SPLITTER_SELECTOR);
if (!root) return;
var vertical = root.getAttribute("data-wrn-splitter") === "vertical";
var step = splitterNumber(root, "data-wrn-splitter-step", 5);
var bounds = splitterBounds(root);
var size = currentSplit(root, handle);
var key = event.key;
var next = size;
if (key === (vertical ? "ArrowDown" : "ArrowRight")) next = size + step;
else if (key === (vertical ? "ArrowUp" : "ArrowLeft")) next = size - step;
else if (key === "Home") next = bounds.min;
else if (key === "End") next = bounds.max;
else return;
event.preventDefault();
applySplit(root, handle, next);
}
function setupSplitters() {
if (window.__wrnexusSplitterBound) return;
window.__wrnexusSplitterBound = true;
document.addEventListener("keydown", handleSplitterKeydown, true);
var dragging = null;
document.addEventListener("pointerdown", function (event) {
var target = event.target;
var handle = target && target.closest ? target.closest("[data-wrn-splitter-handle]") : null;
if (!handle) return;
var root = handle.closest(SPLITTER_SELECTOR);
if (!root) return;
dragging = { root: root, handle: handle };
root.setAttribute("data-wrn-splitter-dragging", "true");
if (handle.setPointerCapture && event.pointerId !== undefined) {
handle.setPointerCapture(event.pointerId);
}
event.preventDefault();
});
document.addEventListener("pointermove", function (event) {
if (!dragging) return;
var rect = dragging.root.getBoundingClientRect();
var vertical = dragging.root.getAttribute("data-wrn-splitter") === "vertical";
var span = vertical ? rect.height : rect.width;
if (!span) return;
var offset = vertical ? event.clientY - rect.top : event.clientX - rect.left;
applySplit(dragging.root, dragging.handle, (offset / span) * 100);
});
var endDrag = function () {
if (!dragging) return;
dragging.root.removeAttribute("data-wrn-splitter-dragging");
dragging = null;
};
document.addEventListener("pointerup", endDrag);
document.addEventListener("pointercancel", endDrag);
}
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);
hydrateSelectControllers(host);
hydratePinInputControllers(host);
}
var navbarOutsideClickBound = false;
var preferenceOutsideClickBound = false;
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();
setupScrollspy();
setupSplitters();
startDocumentWatch();
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();