Add engine orchestration to SX spec (fetch, triggers, swap, SSE, history, init)
29 orchestration functions written in SX + adapter style: request pipeline (execute-request, do-fetch, handle-fetch-success), trigger binding (poll, intersect, load, revealed, event), post-swap processing, OOB swaps, boost, SSE, inline handlers, preload, history/popstate, and engine-init. Platform JS implementations in bootstrap_js.py for all browser-specific operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1252,6 +1252,354 @@
|
||||
// parse-sse-swap
|
||||
var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); };
|
||||
|
||||
// _preload-cache
|
||||
var _preloadCache = {};
|
||||
|
||||
// _css-hash
|
||||
var _cssHash = "";
|
||||
|
||||
// dispatch-trigger-events
|
||||
var dispatchTriggerEvents = function(el, headerVal) { return (isSxTruthy(headerVal) ? (function() {
|
||||
var parsed = tryParseJson(headerVal);
|
||||
return (isSxTruthy((isSxTruthy(parsed) && isDict(parsed))) ? forEach(function(key) { return domDispatch(el, key, dictGet(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() {
|
||||
var n = trim(name);
|
||||
return (isSxTruthy(!(n == "")) ? domDispatch(el, n, {}) : NIL);
|
||||
})(); }, split(headerVal, ",")));
|
||||
})() : NIL); };
|
||||
|
||||
// init-css-tracking
|
||||
var initCssTracking = function() { return (function() {
|
||||
var meta = domQuery("meta[name=\"sx-css-classes\"]");
|
||||
return (isSxTruthy(meta) ? (function() {
|
||||
var content = domGetAttr(meta, "content");
|
||||
return (isSxTruthy(content) ? (_cssHash = content) : NIL);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// execute-request
|
||||
var executeRequest = function(el, verbInfo, extraParams) { return (function() {
|
||||
var currentVerb = getVerbInfo(el);
|
||||
var verb = (isSxTruthy(currentVerb) ? currentVerb : verbInfo);
|
||||
var method = get(verb, "method");
|
||||
var url = get(verb, "url");
|
||||
if (isSxTruthy(!domHasClass(el, "sx-error"))) {
|
||||
domRemoveAttr(el, "data-sx-retry-ms");
|
||||
}
|
||||
return (isSxTruthy((function() {
|
||||
var media = domGetAttr(el, "sx-media");
|
||||
return (isSxTruthy(media) && !browserMediaMatches(media));
|
||||
})()) ? promiseResolve(NIL) : (isSxTruthy((function() {
|
||||
var msg = domGetAttr(el, "sx-confirm");
|
||||
return (isSxTruthy(msg) && !browserConfirm(msg));
|
||||
})()) ? promiseResolve(NIL) : (function() {
|
||||
var promptMsg = domGetAttr(el, "sx-prompt");
|
||||
var params = extraParams;
|
||||
return (isSxTruthy(promptMsg) ? (function() {
|
||||
var promptVal = browserPrompt(promptMsg);
|
||||
return (isSxTruthy(isNil(promptVal)) ? promiseResolve(NIL) : ((params = sxOr(params, {})), dictSet(params, "promptValue", promptVal), doFetch(el, verb, method, url, params)));
|
||||
})() : doFetch(el, verb, method, url, params));
|
||||
})()));
|
||||
})(); };
|
||||
|
||||
// do-fetch
|
||||
var doFetch = function(el, verb, method, url, extraParams) { return (function() {
|
||||
var syncAttr = domGetAttr(el, "sx-sync");
|
||||
if (isSxTruthy((isSxTruthy(syncAttr) && contains(syncAttr, "replace")))) {
|
||||
abortPrevious(el);
|
||||
}
|
||||
return (function() {
|
||||
var ctrl = newAbortController();
|
||||
trackController(el, ctrl);
|
||||
return (function() {
|
||||
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
|
||||
if (isSxTruthy((isSxTruthy(extraParams) && dictHas(extraParams, "promptValue")))) {
|
||||
headers["SX-Prompt"] = get(extraParams, "promptValue");
|
||||
}
|
||||
if (isSxTruthy((isSxTruthy(!(method == "GET")) && browserSameOrigin(url)))) {
|
||||
(function() {
|
||||
var csrf = csrfToken();
|
||||
return (isSxTruthy(csrf) ? dictSet(headers, "X-CSRFToken", csrf) : NIL);
|
||||
})();
|
||||
}
|
||||
return (function() {
|
||||
var bodyInfo = buildRequestBody(el, method, url);
|
||||
return (function() {
|
||||
var body = get(bodyInfo, "body");
|
||||
var finalUrl = get(bodyInfo, "url");
|
||||
var ct = get(bodyInfo, "content-type");
|
||||
if (isSxTruthy(ct)) {
|
||||
headers["Content-Type"] = ct;
|
||||
}
|
||||
return (isSxTruthy(!domDispatch(el, "sx:beforeRequest", {["method"]: method, ["url"]: finalUrl})) ? promiseResolve(NIL) : (domAddClass(el, "sx-request"), domSetAttr(el, "aria-busy", "true"), (function() {
|
||||
var indicator = showIndicator(el);
|
||||
var disabledElts = disableElements(el);
|
||||
var preloaded = (isSxTruthy((method == "GET")) ? preloadCacheGet(_preloadCache, finalUrl) : NIL);
|
||||
return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["preloaded"]: preloaded, ["cross-origin"]: isCrossOrigin(finalUrl)}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status}), handleRetry(el, verb, extraParams)) : (domDispatch(el, "sx:afterRequest", {}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!isAbortError(err)) ? (domDispatch(el, "sx:sendError", {["error"]: err}), handleRetry(el, verb, extraParams)) : NIL)); });
|
||||
})()));
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-fetch-success
|
||||
var handleFetchSuccess = function(el, url, verb, extraParams, getHeader, text) { return (function() {
|
||||
var headers = processResponseHeaders(getHeader);
|
||||
return (isSxTruthy(get(headers, "redirect")) ? browserNavigate(get(headers, "redirect")) : (isSxTruthy((get(headers, "refresh") == "true")) ? browserReload() : (dispatchTriggerEvents(el, get(headers, "trigger")), (function() {
|
||||
var rawSwap = sxOr(domGetAttr(el, "sx-swap"), DEFAULT_SWAP);
|
||||
var target = resolveTarget(el);
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
if (isSxTruthy(get(headers, "retarget"))) {
|
||||
target = sxOr(domQuery(get(headers, "retarget")), target);
|
||||
}
|
||||
if (isSxTruthy(get(headers, "reswap"))) {
|
||||
rawSwap = get(headers, "reswap");
|
||||
}
|
||||
return (function() {
|
||||
var swap = parseSwapSpec(rawSwap, false);
|
||||
var ct = sxOr(get(headers, "content-type"), "");
|
||||
(isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, target, swap, selectSel, text) : handleHtmlResponse(el, target, swap, selectSel, text));
|
||||
if (isSxTruthy(get(headers, "location"))) {
|
||||
fetchLocation(get(headers, "location"));
|
||||
}
|
||||
handleHistory(el, url, headers);
|
||||
domDispatch(el, "sx:afterSwap", {["target"]: target});
|
||||
dispatchTriggerEvents(el, get(headers, "trigger-swap"));
|
||||
return requestAnimationFrame_(function() { return (domDispatch(el, "sx:afterSettle", {["target"]: target}), dispatchTriggerEvents(el, get(headers, "trigger-settle"))); });
|
||||
})();
|
||||
})())));
|
||||
})(); };
|
||||
|
||||
// handle-sx-response
|
||||
var handleSxResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var cleaned = stripComponentScripts(text);
|
||||
var cleaned2 = extractResponseCss(cleaned);
|
||||
return (function() {
|
||||
var source = trim(cleaned2);
|
||||
return (isSxTruthy((isSxTruthy(source) && !(source == ""))) ? (function() {
|
||||
var dom = sxRender(source);
|
||||
var container = domCreateElement("div", NIL);
|
||||
domAppend(container, dom);
|
||||
processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); });
|
||||
return (function() {
|
||||
var selected = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
|
||||
return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapDomNodes(target, selected, get(swap, "style")), hoistHeadElements(target)); }) : NIL);
|
||||
})();
|
||||
})() : NIL);
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-html-response
|
||||
var handleHtmlResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var doc = domParseHtmlDocument(text);
|
||||
sxProcessScripts(doc);
|
||||
processOobSwaps(doc, function(t, oob, s) { return swapHtmlString(t, domOuterHtml(oob), s); });
|
||||
return (function() {
|
||||
var content = (isSxTruthy(selectSel) ? selectHtmlFromDoc(doc, selectSel) : sxOr(domBodyInnerHtml(doc), text));
|
||||
return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapHtmlString(target, content, get(swap, "style")), hoistHeadElements(target)); }) : NIL);
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-retry
|
||||
var handleRetry = function(el, verbInfo, extraParams) { return (function() {
|
||||
var retryAttr = domGetAttr(el, "sx-retry");
|
||||
return (isSxTruthy(retryAttr) ? (function() {
|
||||
var spec = parseRetrySpec(retryAttr);
|
||||
var currentMs = sxOr(parseInt_(domGetAttr(el, "data-sx-retry-ms"), 0), get(spec, "start-ms"));
|
||||
domAddClass(el, "sx-error");
|
||||
domRemoveClass(el, "sx-loading");
|
||||
return setTimeout_(function() { return (domRemoveClass(el, "sx-error"), domAddClass(el, "sx-loading"), domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(currentMs, get(spec, "cap-ms"))))), executeRequest(el, verbInfo, extraParams)); }, currentMs);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// bind-triggers
|
||||
var bindTriggers = function(el, verbInfo) { return (function() {
|
||||
var triggerSpec = domGetAttr(el, "sx-trigger");
|
||||
var triggers = (isSxTruthy(triggerSpec) ? parseTriggerSpec(triggerSpec) : defaultTrigger(domTagName(el)));
|
||||
return forEach(function(trig) { return (function() {
|
||||
var kind = classifyTrigger(trig);
|
||||
return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, verbInfo, NIL); }, sxOr(get(get(trig, "modifiers"), "interval"), 1000)) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, get(get(trig, "modifiers"), "once"), get(get(trig, "modifiers"), "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, 0) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, true, NIL) : bindEvent(el, verbInfo, trig)))));
|
||||
})(); }, triggers);
|
||||
})(); };
|
||||
|
||||
// bind-event
|
||||
var bindEvent = function(el, verbInfo, trig) { return (function() {
|
||||
var eventName = get(trig, "event");
|
||||
var mods = get(trig, "modifiers");
|
||||
var listenTarget = (isSxTruthy(get(mods, "from")) ? sxOr(domQuery(get(mods, "from")), el) : el);
|
||||
var timer = NIL;
|
||||
var lastVal = NIL;
|
||||
return domAddListener(listenTarget, eventName, function(e) { return ((isSxTruthy((eventName == "submit")) ? preventDefault_(e) : NIL), (isSxTruthy((isSxTruthy((eventName == "click")) && (domTagName(el) == "A"))) ? preventDefault_(e) : NIL), (isSxTruthy(!validateForRequest(el)) ? domDispatch(el, "sx:validationFailed", {}) : (isSxTruthy((isSxTruthy(get(mods, "changed")) && isSxTruthy(!isNil(elementValue(el))) && (elementValue(el) == lastVal))) ? NIL : ((isSxTruthy(get(mods, "changed")) ? (lastVal = elementValue(el)) : NIL), (function() {
|
||||
var optState = applyOptimistic(el);
|
||||
var execFn = function() { return (function() {
|
||||
var p = executeRequest(el, verbInfo, NIL);
|
||||
return (isSxTruthy((isSxTruthy(optState) && p)) ? promiseCatch(p, function(_) { return revertOptimistic(optState); }) : NIL);
|
||||
})(); };
|
||||
return (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(execFn, get(mods, "delay")))) : execFn());
|
||||
})())))); }, {["once"]: get(mods, "once")});
|
||||
})(); };
|
||||
|
||||
// post-swap
|
||||
var postSwap = function(root) { return (activateScripts(root), sxProcessScripts(root), sxHydrate(root), processElements(root)); };
|
||||
|
||||
// activate-scripts
|
||||
var activateScripts = function(root) { return (function() {
|
||||
var dead = domQueryAll(root, "script:not([type]), script[type='text/javascript']");
|
||||
return forEach(function(d) { return (function() {
|
||||
var live = createScriptClone(d);
|
||||
return domReplaceChild(domParent(d), live, d);
|
||||
})(); }, dead);
|
||||
})(); };
|
||||
|
||||
// process-oob-swaps
|
||||
var processOobSwaps = function(container, swapFn) { return forEach(function(attr) { return (function() {
|
||||
var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]")));
|
||||
return forEach(function(oob) { return (function() {
|
||||
var swapType = sxOr(domGetAttr(oob, attr), "outerHTML");
|
||||
var targetId = domId(oob);
|
||||
domRemoveAttr(oob, attr);
|
||||
if (isSxTruthy(domParent(oob))) {
|
||||
domRemoveChild(domParent(oob), oob);
|
||||
}
|
||||
return (isSxTruthy(targetId) ? (function() {
|
||||
var target = domQueryById(targetId);
|
||||
return (isSxTruthy(target) ? swapFn(target, oob, swapType) : NIL);
|
||||
})() : NIL);
|
||||
})(); }, oobEls);
|
||||
})(); }, ["sx-swap-oob", "hx-swap-oob"]); };
|
||||
|
||||
// hoist-head-elements
|
||||
var hoistHeadElements = function(root) { return (function() {
|
||||
var styles = domQueryAll(root, "style[data-sx-css]");
|
||||
var links = domQueryAll(root, "link[rel='stylesheet']");
|
||||
{ var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var el = _c[_i]; if (isSxTruthy(domParent(el))) {
|
||||
domRemoveChild(domParent(el), el);
|
||||
} } }
|
||||
return forEach(function(el) { return (isSxTruthy(domParent(el)) ? domRemoveChild(domParent(el), el) : NIL); }, links);
|
||||
})(); };
|
||||
|
||||
// process-boosted
|
||||
var processBoosted = function(root) { return (function() {
|
||||
var containers = domQueryAll(root, "[sx-boost]");
|
||||
if (isSxTruthy(domMatches(root, "[sx-boost]"))) {
|
||||
boostDescendants(root);
|
||||
}
|
||||
return forEach(boostDescendants, containers);
|
||||
})(); };
|
||||
|
||||
// boost-descendants
|
||||
var boostDescendants = function(container) { return ((function() {
|
||||
var links = domQueryAll(container, "a[href]");
|
||||
return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), bindBoostLink(link, domGetAttr(link, "href")), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-select")) ? domSetAttr(link, "sx-select", "#main-panel") : NIL)) : NIL); }, links);
|
||||
})(), (function() {
|
||||
var forms = domQueryAll(container, "form");
|
||||
return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form, "boost")) && shouldBoostForm(form))) ? (markProcessed(form, "boost"), bindBoostForm(form, sxOr(upper(domGetAttr(form, "method")), "GET"), sxOr(domGetAttr(form, "action"), browserLocationHref())), (isSxTruthy(!domHasAttr(form, "sx-target")) ? domSetAttr(form, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(form, "sx-swap")) ? domSetAttr(form, "sx-swap", "innerHTML") : NIL)) : NIL); }, forms);
|
||||
})()); };
|
||||
|
||||
// process-sse
|
||||
var processSse = function(root) { return (function() {
|
||||
var sseEls = domQueryAll(root, "[sx-sse]");
|
||||
if (isSxTruthy(domMatches(root, "[sx-sse]"))) {
|
||||
bindSse(root);
|
||||
}
|
||||
return forEach(bindSse, sseEls);
|
||||
})(); };
|
||||
|
||||
// bind-sse
|
||||
var bindSse = function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), (function() {
|
||||
var url = domGetAttr(el, "sx-sse");
|
||||
return (isSxTruthy(url) ? (function() {
|
||||
var source = eventSourceConnect(url, el);
|
||||
return (function() {
|
||||
var swapEls = domQueryAll(el, "[sx-sse-swap]");
|
||||
if (isSxTruthy(domHasAttr(el, "sx-sse-swap"))) {
|
||||
bindSseSwap(el, source);
|
||||
}
|
||||
return forEach(function(child) { return bindSseSwap(child, source); }, swapEls);
|
||||
})();
|
||||
})() : NIL);
|
||||
})()) : NIL); };
|
||||
|
||||
// bind-sse-swap
|
||||
var bindSseSwap = function(el, source) { return (function() {
|
||||
var eventName = parseSseSwap(el);
|
||||
return eventSourceListen(source, eventName, function(data) { return (function() {
|
||||
var target = sxOr(resolveTarget(el), el);
|
||||
var swapStyle = sxOr(domGetAttr(el, "sx-swap"), "innerHTML");
|
||||
(isSxTruthy(startsWith(trim(data), "(")) ? (function() {
|
||||
var dom = sxRender(data);
|
||||
return swapDomNodes(target, dom, swapStyle);
|
||||
})() : swapHtmlString(target, data, swapStyle));
|
||||
postSwap(target);
|
||||
return domDispatch(el, "sx:sseMessage", {["data"]: data, ["event"]: eventName});
|
||||
})(); });
|
||||
})(); };
|
||||
|
||||
// bind-inline-handlers
|
||||
var bindInlineHandlers = function(el) { return (isSxTruthy(!isProcessed(el, "on")) ? (markProcessed(el, "on"), (function() {
|
||||
var attrs = domAttrList(el);
|
||||
return forEach(function(attr) { return (function() {
|
||||
var name = first(attr);
|
||||
var val = nth(attr, 1);
|
||||
return (isSxTruthy(startsWith(name, "sx-on:")) ? bindInlineHandler(el, slice(name, 6), val) : NIL);
|
||||
})(); }, attrs);
|
||||
})()) : NIL); };
|
||||
|
||||
// bind-preload-for
|
||||
var bindPreloadFor = function(el) { return (isSxTruthy(domHasAttr(el, "sx-preload")) ? (function() {
|
||||
var mode = sxOr(domGetAttr(el, "sx-preload"), "mousedown");
|
||||
var events = (isSxTruthy((mode == "mouseover")) ? ["mouseenter", "focusin"] : ["mousedown", "focusin"]);
|
||||
var debounceMs = (isSxTruthy((mode == "mouseover")) ? 100 : 0);
|
||||
return bindPreload(el, events, debounceMs, function() { return (function() {
|
||||
var verb = getVerbInfo(el);
|
||||
return (isSxTruthy(verb) ? (function() {
|
||||
var url = get(verb, "url");
|
||||
return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? doPreload(url) : NIL);
|
||||
})() : NIL);
|
||||
})(); });
|
||||
})() : NIL); };
|
||||
|
||||
// do-preload
|
||||
var doPreload = function(url) { return (function() {
|
||||
var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash);
|
||||
return fetchPreload(url, headers, _preloadCache);
|
||||
})(); };
|
||||
|
||||
// VERB_SELECTOR
|
||||
var VERB_SELECTOR = "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]";
|
||||
|
||||
// process-elements
|
||||
var processElements = function(root) { return (function() {
|
||||
var root = sxOr(root, domBody());
|
||||
return (isSxTruthy(root) ? ((isSxTruthy(domMatches(root, VERB_SELECTOR)) ? processOne(root) : NIL), (function() {
|
||||
var elements = domQueryAll(root, VERB_SELECTOR);
|
||||
return forEach(processOne, elements);
|
||||
})(), processBoosted(root), processSse(root), (function() {
|
||||
var onEls = domQueryAll(root, "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:responseError]");
|
||||
return forEach(bindInlineHandlers, onEls);
|
||||
})()) : NIL);
|
||||
})(); };
|
||||
|
||||
// process-one
|
||||
var processOne = function(el) { return (isSxTruthy(!isProcessed(el, "bound")) ? (isSxTruthy(!sxOr(domHasAttr(el, "sx-disable"), domClosest(el, "[sx-disable]"))) ? (markProcessed(el, "bound"), (function() {
|
||||
var verbInfo = getVerbInfo(el);
|
||||
return (isSxTruthy(verbInfo) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL);
|
||||
})()) : NIL) : NIL); };
|
||||
|
||||
// handle-popstate
|
||||
var handlePopstate = function(scrollY) { return (function() {
|
||||
var url = browserLocationHref();
|
||||
var main = domQueryById("main-panel");
|
||||
return (isSxTruthy(!main) ? browserReload() : (function() {
|
||||
var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash);
|
||||
headers["SX-History-Restore"] = "true";
|
||||
return fetchAndRestore(main, url, headers, scrollY);
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// engine-init
|
||||
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — DOM adapter (browser-only)
|
||||
@@ -1411,6 +1759,8 @@
|
||||
// Platform interface — Engine (browser-only)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserLocationHref() {
|
||||
return typeof location !== "undefined" ? location.href : "";
|
||||
}
|
||||
@@ -1472,6 +1822,531 @@
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function csrfToken() {
|
||||
if (!_hasDom) return NIL;
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
return m ? m.getAttribute("content") : NIL;
|
||||
}
|
||||
|
||||
function isCrossOrigin(url) {
|
||||
try {
|
||||
var h = new URL(url, location.href).hostname;
|
||||
return h !== location.hostname &&
|
||||
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0);
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
// --- Promises ---
|
||||
|
||||
function promiseResolve(val) { return Promise.resolve(val); }
|
||||
|
||||
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
||||
|
||||
// --- Abort controllers ---
|
||||
|
||||
var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
||||
|
||||
function abortPrevious(el) {
|
||||
if (_controllers) {
|
||||
var prev = _controllers.get(el);
|
||||
if (prev) prev.abort();
|
||||
}
|
||||
}
|
||||
|
||||
function trackController(el, ctrl) {
|
||||
if (_controllers) _controllers.set(el, ctrl);
|
||||
}
|
||||
|
||||
function newAbortController() {
|
||||
return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} };
|
||||
}
|
||||
|
||||
function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; }
|
||||
|
||||
function isAbortError(err) { return err && err.name === "AbortError"; }
|
||||
|
||||
// --- Timers ---
|
||||
|
||||
function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); }
|
||||
function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); }
|
||||
function clearTimeout_(id) { clearTimeout(id); }
|
||||
function requestAnimationFrame_(fn) {
|
||||
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(fn);
|
||||
else setTimeout(fn, 16);
|
||||
}
|
||||
|
||||
// --- Fetch ---
|
||||
|
||||
function fetchRequest(config, successFn, errorFn) {
|
||||
var opts = { method: config.method, headers: config.headers };
|
||||
if (config.signal) opts.signal = config.signal;
|
||||
if (config.body && config.method !== "GET") opts.body = config.body;
|
||||
if (config["cross-origin"]) opts.credentials = "include";
|
||||
|
||||
var p = config.preloaded
|
||||
? Promise.resolve({
|
||||
ok: true, status: 200,
|
||||
headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
|
||||
text: function() { return Promise.resolve(config.preloaded.text); }
|
||||
})
|
||||
: fetch(config.url, opts);
|
||||
|
||||
return p.then(function(resp) {
|
||||
return resp.text().then(function(text) {
|
||||
var getHeader = function(name) {
|
||||
var v = resp.headers.get(name);
|
||||
return v === null ? NIL : v;
|
||||
};
|
||||
return successFn(resp.ok, resp.status, getHeader, text);
|
||||
});
|
||||
}).catch(function(err) {
|
||||
return errorFn(err);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchLocation(headerVal) {
|
||||
if (!_hasDom) return;
|
||||
var locUrl = headerVal;
|
||||
try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {}
|
||||
fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) {
|
||||
return r.text().then(function(t) {
|
||||
var main = document.getElementById("main-panel");
|
||||
if (main) {
|
||||
main.innerHTML = t;
|
||||
postSwap(main);
|
||||
try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAndRestore(main, url, headers, scrollY) {
|
||||
var opts = { headers: headers };
|
||||
try {
|
||||
var h = new URL(url, location.href).hostname;
|
||||
if (h !== location.hostname &&
|
||||
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
|
||||
opts.credentials = "include";
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
fetch(url, opts).then(function(resp) {
|
||||
return resp.text().then(function(text) {
|
||||
text = stripComponentScripts(text);
|
||||
text = extractResponseCss(text);
|
||||
text = text.trim();
|
||||
if (text.charAt(0) === "(") {
|
||||
try {
|
||||
var dom = sxRender(text);
|
||||
var container = document.createElement("div");
|
||||
container.appendChild(dom);
|
||||
processOobSwaps(container, function(t, oob, s) {
|
||||
swapDomNodes(t, oob, s);
|
||||
sxHydrate(t);
|
||||
processElements(t);
|
||||
});
|
||||
var newMain = container.querySelector("#main-panel");
|
||||
morphChildren(main, newMain || container);
|
||||
postSwap(main);
|
||||
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
||||
} catch (err) {
|
||||
console.error("sx-ref popstate error:", err);
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(text, "text/html");
|
||||
var newMain = doc.getElementById("main-panel");
|
||||
if (newMain) {
|
||||
morphChildren(main, newMain);
|
||||
postSwap(main);
|
||||
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).catch(function() { location.reload(); });
|
||||
}
|
||||
|
||||
function fetchPreload(url, headers, cache) {
|
||||
fetch(url, { headers: headers }).then(function(resp) {
|
||||
if (!resp.ok) return;
|
||||
var ct = resp.headers.get("Content-Type") || "";
|
||||
return resp.text().then(function(text) {
|
||||
preloadCacheSet(cache, url, text, ct);
|
||||
});
|
||||
}).catch(function() { /* ignore */ });
|
||||
}
|
||||
|
||||
// --- Request body building ---
|
||||
|
||||
function buildRequestBody(el, method, url) {
|
||||
if (!_hasDom) return { body: null, url: url, "content-type": NIL };
|
||||
var body = null;
|
||||
var ct = NIL;
|
||||
var finalUrl = url;
|
||||
var isJson = el.getAttribute("sx-encoding") === "json";
|
||||
|
||||
if (method !== "GET") {
|
||||
var form = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
||||
if (form) {
|
||||
if (isJson) {
|
||||
var fd = new FormData(form);
|
||||
var obj = {};
|
||||
fd.forEach(function(v, k) {
|
||||
if (obj[k] !== undefined) {
|
||||
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
||||
obj[k].push(v);
|
||||
} else { obj[k] = v; }
|
||||
});
|
||||
body = JSON.stringify(obj);
|
||||
ct = "application/json";
|
||||
} else {
|
||||
body = new URLSearchParams(new FormData(form));
|
||||
ct = "application/x-www-form-urlencoded";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sx-params
|
||||
var paramsSpec = el.getAttribute("sx-params");
|
||||
if (paramsSpec && body instanceof URLSearchParams) {
|
||||
if (paramsSpec === "none") {
|
||||
body = new URLSearchParams();
|
||||
} else if (paramsSpec.indexOf("not ") === 0) {
|
||||
paramsSpec.substring(4).split(",").forEach(function(k) { body.delete(k.trim()); });
|
||||
} else if (paramsSpec !== "*") {
|
||||
var allowed = paramsSpec.split(",").map(function(s) { return s.trim(); });
|
||||
var filtered = new URLSearchParams();
|
||||
allowed.forEach(function(k) {
|
||||
body.getAll(k).forEach(function(v) { filtered.append(k, v); });
|
||||
});
|
||||
body = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
// sx-include
|
||||
var includeSel = el.getAttribute("sx-include");
|
||||
if (includeSel && method !== "GET") {
|
||||
if (!body) body = new URLSearchParams();
|
||||
document.querySelectorAll(includeSel).forEach(function(inp) {
|
||||
if (inp.name) body.append(inp.name, inp.value);
|
||||
});
|
||||
}
|
||||
|
||||
// sx-vals
|
||||
var valsAttr = el.getAttribute("sx-vals");
|
||||
if (valsAttr) {
|
||||
try {
|
||||
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
||||
if (method === "GET") {
|
||||
for (var vk in vals) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
||||
} else if (body instanceof URLSearchParams) {
|
||||
for (var vk2 in vals) body.append(vk2, vals[vk2]);
|
||||
} else if (!body) {
|
||||
body = new URLSearchParams();
|
||||
for (var vk3 in vals) body.append(vk3, vals[vk3]);
|
||||
ct = "application/x-www-form-urlencoded";
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// GET form data → URL
|
||||
if (method === "GET") {
|
||||
var form2 = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
||||
if (form2) {
|
||||
var qs = new URLSearchParams(new FormData(form2)).toString();
|
||||
if (qs) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + qs;
|
||||
}
|
||||
if ((el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA") && el.name) {
|
||||
finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(el.name) + "=" + encodeURIComponent(el.value);
|
||||
}
|
||||
}
|
||||
|
||||
return { body: body, url: finalUrl, "content-type": ct };
|
||||
}
|
||||
|
||||
// --- Loading state ---
|
||||
|
||||
function showIndicator(el) {
|
||||
if (!_hasDom) return NIL;
|
||||
var sel = el.getAttribute("sx-indicator");
|
||||
var ind = sel ? (document.querySelector(sel) || el.closest(sel)) : null;
|
||||
if (ind) { ind.classList.add("sx-request"); ind.style.display = ""; }
|
||||
return ind || NIL;
|
||||
}
|
||||
|
||||
function disableElements(el) {
|
||||
if (!_hasDom) return [];
|
||||
var sel = el.getAttribute("sx-disabled-elt");
|
||||
if (!sel) return [];
|
||||
var elts = Array.prototype.slice.call(document.querySelectorAll(sel));
|
||||
elts.forEach(function(e) { e.disabled = true; });
|
||||
return elts;
|
||||
}
|
||||
|
||||
function clearLoadingState(el, indicator, disabledElts) {
|
||||
el.classList.remove("sx-request");
|
||||
el.removeAttribute("aria-busy");
|
||||
if (indicator && !isNil(indicator)) {
|
||||
indicator.classList.remove("sx-request");
|
||||
indicator.style.display = "none";
|
||||
}
|
||||
if (disabledElts) {
|
||||
for (var i = 0; i < disabledElts.length; i++) disabledElts[i].disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- DOM extras ---
|
||||
|
||||
function domQueryById(id) {
|
||||
return _hasDom ? document.getElementById(id) : null;
|
||||
}
|
||||
|
||||
function domMatches(el, sel) {
|
||||
return el && el.matches ? el.matches(sel) : false;
|
||||
}
|
||||
|
||||
function domClosest(el, sel) {
|
||||
return el && el.closest ? el.closest(sel) : null;
|
||||
}
|
||||
|
||||
function domBody() {
|
||||
return _hasDom ? document.body : null;
|
||||
}
|
||||
|
||||
function domHasClass(el, cls) {
|
||||
return el && el.classList ? el.classList.contains(cls) : false;
|
||||
}
|
||||
|
||||
function domAppendToHead(el) {
|
||||
if (_hasDom && document.head) document.head.appendChild(el);
|
||||
}
|
||||
|
||||
function domParseHtmlDocument(text) {
|
||||
if (!_hasDom) return null;
|
||||
return new DOMParser().parseFromString(text, "text/html");
|
||||
}
|
||||
|
||||
function domOuterHtml(el) {
|
||||
return el ? el.outerHTML : "";
|
||||
}
|
||||
|
||||
function domBodyInnerHtml(doc) {
|
||||
return doc && doc.body ? doc.body.innerHTML : "";
|
||||
}
|
||||
|
||||
// --- Events ---
|
||||
|
||||
function preventDefault_(e) { if (e && e.preventDefault) e.preventDefault(); }
|
||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||
|
||||
function domAddListener(el, event, fn, opts) {
|
||||
if (!el || !el.addEventListener) return;
|
||||
var o = {};
|
||||
if (opts && !isNil(opts)) {
|
||||
if (opts.once || opts["once"]) o.once = true;
|
||||
}
|
||||
el.addEventListener(event, fn, o);
|
||||
}
|
||||
|
||||
// --- Validation ---
|
||||
|
||||
function validateForRequest(el) {
|
||||
if (!_hasDom) return true;
|
||||
var attr = el.getAttribute("sx-validate");
|
||||
if (attr === null) {
|
||||
var vForm = el.closest("[sx-validate]");
|
||||
if (vForm) attr = vForm.getAttribute("sx-validate");
|
||||
}
|
||||
if (attr === null) return true; // no validation configured
|
||||
var form = el.tagName === "FORM" ? el : el.closest("form");
|
||||
if (form && !form.reportValidity()) return false;
|
||||
if (attr && attr !== "true" && attr !== "") {
|
||||
var fn = window[attr];
|
||||
if (typeof fn === "function" && !fn(el)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- View Transitions ---
|
||||
|
||||
function withTransition(enabled, fn) {
|
||||
if (enabled && _hasDom && document.startViewTransition) {
|
||||
document.startViewTransition(fn);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
// --- IntersectionObserver ---
|
||||
|
||||
function observeIntersection(el, fn, once, delay) {
|
||||
if (!_hasDom || !("IntersectionObserver" in window)) { fn(); return; }
|
||||
var fired = false;
|
||||
var d = isNil(delay) ? 0 : delay;
|
||||
var obs = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
if (once && fired) return;
|
||||
fired = true;
|
||||
if (once) obs.unobserve(el);
|
||||
if (d) setTimeout(fn, d); else fn();
|
||||
});
|
||||
});
|
||||
obs.observe(el);
|
||||
}
|
||||
|
||||
// --- EventSource ---
|
||||
|
||||
function eventSourceConnect(url, el) {
|
||||
var source = new EventSource(url);
|
||||
source.addEventListener("error", function() { domDispatch(el, "sx:sseError", {}); });
|
||||
source.addEventListener("open", function() { domDispatch(el, "sx:sseOpen", {}); });
|
||||
if (typeof MutationObserver !== "undefined") {
|
||||
var obs = new MutationObserver(function() {
|
||||
if (!document.body.contains(el)) { source.close(); obs.disconnect(); }
|
||||
});
|
||||
obs.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function eventSourceListen(source, event, fn) {
|
||||
source.addEventListener(event, function(e) { fn(e.data); });
|
||||
}
|
||||
|
||||
// --- Boost bindings ---
|
||||
|
||||
function bindBoostLink(el, href) {
|
||||
el.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
executeRequest(el, { method: "GET", url: href }).then(function() {
|
||||
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindBoostForm(form, method, action) {
|
||||
form.addEventListener("submit", function(e) {
|
||||
e.preventDefault();
|
||||
executeRequest(form, { method: method, url: action }).then(function() {
|
||||
try { history.pushState({ sxUrl: action, scrollY: window.scrollY }, "", action); } catch (err) {}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Inline handlers ---
|
||||
|
||||
function bindInlineHandler(el, eventName, body) {
|
||||
el.addEventListener(eventName, new Function("event", body));
|
||||
}
|
||||
|
||||
// --- Preload binding ---
|
||||
|
||||
function bindPreload(el, events, debounceMs, fn) {
|
||||
var timer = null;
|
||||
events.forEach(function(evt) {
|
||||
el.addEventListener(evt, function() {
|
||||
if (debounceMs) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(fn, debounceMs);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Processing markers ---
|
||||
|
||||
var PROCESSED = "_sxBound";
|
||||
|
||||
function markProcessed(el, key) { el[PROCESSED + key] = true; }
|
||||
function isProcessed(el, key) { return !!el[PROCESSED + key]; }
|
||||
|
||||
// --- Script cloning ---
|
||||
|
||||
function createScriptClone(dead) {
|
||||
var live = document.createElement("script");
|
||||
for (var i = 0; i < dead.attributes.length; i++)
|
||||
live.setAttribute(dead.attributes[i].name, dead.attributes[i].value);
|
||||
live.textContent = dead.textContent;
|
||||
return live;
|
||||
}
|
||||
|
||||
// --- SX API references ---
|
||||
|
||||
function sxRender(source) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.render) return SxObj.render(source);
|
||||
throw new Error("No SX renderer available");
|
||||
}
|
||||
|
||||
function sxProcessScripts(root) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.processScripts) SxObj.processScripts(root || undefined);
|
||||
}
|
||||
|
||||
function sxHydrate(root) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.hydrate) SxObj.hydrate(root || undefined);
|
||||
}
|
||||
|
||||
function loadedComponentNames() {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (!SxObj) return [];
|
||||
var env = SxObj.componentEnv || (SxObj.getEnv ? SxObj.getEnv() : {});
|
||||
return Object.keys(env).filter(function(k) { return k.charAt(0) === "~"; });
|
||||
}
|
||||
|
||||
// --- Response processing ---
|
||||
|
||||
function stripComponentScripts(text) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
return text.replace(/<script[^>]*type="text\/sx"[^>]*data-components[^>]*>([\s\S]*?)<\/script>/gi,
|
||||
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
||||
}
|
||||
|
||||
function extractResponseCss(text) {
|
||||
if (!_hasDom) return text;
|
||||
var target = document.getElementById("sx-css");
|
||||
if (!target) return text;
|
||||
return text.replace(/<style[^>]*data-sx-css[^>]*>([\s\S]*?)<\/style>/gi,
|
||||
function(_, css) { target.textContent += css; return ""; });
|
||||
}
|
||||
|
||||
function selectFromContainer(container, sel) {
|
||||
var frag = document.createDocumentFragment();
|
||||
sel.split(",").forEach(function(s) {
|
||||
container.querySelectorAll(s.trim()).forEach(function(m) { frag.appendChild(m); });
|
||||
});
|
||||
return frag;
|
||||
}
|
||||
|
||||
function childrenToFragment(container) {
|
||||
var frag = document.createDocumentFragment();
|
||||
while (container.firstChild) frag.appendChild(container.firstChild);
|
||||
return frag;
|
||||
}
|
||||
|
||||
function selectHtmlFromDoc(doc, sel) {
|
||||
var parts = sel.split(",").map(function(s) { return s.trim(); });
|
||||
var frags = [];
|
||||
parts.forEach(function(s) {
|
||||
doc.querySelectorAll(s).forEach(function(m) { frags.push(m.outerHTML); });
|
||||
});
|
||||
return frags.join("");
|
||||
}
|
||||
|
||||
// --- Parsing ---
|
||||
|
||||
function tryParseJson(s) {
|
||||
if (!s) return NIL;
|
||||
try { return JSON.parse(s); } catch (e) { return NIL; }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Post-transpilation fixups
|
||||
@@ -1625,9 +2500,30 @@
|
||||
morphNode: typeof morphNode === "function" ? morphNode : null,
|
||||
morphChildren: typeof morphChildren === "function" ? morphChildren : null,
|
||||
swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,
|
||||
process: typeof processElements === "function" ? processElements : null,
|
||||
executeRequest: typeof executeRequest === "function" ? executeRequest : null,
|
||||
postSwap: typeof postSwap === "function" ? postSwap : null,
|
||||
init: typeof engineInit === "function" ? engineInit : null,
|
||||
_version: "ref-2.0 (dom+engine, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
// --- Popstate listener ---
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("popstate", function(e) {
|
||||
handlePopstate(e && e.state ? e.state.scrollY || 0 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxRefInit = function() { engineInit(); };
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||
} else {
|
||||
_sxRefInit();
|
||||
}
|
||||
}
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = SxRef;
|
||||
else global.SxRef = SxRef;
|
||||
|
||||
|
||||
@@ -1400,6 +1400,354 @@
|
||||
// parse-sse-swap
|
||||
var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); };
|
||||
|
||||
// _preload-cache
|
||||
var _preloadCache = {};
|
||||
|
||||
// _css-hash
|
||||
var _cssHash = "";
|
||||
|
||||
// dispatch-trigger-events
|
||||
var dispatchTriggerEvents = function(el, headerVal) { return (isSxTruthy(headerVal) ? (function() {
|
||||
var parsed = tryParseJson(headerVal);
|
||||
return (isSxTruthy((isSxTruthy(parsed) && isDict(parsed))) ? forEach(function(key) { return domDispatch(el, key, dictGet(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() {
|
||||
var n = trim(name);
|
||||
return (isSxTruthy(!(n == "")) ? domDispatch(el, n, {}) : NIL);
|
||||
})(); }, split(headerVal, ",")));
|
||||
})() : NIL); };
|
||||
|
||||
// init-css-tracking
|
||||
var initCssTracking = function() { return (function() {
|
||||
var meta = domQuery("meta[name=\"sx-css-classes\"]");
|
||||
return (isSxTruthy(meta) ? (function() {
|
||||
var content = domGetAttr(meta, "content");
|
||||
return (isSxTruthy(content) ? (_cssHash = content) : NIL);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// execute-request
|
||||
var executeRequest = function(el, verbInfo, extraParams) { return (function() {
|
||||
var currentVerb = getVerbInfo(el);
|
||||
var verb = (isSxTruthy(currentVerb) ? currentVerb : verbInfo);
|
||||
var method = get(verb, "method");
|
||||
var url = get(verb, "url");
|
||||
if (isSxTruthy(!domHasClass(el, "sx-error"))) {
|
||||
domRemoveAttr(el, "data-sx-retry-ms");
|
||||
}
|
||||
return (isSxTruthy((function() {
|
||||
var media = domGetAttr(el, "sx-media");
|
||||
return (isSxTruthy(media) && !browserMediaMatches(media));
|
||||
})()) ? promiseResolve(NIL) : (isSxTruthy((function() {
|
||||
var msg = domGetAttr(el, "sx-confirm");
|
||||
return (isSxTruthy(msg) && !browserConfirm(msg));
|
||||
})()) ? promiseResolve(NIL) : (function() {
|
||||
var promptMsg = domGetAttr(el, "sx-prompt");
|
||||
var params = extraParams;
|
||||
return (isSxTruthy(promptMsg) ? (function() {
|
||||
var promptVal = browserPrompt(promptMsg);
|
||||
return (isSxTruthy(isNil(promptVal)) ? promiseResolve(NIL) : ((params = sxOr(params, {})), dictSet(params, "promptValue", promptVal), doFetch(el, verb, method, url, params)));
|
||||
})() : doFetch(el, verb, method, url, params));
|
||||
})()));
|
||||
})(); };
|
||||
|
||||
// do-fetch
|
||||
var doFetch = function(el, verb, method, url, extraParams) { return (function() {
|
||||
var syncAttr = domGetAttr(el, "sx-sync");
|
||||
if (isSxTruthy((isSxTruthy(syncAttr) && contains(syncAttr, "replace")))) {
|
||||
abortPrevious(el);
|
||||
}
|
||||
return (function() {
|
||||
var ctrl = newAbortController();
|
||||
trackController(el, ctrl);
|
||||
return (function() {
|
||||
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
|
||||
if (isSxTruthy((isSxTruthy(extraParams) && dictHas(extraParams, "promptValue")))) {
|
||||
headers["SX-Prompt"] = get(extraParams, "promptValue");
|
||||
}
|
||||
if (isSxTruthy((isSxTruthy(!(method == "GET")) && browserSameOrigin(url)))) {
|
||||
(function() {
|
||||
var csrf = csrfToken();
|
||||
return (isSxTruthy(csrf) ? dictSet(headers, "X-CSRFToken", csrf) : NIL);
|
||||
})();
|
||||
}
|
||||
return (function() {
|
||||
var bodyInfo = buildRequestBody(el, method, url);
|
||||
return (function() {
|
||||
var body = get(bodyInfo, "body");
|
||||
var finalUrl = get(bodyInfo, "url");
|
||||
var ct = get(bodyInfo, "content-type");
|
||||
if (isSxTruthy(ct)) {
|
||||
headers["Content-Type"] = ct;
|
||||
}
|
||||
return (isSxTruthy(!domDispatch(el, "sx:beforeRequest", {["method"]: method, ["url"]: finalUrl})) ? promiseResolve(NIL) : (domAddClass(el, "sx-request"), domSetAttr(el, "aria-busy", "true"), (function() {
|
||||
var indicator = showIndicator(el);
|
||||
var disabledElts = disableElements(el);
|
||||
var preloaded = (isSxTruthy((method == "GET")) ? preloadCacheGet(_preloadCache, finalUrl) : NIL);
|
||||
return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["preloaded"]: preloaded, ["cross-origin"]: isCrossOrigin(finalUrl)}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status}), handleRetry(el, verb, extraParams)) : (domDispatch(el, "sx:afterRequest", {}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!isAbortError(err)) ? (domDispatch(el, "sx:sendError", {["error"]: err}), handleRetry(el, verb, extraParams)) : NIL)); });
|
||||
})()));
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-fetch-success
|
||||
var handleFetchSuccess = function(el, url, verb, extraParams, getHeader, text) { return (function() {
|
||||
var headers = processResponseHeaders(getHeader);
|
||||
return (isSxTruthy(get(headers, "redirect")) ? browserNavigate(get(headers, "redirect")) : (isSxTruthy((get(headers, "refresh") == "true")) ? browserReload() : (dispatchTriggerEvents(el, get(headers, "trigger")), (function() {
|
||||
var rawSwap = sxOr(domGetAttr(el, "sx-swap"), DEFAULT_SWAP);
|
||||
var target = resolveTarget(el);
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
if (isSxTruthy(get(headers, "retarget"))) {
|
||||
target = sxOr(domQuery(get(headers, "retarget")), target);
|
||||
}
|
||||
if (isSxTruthy(get(headers, "reswap"))) {
|
||||
rawSwap = get(headers, "reswap");
|
||||
}
|
||||
return (function() {
|
||||
var swap = parseSwapSpec(rawSwap, false);
|
||||
var ct = sxOr(get(headers, "content-type"), "");
|
||||
(isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, target, swap, selectSel, text) : handleHtmlResponse(el, target, swap, selectSel, text));
|
||||
if (isSxTruthy(get(headers, "location"))) {
|
||||
fetchLocation(get(headers, "location"));
|
||||
}
|
||||
handleHistory(el, url, headers);
|
||||
domDispatch(el, "sx:afterSwap", {["target"]: target});
|
||||
dispatchTriggerEvents(el, get(headers, "trigger-swap"));
|
||||
return requestAnimationFrame_(function() { return (domDispatch(el, "sx:afterSettle", {["target"]: target}), dispatchTriggerEvents(el, get(headers, "trigger-settle"))); });
|
||||
})();
|
||||
})())));
|
||||
})(); };
|
||||
|
||||
// handle-sx-response
|
||||
var handleSxResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var cleaned = stripComponentScripts(text);
|
||||
var cleaned2 = extractResponseCss(cleaned);
|
||||
return (function() {
|
||||
var source = trim(cleaned2);
|
||||
return (isSxTruthy((isSxTruthy(source) && !(source == ""))) ? (function() {
|
||||
var dom = sxRender(source);
|
||||
var container = domCreateElement("div", NIL);
|
||||
domAppend(container, dom);
|
||||
processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); });
|
||||
return (function() {
|
||||
var selected = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
|
||||
return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapDomNodes(target, selected, get(swap, "style")), hoistHeadElements(target)); }) : NIL);
|
||||
})();
|
||||
})() : NIL);
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-html-response
|
||||
var handleHtmlResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var doc = domParseHtmlDocument(text);
|
||||
sxProcessScripts(doc);
|
||||
processOobSwaps(doc, function(t, oob, s) { return swapHtmlString(t, domOuterHtml(oob), s); });
|
||||
return (function() {
|
||||
var content = (isSxTruthy(selectSel) ? selectHtmlFromDoc(doc, selectSel) : sxOr(domBodyInnerHtml(doc), text));
|
||||
return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapHtmlString(target, content, get(swap, "style")), hoistHeadElements(target)); }) : NIL);
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-retry
|
||||
var handleRetry = function(el, verbInfo, extraParams) { return (function() {
|
||||
var retryAttr = domGetAttr(el, "sx-retry");
|
||||
return (isSxTruthy(retryAttr) ? (function() {
|
||||
var spec = parseRetrySpec(retryAttr);
|
||||
var currentMs = sxOr(parseInt_(domGetAttr(el, "data-sx-retry-ms"), 0), get(spec, "start-ms"));
|
||||
domAddClass(el, "sx-error");
|
||||
domRemoveClass(el, "sx-loading");
|
||||
return setTimeout_(function() { return (domRemoveClass(el, "sx-error"), domAddClass(el, "sx-loading"), domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(currentMs, get(spec, "cap-ms"))))), executeRequest(el, verbInfo, extraParams)); }, currentMs);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// bind-triggers
|
||||
var bindTriggers = function(el, verbInfo) { return (function() {
|
||||
var triggerSpec = domGetAttr(el, "sx-trigger");
|
||||
var triggers = (isSxTruthy(triggerSpec) ? parseTriggerSpec(triggerSpec) : defaultTrigger(domTagName(el)));
|
||||
return forEach(function(trig) { return (function() {
|
||||
var kind = classifyTrigger(trig);
|
||||
return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, verbInfo, NIL); }, sxOr(get(get(trig, "modifiers"), "interval"), 1000)) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, get(get(trig, "modifiers"), "once"), get(get(trig, "modifiers"), "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, 0) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, true, NIL) : bindEvent(el, verbInfo, trig)))));
|
||||
})(); }, triggers);
|
||||
})(); };
|
||||
|
||||
// bind-event
|
||||
var bindEvent = function(el, verbInfo, trig) { return (function() {
|
||||
var eventName = get(trig, "event");
|
||||
var mods = get(trig, "modifiers");
|
||||
var listenTarget = (isSxTruthy(get(mods, "from")) ? sxOr(domQuery(get(mods, "from")), el) : el);
|
||||
var timer = NIL;
|
||||
var lastVal = NIL;
|
||||
return domAddListener(listenTarget, eventName, function(e) { return ((isSxTruthy((eventName == "submit")) ? preventDefault_(e) : NIL), (isSxTruthy((isSxTruthy((eventName == "click")) && (domTagName(el) == "A"))) ? preventDefault_(e) : NIL), (isSxTruthy(!validateForRequest(el)) ? domDispatch(el, "sx:validationFailed", {}) : (isSxTruthy((isSxTruthy(get(mods, "changed")) && isSxTruthy(!isNil(elementValue(el))) && (elementValue(el) == lastVal))) ? NIL : ((isSxTruthy(get(mods, "changed")) ? (lastVal = elementValue(el)) : NIL), (function() {
|
||||
var optState = applyOptimistic(el);
|
||||
var execFn = function() { return (function() {
|
||||
var p = executeRequest(el, verbInfo, NIL);
|
||||
return (isSxTruthy((isSxTruthy(optState) && p)) ? promiseCatch(p, function(_) { return revertOptimistic(optState); }) : NIL);
|
||||
})(); };
|
||||
return (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(execFn, get(mods, "delay")))) : execFn());
|
||||
})())))); }, {["once"]: get(mods, "once")});
|
||||
})(); };
|
||||
|
||||
// post-swap
|
||||
var postSwap = function(root) { return (activateScripts(root), sxProcessScripts(root), sxHydrate(root), processElements(root)); };
|
||||
|
||||
// activate-scripts
|
||||
var activateScripts = function(root) { return (function() {
|
||||
var dead = domQueryAll(root, "script:not([type]), script[type='text/javascript']");
|
||||
return forEach(function(d) { return (function() {
|
||||
var live = createScriptClone(d);
|
||||
return domReplaceChild(domParent(d), live, d);
|
||||
})(); }, dead);
|
||||
})(); };
|
||||
|
||||
// process-oob-swaps
|
||||
var processOobSwaps = function(container, swapFn) { return forEach(function(attr) { return (function() {
|
||||
var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]")));
|
||||
return forEach(function(oob) { return (function() {
|
||||
var swapType = sxOr(domGetAttr(oob, attr), "outerHTML");
|
||||
var targetId = domId(oob);
|
||||
domRemoveAttr(oob, attr);
|
||||
if (isSxTruthy(domParent(oob))) {
|
||||
domRemoveChild(domParent(oob), oob);
|
||||
}
|
||||
return (isSxTruthy(targetId) ? (function() {
|
||||
var target = domQueryById(targetId);
|
||||
return (isSxTruthy(target) ? swapFn(target, oob, swapType) : NIL);
|
||||
})() : NIL);
|
||||
})(); }, oobEls);
|
||||
})(); }, ["sx-swap-oob", "hx-swap-oob"]); };
|
||||
|
||||
// hoist-head-elements
|
||||
var hoistHeadElements = function(root) { return (function() {
|
||||
var styles = domQueryAll(root, "style[data-sx-css]");
|
||||
var links = domQueryAll(root, "link[rel='stylesheet']");
|
||||
{ var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var el = _c[_i]; if (isSxTruthy(domParent(el))) {
|
||||
domRemoveChild(domParent(el), el);
|
||||
} } }
|
||||
return forEach(function(el) { return (isSxTruthy(domParent(el)) ? domRemoveChild(domParent(el), el) : NIL); }, links);
|
||||
})(); };
|
||||
|
||||
// process-boosted
|
||||
var processBoosted = function(root) { return (function() {
|
||||
var containers = domQueryAll(root, "[sx-boost]");
|
||||
if (isSxTruthy(domMatches(root, "[sx-boost]"))) {
|
||||
boostDescendants(root);
|
||||
}
|
||||
return forEach(boostDescendants, containers);
|
||||
})(); };
|
||||
|
||||
// boost-descendants
|
||||
var boostDescendants = function(container) { return ((function() {
|
||||
var links = domQueryAll(container, "a[href]");
|
||||
return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), bindBoostLink(link, domGetAttr(link, "href")), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-select")) ? domSetAttr(link, "sx-select", "#main-panel") : NIL)) : NIL); }, links);
|
||||
})(), (function() {
|
||||
var forms = domQueryAll(container, "form");
|
||||
return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form, "boost")) && shouldBoostForm(form))) ? (markProcessed(form, "boost"), bindBoostForm(form, sxOr(upper(domGetAttr(form, "method")), "GET"), sxOr(domGetAttr(form, "action"), browserLocationHref())), (isSxTruthy(!domHasAttr(form, "sx-target")) ? domSetAttr(form, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(form, "sx-swap")) ? domSetAttr(form, "sx-swap", "innerHTML") : NIL)) : NIL); }, forms);
|
||||
})()); };
|
||||
|
||||
// process-sse
|
||||
var processSse = function(root) { return (function() {
|
||||
var sseEls = domQueryAll(root, "[sx-sse]");
|
||||
if (isSxTruthy(domMatches(root, "[sx-sse]"))) {
|
||||
bindSse(root);
|
||||
}
|
||||
return forEach(bindSse, sseEls);
|
||||
})(); };
|
||||
|
||||
// bind-sse
|
||||
var bindSse = function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), (function() {
|
||||
var url = domGetAttr(el, "sx-sse");
|
||||
return (isSxTruthy(url) ? (function() {
|
||||
var source = eventSourceConnect(url, el);
|
||||
return (function() {
|
||||
var swapEls = domQueryAll(el, "[sx-sse-swap]");
|
||||
if (isSxTruthy(domHasAttr(el, "sx-sse-swap"))) {
|
||||
bindSseSwap(el, source);
|
||||
}
|
||||
return forEach(function(child) { return bindSseSwap(child, source); }, swapEls);
|
||||
})();
|
||||
})() : NIL);
|
||||
})()) : NIL); };
|
||||
|
||||
// bind-sse-swap
|
||||
var bindSseSwap = function(el, source) { return (function() {
|
||||
var eventName = parseSseSwap(el);
|
||||
return eventSourceListen(source, eventName, function(data) { return (function() {
|
||||
var target = sxOr(resolveTarget(el), el);
|
||||
var swapStyle = sxOr(domGetAttr(el, "sx-swap"), "innerHTML");
|
||||
(isSxTruthy(startsWith(trim(data), "(")) ? (function() {
|
||||
var dom = sxRender(data);
|
||||
return swapDomNodes(target, dom, swapStyle);
|
||||
})() : swapHtmlString(target, data, swapStyle));
|
||||
postSwap(target);
|
||||
return domDispatch(el, "sx:sseMessage", {["data"]: data, ["event"]: eventName});
|
||||
})(); });
|
||||
})(); };
|
||||
|
||||
// bind-inline-handlers
|
||||
var bindInlineHandlers = function(el) { return (isSxTruthy(!isProcessed(el, "on")) ? (markProcessed(el, "on"), (function() {
|
||||
var attrs = domAttrList(el);
|
||||
return forEach(function(attr) { return (function() {
|
||||
var name = first(attr);
|
||||
var val = nth(attr, 1);
|
||||
return (isSxTruthy(startsWith(name, "sx-on:")) ? bindInlineHandler(el, slice(name, 6), val) : NIL);
|
||||
})(); }, attrs);
|
||||
})()) : NIL); };
|
||||
|
||||
// bind-preload-for
|
||||
var bindPreloadFor = function(el) { return (isSxTruthy(domHasAttr(el, "sx-preload")) ? (function() {
|
||||
var mode = sxOr(domGetAttr(el, "sx-preload"), "mousedown");
|
||||
var events = (isSxTruthy((mode == "mouseover")) ? ["mouseenter", "focusin"] : ["mousedown", "focusin"]);
|
||||
var debounceMs = (isSxTruthy((mode == "mouseover")) ? 100 : 0);
|
||||
return bindPreload(el, events, debounceMs, function() { return (function() {
|
||||
var verb = getVerbInfo(el);
|
||||
return (isSxTruthy(verb) ? (function() {
|
||||
var url = get(verb, "url");
|
||||
return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? doPreload(url) : NIL);
|
||||
})() : NIL);
|
||||
})(); });
|
||||
})() : NIL); };
|
||||
|
||||
// do-preload
|
||||
var doPreload = function(url) { return (function() {
|
||||
var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash);
|
||||
return fetchPreload(url, headers, _preloadCache);
|
||||
})(); };
|
||||
|
||||
// VERB_SELECTOR
|
||||
var VERB_SELECTOR = "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]";
|
||||
|
||||
// process-elements
|
||||
var processElements = function(root) { return (function() {
|
||||
var root = sxOr(root, domBody());
|
||||
return (isSxTruthy(root) ? ((isSxTruthy(domMatches(root, VERB_SELECTOR)) ? processOne(root) : NIL), (function() {
|
||||
var elements = domQueryAll(root, VERB_SELECTOR);
|
||||
return forEach(processOne, elements);
|
||||
})(), processBoosted(root), processSse(root), (function() {
|
||||
var onEls = domQueryAll(root, "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:responseError]");
|
||||
return forEach(bindInlineHandlers, onEls);
|
||||
})()) : NIL);
|
||||
})(); };
|
||||
|
||||
// process-one
|
||||
var processOne = function(el) { return (isSxTruthy(!isProcessed(el, "bound")) ? (isSxTruthy(!sxOr(domHasAttr(el, "sx-disable"), domClosest(el, "[sx-disable]"))) ? (markProcessed(el, "bound"), (function() {
|
||||
var verbInfo = getVerbInfo(el);
|
||||
return (isSxTruthy(verbInfo) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL);
|
||||
})()) : NIL) : NIL); };
|
||||
|
||||
// handle-popstate
|
||||
var handlePopstate = function(scrollY) { return (function() {
|
||||
var url = browserLocationHref();
|
||||
var main = domQueryById("main-panel");
|
||||
return (isSxTruthy(!main) ? browserReload() : (function() {
|
||||
var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash);
|
||||
headers["SX-History-Restore"] = "true";
|
||||
return fetchAndRestore(main, url, headers, scrollY);
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// engine-init
|
||||
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — DOM adapter (browser-only)
|
||||
@@ -1559,6 +1907,8 @@
|
||||
// Platform interface — Engine (browser-only)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserLocationHref() {
|
||||
return typeof location !== "undefined" ? location.href : "";
|
||||
}
|
||||
@@ -1620,6 +1970,531 @@
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function csrfToken() {
|
||||
if (!_hasDom) return NIL;
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
return m ? m.getAttribute("content") : NIL;
|
||||
}
|
||||
|
||||
function isCrossOrigin(url) {
|
||||
try {
|
||||
var h = new URL(url, location.href).hostname;
|
||||
return h !== location.hostname &&
|
||||
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0);
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
// --- Promises ---
|
||||
|
||||
function promiseResolve(val) { return Promise.resolve(val); }
|
||||
|
||||
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
||||
|
||||
// --- Abort controllers ---
|
||||
|
||||
var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
||||
|
||||
function abortPrevious(el) {
|
||||
if (_controllers) {
|
||||
var prev = _controllers.get(el);
|
||||
if (prev) prev.abort();
|
||||
}
|
||||
}
|
||||
|
||||
function trackController(el, ctrl) {
|
||||
if (_controllers) _controllers.set(el, ctrl);
|
||||
}
|
||||
|
||||
function newAbortController() {
|
||||
return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} };
|
||||
}
|
||||
|
||||
function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; }
|
||||
|
||||
function isAbortError(err) { return err && err.name === "AbortError"; }
|
||||
|
||||
// --- Timers ---
|
||||
|
||||
function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); }
|
||||
function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); }
|
||||
function clearTimeout_(id) { clearTimeout(id); }
|
||||
function requestAnimationFrame_(fn) {
|
||||
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(fn);
|
||||
else setTimeout(fn, 16);
|
||||
}
|
||||
|
||||
// --- Fetch ---
|
||||
|
||||
function fetchRequest(config, successFn, errorFn) {
|
||||
var opts = { method: config.method, headers: config.headers };
|
||||
if (config.signal) opts.signal = config.signal;
|
||||
if (config.body && config.method !== "GET") opts.body = config.body;
|
||||
if (config["cross-origin"]) opts.credentials = "include";
|
||||
|
||||
var p = config.preloaded
|
||||
? Promise.resolve({
|
||||
ok: true, status: 200,
|
||||
headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
|
||||
text: function() { return Promise.resolve(config.preloaded.text); }
|
||||
})
|
||||
: fetch(config.url, opts);
|
||||
|
||||
return p.then(function(resp) {
|
||||
return resp.text().then(function(text) {
|
||||
var getHeader = function(name) {
|
||||
var v = resp.headers.get(name);
|
||||
return v === null ? NIL : v;
|
||||
};
|
||||
return successFn(resp.ok, resp.status, getHeader, text);
|
||||
});
|
||||
}).catch(function(err) {
|
||||
return errorFn(err);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchLocation(headerVal) {
|
||||
if (!_hasDom) return;
|
||||
var locUrl = headerVal;
|
||||
try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {}
|
||||
fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) {
|
||||
return r.text().then(function(t) {
|
||||
var main = document.getElementById("main-panel");
|
||||
if (main) {
|
||||
main.innerHTML = t;
|
||||
postSwap(main);
|
||||
try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAndRestore(main, url, headers, scrollY) {
|
||||
var opts = { headers: headers };
|
||||
try {
|
||||
var h = new URL(url, location.href).hostname;
|
||||
if (h !== location.hostname &&
|
||||
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
|
||||
opts.credentials = "include";
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
fetch(url, opts).then(function(resp) {
|
||||
return resp.text().then(function(text) {
|
||||
text = stripComponentScripts(text);
|
||||
text = extractResponseCss(text);
|
||||
text = text.trim();
|
||||
if (text.charAt(0) === "(") {
|
||||
try {
|
||||
var dom = sxRender(text);
|
||||
var container = document.createElement("div");
|
||||
container.appendChild(dom);
|
||||
processOobSwaps(container, function(t, oob, s) {
|
||||
swapDomNodes(t, oob, s);
|
||||
sxHydrate(t);
|
||||
processElements(t);
|
||||
});
|
||||
var newMain = container.querySelector("#main-panel");
|
||||
morphChildren(main, newMain || container);
|
||||
postSwap(main);
|
||||
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
||||
} catch (err) {
|
||||
console.error("sx-ref popstate error:", err);
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(text, "text/html");
|
||||
var newMain = doc.getElementById("main-panel");
|
||||
if (newMain) {
|
||||
morphChildren(main, newMain);
|
||||
postSwap(main);
|
||||
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).catch(function() { location.reload(); });
|
||||
}
|
||||
|
||||
function fetchPreload(url, headers, cache) {
|
||||
fetch(url, { headers: headers }).then(function(resp) {
|
||||
if (!resp.ok) return;
|
||||
var ct = resp.headers.get("Content-Type") || "";
|
||||
return resp.text().then(function(text) {
|
||||
preloadCacheSet(cache, url, text, ct);
|
||||
});
|
||||
}).catch(function() { /* ignore */ });
|
||||
}
|
||||
|
||||
// --- Request body building ---
|
||||
|
||||
function buildRequestBody(el, method, url) {
|
||||
if (!_hasDom) return { body: null, url: url, "content-type": NIL };
|
||||
var body = null;
|
||||
var ct = NIL;
|
||||
var finalUrl = url;
|
||||
var isJson = el.getAttribute("sx-encoding") === "json";
|
||||
|
||||
if (method !== "GET") {
|
||||
var form = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
||||
if (form) {
|
||||
if (isJson) {
|
||||
var fd = new FormData(form);
|
||||
var obj = {};
|
||||
fd.forEach(function(v, k) {
|
||||
if (obj[k] !== undefined) {
|
||||
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
||||
obj[k].push(v);
|
||||
} else { obj[k] = v; }
|
||||
});
|
||||
body = JSON.stringify(obj);
|
||||
ct = "application/json";
|
||||
} else {
|
||||
body = new URLSearchParams(new FormData(form));
|
||||
ct = "application/x-www-form-urlencoded";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sx-params
|
||||
var paramsSpec = el.getAttribute("sx-params");
|
||||
if (paramsSpec && body instanceof URLSearchParams) {
|
||||
if (paramsSpec === "none") {
|
||||
body = new URLSearchParams();
|
||||
} else if (paramsSpec.indexOf("not ") === 0) {
|
||||
paramsSpec.substring(4).split(",").forEach(function(k) { body.delete(k.trim()); });
|
||||
} else if (paramsSpec !== "*") {
|
||||
var allowed = paramsSpec.split(",").map(function(s) { return s.trim(); });
|
||||
var filtered = new URLSearchParams();
|
||||
allowed.forEach(function(k) {
|
||||
body.getAll(k).forEach(function(v) { filtered.append(k, v); });
|
||||
});
|
||||
body = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
// sx-include
|
||||
var includeSel = el.getAttribute("sx-include");
|
||||
if (includeSel && method !== "GET") {
|
||||
if (!body) body = new URLSearchParams();
|
||||
document.querySelectorAll(includeSel).forEach(function(inp) {
|
||||
if (inp.name) body.append(inp.name, inp.value);
|
||||
});
|
||||
}
|
||||
|
||||
// sx-vals
|
||||
var valsAttr = el.getAttribute("sx-vals");
|
||||
if (valsAttr) {
|
||||
try {
|
||||
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
||||
if (method === "GET") {
|
||||
for (var vk in vals) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
||||
} else if (body instanceof URLSearchParams) {
|
||||
for (var vk2 in vals) body.append(vk2, vals[vk2]);
|
||||
} else if (!body) {
|
||||
body = new URLSearchParams();
|
||||
for (var vk3 in vals) body.append(vk3, vals[vk3]);
|
||||
ct = "application/x-www-form-urlencoded";
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// GET form data → URL
|
||||
if (method === "GET") {
|
||||
var form2 = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
||||
if (form2) {
|
||||
var qs = new URLSearchParams(new FormData(form2)).toString();
|
||||
if (qs) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + qs;
|
||||
}
|
||||
if ((el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA") && el.name) {
|
||||
finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(el.name) + "=" + encodeURIComponent(el.value);
|
||||
}
|
||||
}
|
||||
|
||||
return { body: body, url: finalUrl, "content-type": ct };
|
||||
}
|
||||
|
||||
// --- Loading state ---
|
||||
|
||||
function showIndicator(el) {
|
||||
if (!_hasDom) return NIL;
|
||||
var sel = el.getAttribute("sx-indicator");
|
||||
var ind = sel ? (document.querySelector(sel) || el.closest(sel)) : null;
|
||||
if (ind) { ind.classList.add("sx-request"); ind.style.display = ""; }
|
||||
return ind || NIL;
|
||||
}
|
||||
|
||||
function disableElements(el) {
|
||||
if (!_hasDom) return [];
|
||||
var sel = el.getAttribute("sx-disabled-elt");
|
||||
if (!sel) return [];
|
||||
var elts = Array.prototype.slice.call(document.querySelectorAll(sel));
|
||||
elts.forEach(function(e) { e.disabled = true; });
|
||||
return elts;
|
||||
}
|
||||
|
||||
function clearLoadingState(el, indicator, disabledElts) {
|
||||
el.classList.remove("sx-request");
|
||||
el.removeAttribute("aria-busy");
|
||||
if (indicator && !isNil(indicator)) {
|
||||
indicator.classList.remove("sx-request");
|
||||
indicator.style.display = "none";
|
||||
}
|
||||
if (disabledElts) {
|
||||
for (var i = 0; i < disabledElts.length; i++) disabledElts[i].disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- DOM extras ---
|
||||
|
||||
function domQueryById(id) {
|
||||
return _hasDom ? document.getElementById(id) : null;
|
||||
}
|
||||
|
||||
function domMatches(el, sel) {
|
||||
return el && el.matches ? el.matches(sel) : false;
|
||||
}
|
||||
|
||||
function domClosest(el, sel) {
|
||||
return el && el.closest ? el.closest(sel) : null;
|
||||
}
|
||||
|
||||
function domBody() {
|
||||
return _hasDom ? document.body : null;
|
||||
}
|
||||
|
||||
function domHasClass(el, cls) {
|
||||
return el && el.classList ? el.classList.contains(cls) : false;
|
||||
}
|
||||
|
||||
function domAppendToHead(el) {
|
||||
if (_hasDom && document.head) document.head.appendChild(el);
|
||||
}
|
||||
|
||||
function domParseHtmlDocument(text) {
|
||||
if (!_hasDom) return null;
|
||||
return new DOMParser().parseFromString(text, "text/html");
|
||||
}
|
||||
|
||||
function domOuterHtml(el) {
|
||||
return el ? el.outerHTML : "";
|
||||
}
|
||||
|
||||
function domBodyInnerHtml(doc) {
|
||||
return doc && doc.body ? doc.body.innerHTML : "";
|
||||
}
|
||||
|
||||
// --- Events ---
|
||||
|
||||
function preventDefault_(e) { if (e && e.preventDefault) e.preventDefault(); }
|
||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||
|
||||
function domAddListener(el, event, fn, opts) {
|
||||
if (!el || !el.addEventListener) return;
|
||||
var o = {};
|
||||
if (opts && !isNil(opts)) {
|
||||
if (opts.once || opts["once"]) o.once = true;
|
||||
}
|
||||
el.addEventListener(event, fn, o);
|
||||
}
|
||||
|
||||
// --- Validation ---
|
||||
|
||||
function validateForRequest(el) {
|
||||
if (!_hasDom) return true;
|
||||
var attr = el.getAttribute("sx-validate");
|
||||
if (attr === null) {
|
||||
var vForm = el.closest("[sx-validate]");
|
||||
if (vForm) attr = vForm.getAttribute("sx-validate");
|
||||
}
|
||||
if (attr === null) return true; // no validation configured
|
||||
var form = el.tagName === "FORM" ? el : el.closest("form");
|
||||
if (form && !form.reportValidity()) return false;
|
||||
if (attr && attr !== "true" && attr !== "") {
|
||||
var fn = window[attr];
|
||||
if (typeof fn === "function" && !fn(el)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- View Transitions ---
|
||||
|
||||
function withTransition(enabled, fn) {
|
||||
if (enabled && _hasDom && document.startViewTransition) {
|
||||
document.startViewTransition(fn);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
// --- IntersectionObserver ---
|
||||
|
||||
function observeIntersection(el, fn, once, delay) {
|
||||
if (!_hasDom || !("IntersectionObserver" in window)) { fn(); return; }
|
||||
var fired = false;
|
||||
var d = isNil(delay) ? 0 : delay;
|
||||
var obs = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
if (once && fired) return;
|
||||
fired = true;
|
||||
if (once) obs.unobserve(el);
|
||||
if (d) setTimeout(fn, d); else fn();
|
||||
});
|
||||
});
|
||||
obs.observe(el);
|
||||
}
|
||||
|
||||
// --- EventSource ---
|
||||
|
||||
function eventSourceConnect(url, el) {
|
||||
var source = new EventSource(url);
|
||||
source.addEventListener("error", function() { domDispatch(el, "sx:sseError", {}); });
|
||||
source.addEventListener("open", function() { domDispatch(el, "sx:sseOpen", {}); });
|
||||
if (typeof MutationObserver !== "undefined") {
|
||||
var obs = new MutationObserver(function() {
|
||||
if (!document.body.contains(el)) { source.close(); obs.disconnect(); }
|
||||
});
|
||||
obs.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function eventSourceListen(source, event, fn) {
|
||||
source.addEventListener(event, function(e) { fn(e.data); });
|
||||
}
|
||||
|
||||
// --- Boost bindings ---
|
||||
|
||||
function bindBoostLink(el, href) {
|
||||
el.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
executeRequest(el, { method: "GET", url: href }).then(function() {
|
||||
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindBoostForm(form, method, action) {
|
||||
form.addEventListener("submit", function(e) {
|
||||
e.preventDefault();
|
||||
executeRequest(form, { method: method, url: action }).then(function() {
|
||||
try { history.pushState({ sxUrl: action, scrollY: window.scrollY }, "", action); } catch (err) {}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Inline handlers ---
|
||||
|
||||
function bindInlineHandler(el, eventName, body) {
|
||||
el.addEventListener(eventName, new Function("event", body));
|
||||
}
|
||||
|
||||
// --- Preload binding ---
|
||||
|
||||
function bindPreload(el, events, debounceMs, fn) {
|
||||
var timer = null;
|
||||
events.forEach(function(evt) {
|
||||
el.addEventListener(evt, function() {
|
||||
if (debounceMs) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(fn, debounceMs);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Processing markers ---
|
||||
|
||||
var PROCESSED = "_sxBound";
|
||||
|
||||
function markProcessed(el, key) { el[PROCESSED + key] = true; }
|
||||
function isProcessed(el, key) { return !!el[PROCESSED + key]; }
|
||||
|
||||
// --- Script cloning ---
|
||||
|
||||
function createScriptClone(dead) {
|
||||
var live = document.createElement("script");
|
||||
for (var i = 0; i < dead.attributes.length; i++)
|
||||
live.setAttribute(dead.attributes[i].name, dead.attributes[i].value);
|
||||
live.textContent = dead.textContent;
|
||||
return live;
|
||||
}
|
||||
|
||||
// --- SX API references ---
|
||||
|
||||
function sxRender(source) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.render) return SxObj.render(source);
|
||||
throw new Error("No SX renderer available");
|
||||
}
|
||||
|
||||
function sxProcessScripts(root) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.processScripts) SxObj.processScripts(root || undefined);
|
||||
}
|
||||
|
||||
function sxHydrate(root) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.hydrate) SxObj.hydrate(root || undefined);
|
||||
}
|
||||
|
||||
function loadedComponentNames() {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (!SxObj) return [];
|
||||
var env = SxObj.componentEnv || (SxObj.getEnv ? SxObj.getEnv() : {});
|
||||
return Object.keys(env).filter(function(k) { return k.charAt(0) === "~"; });
|
||||
}
|
||||
|
||||
// --- Response processing ---
|
||||
|
||||
function stripComponentScripts(text) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
return text.replace(/<script[^>]*type="text\/sx"[^>]*data-components[^>]*>([\s\S]*?)<\/script>/gi,
|
||||
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
||||
}
|
||||
|
||||
function extractResponseCss(text) {
|
||||
if (!_hasDom) return text;
|
||||
var target = document.getElementById("sx-css");
|
||||
if (!target) return text;
|
||||
return text.replace(/<style[^>]*data-sx-css[^>]*>([\s\S]*?)<\/style>/gi,
|
||||
function(_, css) { target.textContent += css; return ""; });
|
||||
}
|
||||
|
||||
function selectFromContainer(container, sel) {
|
||||
var frag = document.createDocumentFragment();
|
||||
sel.split(",").forEach(function(s) {
|
||||
container.querySelectorAll(s.trim()).forEach(function(m) { frag.appendChild(m); });
|
||||
});
|
||||
return frag;
|
||||
}
|
||||
|
||||
function childrenToFragment(container) {
|
||||
var frag = document.createDocumentFragment();
|
||||
while (container.firstChild) frag.appendChild(container.firstChild);
|
||||
return frag;
|
||||
}
|
||||
|
||||
function selectHtmlFromDoc(doc, sel) {
|
||||
var parts = sel.split(",").map(function(s) { return s.trim(); });
|
||||
var frags = [];
|
||||
parts.forEach(function(s) {
|
||||
doc.querySelectorAll(s).forEach(function(m) { frags.push(m.outerHTML); });
|
||||
});
|
||||
return frags.join("");
|
||||
}
|
||||
|
||||
// --- Parsing ---
|
||||
|
||||
function tryParseJson(s) {
|
||||
if (!s) return NIL;
|
||||
try { return JSON.parse(s); } catch (e) { return NIL; }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Post-transpilation fixups
|
||||
@@ -1791,9 +2666,30 @@
|
||||
morphNode: typeof morphNode === "function" ? morphNode : null,
|
||||
morphChildren: typeof morphChildren === "function" ? morphChildren : null,
|
||||
swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,
|
||||
process: typeof processElements === "function" ? processElements : null,
|
||||
executeRequest: typeof executeRequest === "function" ? executeRequest : null,
|
||||
postSwap: typeof postSwap === "function" ? postSwap : null,
|
||||
init: typeof engineInit === "function" ? engineInit : null,
|
||||
_version: "ref-2.0 (dom+engine+html+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
// --- Popstate listener ---
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("popstate", function(e) {
|
||||
handlePopstate(e && e.state ? e.state.scrollY || 0 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxRefInit = function() { engineInit(); };
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||
} else {
|
||||
_sxRefInit();
|
||||
}
|
||||
}
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = SxRef;
|
||||
else global.SxRef = SxRef;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user