Merge branch 'worktree-sx-meta-eval' into macros
This commit is contained in:
@@ -1252,6 +1252,9 @@
|
||||
// parse-sse-swap
|
||||
var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); };
|
||||
|
||||
|
||||
// === Transpiled from orchestration ===
|
||||
|
||||
// _preload-cache
|
||||
var _preloadCache = {};
|
||||
|
||||
@@ -1261,15 +1264,15 @@
|
||||
// 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);
|
||||
return (isSxTruthy(parsed) ? forEach(function(key) { return domDispatch(el, key, get(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() {
|
||||
var trimmed = trim(name);
|
||||
return (isSxTruthy(!isEmpty(trimmed)) ? domDispatch(el, trimmed, {}) : NIL);
|
||||
})(); }, split(headerVal, ",")));
|
||||
})() : NIL); };
|
||||
|
||||
// init-css-tracking
|
||||
var initCssTracking = function() { return (function() {
|
||||
var meta = domQuery("meta[name=\"sx-css-classes\"]");
|
||||
var meta = domQuery("meta[name=\"sx-css-hash\"]");
|
||||
return (isSxTruthy(meta) ? (function() {
|
||||
var content = domGetAttr(meta, "content");
|
||||
return (isSxTruthy(content) ? (_cssHash = content) : NIL);
|
||||
@@ -1278,65 +1281,58 @@
|
||||
|
||||
// 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");
|
||||
}
|
||||
var info = sxOr(verbInfo, getVerbInfo(el));
|
||||
return (isSxTruthy(isNil(info)) ? promiseResolve(NIL) : (function() {
|
||||
var verb = get(info, "method");
|
||||
var url = get(info, "url");
|
||||
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));
|
||||
var confirmMsg = domGetAttr(el, "sx-confirm");
|
||||
return (isSxTruthy(confirmMsg) && !browserConfirm(confirmMsg));
|
||||
})()) ? 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));
|
||||
var promptVal = (isSxTruthy(promptMsg) ? browserPrompt(promptMsg) : NIL);
|
||||
return (isSxTruthy((isSxTruthy(promptMsg) && isNil(promptVal))) ? promiseResolve(NIL) : (isSxTruthy(!validateForRequest(el)) ? promiseResolve(NIL) : doFetch(el, verb, verb, url, (isSxTruthy(promptVal) ? assoc(sxOr(extraParams, {}), "SX-Prompt", promptVal) : extraParams))));
|
||||
})()));
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// 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")))) {
|
||||
var sync = domGetAttr(el, "sx-sync");
|
||||
if (isSxTruthy((sync == "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 body = get(bodyInfo, "body");
|
||||
var ct = get(bodyInfo, "content-type");
|
||||
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
|
||||
var csrf = csrfToken();
|
||||
if (isSxTruthy(extraParams)) {
|
||||
{ var _c = keys(extraParams); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; headers[k] = get(extraParams, k); } }
|
||||
}
|
||||
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() {
|
||||
if (isSxTruthy(csrf)) {
|
||||
headers["X-CSRFToken"] = csrf;
|
||||
}
|
||||
return (function() {
|
||||
var cached = preloadCacheGet(_preloadCache, finalUrl);
|
||||
var optimisticState = applyOptimistic(el);
|
||||
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)); });
|
||||
})()));
|
||||
})();
|
||||
domAddClass(el, "sx-request");
|
||||
domSetAttr(el, "aria-busy", "true");
|
||||
domDispatch(el, "sx:beforeRequest", {["url"]: finalUrl, ["method"]: method});
|
||||
return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["cross-origin"]: isCrossOrigin(finalUrl), ["preloaded"]: cached}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status, ["text"]: text}), handleRetry(el, verb, method, finalUrl, extraParams)) : (domDispatch(el, "sx:afterRequest", {["status"]: status}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!isAbortError(err)) ? domDispatch(el, "sx:requestError", {["error"]: err}) : NIL)); });
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
@@ -1344,257 +1340,225 @@
|
||||
|
||||
// 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"))); });
|
||||
var respHeaders = processResponseHeaders(getHeader);
|
||||
(function() {
|
||||
var newHash = get(respHeaders, "css-hash");
|
||||
return (isSxTruthy(newHash) ? (_cssHash = newHash) : NIL);
|
||||
})();
|
||||
dispatchTriggerEvents(el, get(respHeaders, "trigger"));
|
||||
return (isSxTruthy(get(respHeaders, "redirect")) ? browserNavigate(get(respHeaders, "redirect")) : (isSxTruthy(get(respHeaders, "refresh")) ? browserReload() : (isSxTruthy(get(respHeaders, "location")) ? fetchLocation(get(respHeaders, "location")) : (function() {
|
||||
var targetEl = (isSxTruthy(get(respHeaders, "retarget")) ? domQuery(get(respHeaders, "retarget")) : resolveTarget(el));
|
||||
var swapSpec = parseSwapSpec(sxOr(get(respHeaders, "reswap"), domGetAttr(el, "sx-swap")), domHasClass(domBody(), "sx-transitions"));
|
||||
var swapStyle = get(swapSpec, "style");
|
||||
var useTransition = get(swapSpec, "transition");
|
||||
var ct = sxOr(get(respHeaders, "content-type"), "");
|
||||
(isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, targetEl, text, swapStyle, useTransition) : handleHtmlResponse(el, targetEl, text, swapStyle, useTransition));
|
||||
dispatchTriggerEvents(el, get(respHeaders, "trigger-swap"));
|
||||
handleHistory(el, url, respHeaders);
|
||||
if (isSxTruthy(get(respHeaders, "trigger-settle"))) {
|
||||
setTimeout_(function() { return dispatchTriggerEvents(el, get(respHeaders, "trigger-settle")); }, 20);
|
||||
}
|
||||
return domDispatch(el, "sx:afterSwap", {["target"]: targetEl, ["swap"]: swapStyle});
|
||||
})())));
|
||||
})(); };
|
||||
|
||||
// handle-sx-response
|
||||
var handleSxResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var handleSxResponse = function(el, target, text, swapStyle, useTransition) { 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 final = extractResponseCss(cleaned);
|
||||
return (function() {
|
||||
var trimmed = trim(final);
|
||||
return (isSxTruthy(!isEmpty(trimmed)) ? (function() {
|
||||
var rendered = sxRender(trimmed);
|
||||
var container = domCreateElement("div", NIL);
|
||||
domAppend(container, dom);
|
||||
domAppend(container, rendered);
|
||||
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);
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
|
||||
return withTransition(useTransition, function() { return swapDomNodes(target, content, swapStyle); });
|
||||
})();
|
||||
})() : NIL);
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-html-response
|
||||
var handleHtmlResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var handleHtmlResponse = function(el, target, text, swapStyle, useTransition) { 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);
|
||||
})();
|
||||
return (isSxTruthy(doc) ? (function() {
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
return (isSxTruthy(selectSel) ? (function() {
|
||||
var html = selectHtmlFromDoc(doc, selectSel);
|
||||
return withTransition(useTransition, function() { return swapHtmlString(target, html, swapStyle); });
|
||||
})() : (function() {
|
||||
var container = domCreateElement("div", NIL);
|
||||
domSetInnerHtml(container, domBodyInnerHtml(doc));
|
||||
processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); });
|
||||
hoistHeadElements(container);
|
||||
return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); });
|
||||
})());
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// handle-retry
|
||||
var handleRetry = function(el, verbInfo, extraParams) { return (function() {
|
||||
var handleRetry = function(el, verb, method, url, 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);
|
||||
return (isSxTruthy(spec) ? (function() {
|
||||
var currentMs = sxOr(domGetAttr(el, "data-sx-retry-ms"), get(spec, "start-ms"));
|
||||
return (function() {
|
||||
var ms = parseInt_(currentMs, get(spec, "start-ms"));
|
||||
domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(ms, get(spec, "cap-ms")))));
|
||||
return setTimeout_(function() { return doFetch(el, verb, method, url, extraParams); }, ms);
|
||||
})();
|
||||
})() : 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)))));
|
||||
var triggers = sxOr(parseTriggerSpec(domGetAttr(el, "sx-trigger")), defaultTrigger(domTagName(el)));
|
||||
return forEach(function(trigger) { return (function() {
|
||||
var kind = classifyTrigger(trigger);
|
||||
var mods = get(trigger, "modifiers");
|
||||
return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "interval")) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy((kind == "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL)))));
|
||||
})(); }, 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 bindEvent = function(el, eventName, mods, verbInfo) { return (function() {
|
||||
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")});
|
||||
var listenTarget = (isSxTruthy(get(mods, "from")) ? domQuery(get(mods, "from")) : el);
|
||||
return (isSxTruthy(listenTarget) ? domAddListener(listenTarget, eventName, function(e) { return (function() {
|
||||
var shouldFire = true;
|
||||
if (isSxTruthy(get(mods, "changed"))) {
|
||||
(function() {
|
||||
var val = elementValue(el);
|
||||
return (isSxTruthy((val == lastVal)) ? (shouldFire = false) : (lastVal = val));
|
||||
})();
|
||||
}
|
||||
return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL))) : NIL);
|
||||
})(); }, (isSxTruthy(get(mods, "once")) ? {["once"]: true} : NIL)) : NIL);
|
||||
})(); };
|
||||
|
||||
// post-swap
|
||||
var postSwap = function(root) { return (activateScripts(root), sxProcessScripts(root), sxHydrate(root), processElements(root)); };
|
||||
var postSwap = function(root) { return activateScripts(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);
|
||||
})(); };
|
||||
var activateScripts = function(root) { return (isSxTruthy(root) ? (function() {
|
||||
var scripts = domQueryAll(root, "script");
|
||||
return forEach(function(dead) { return (isSxTruthy((isSxTruthy(!domHasAttr(dead, "data-components")) && !domHasAttr(dead, "data-sx-activated"))) ? (function() {
|
||||
var live = createScriptClone(dead);
|
||||
domSetAttr(live, "data-sx-activated", "true");
|
||||
return domReplaceChild(domParent(dead), live, dead);
|
||||
})() : NIL); }, scripts);
|
||||
})() : NIL); };
|
||||
|
||||
// process-oob-swaps
|
||||
var processOobSwaps = function(container, swapFn) { return forEach(function(attr) { return (function() {
|
||||
var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]")));
|
||||
var processOobSwaps = function(container, swapFn) { return (function() {
|
||||
var oobs = findOobSwaps(container);
|
||||
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 targetId = get(oob, "target-id");
|
||||
var target = domQueryById(targetId);
|
||||
return (isSxTruthy(target) ? swapFn(target, oob, swapType) : NIL);
|
||||
})() : NIL);
|
||||
})(); }, oobEls);
|
||||
})(); }, ["sx-swap-oob", "hx-swap-oob"]); };
|
||||
var oobEl = get(oob, "element");
|
||||
var swapType = get(oob, "swap-type");
|
||||
if (isSxTruthy(domParent(oobEl))) {
|
||||
domRemoveChild(domParent(oobEl), oobEl);
|
||||
}
|
||||
return (isSxTruthy(target) ? swapFn(target, oobEl, swapType) : NIL);
|
||||
})(); }, oobs);
|
||||
})(); };
|
||||
|
||||
// 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);
|
||||
})(); };
|
||||
var hoistHeadElements = function(container) { return forEach(function(style) { return (isSxTruthy(domParent(style)) ? domRemoveChild(domParent(style), style) : NIL); }, domQueryAll(container, "style[data-sx-css]")); };
|
||||
|
||||
// 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);
|
||||
})(); };
|
||||
var processBoosted = function(root) { return forEach(function(container) { return boostDescendants(container); }, domQueryAll(sxOr(root, domBody()), "[sx-boost]")); };
|
||||
|
||||
// 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);
|
||||
})()); };
|
||||
var boostDescendants = function(container) { return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), (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-push-url")) ? domSetAttr(link, "sx-push-url", "true") : NIL), bindBoostLink(link, domGetAttr(link, "href"))) : NIL); }, domQueryAll(container, "a[href]")); };
|
||||
|
||||
// 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);
|
||||
})(); };
|
||||
var processSse = function(root) { return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), bindSse(el)) : NIL); }, domQueryAll(sxOr(root, domBody()), "[sx-sse]")); };
|
||||
|
||||
// bind-sse
|
||||
var bindSse = function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), (function() {
|
||||
var bindSse = function(el) { return (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);
|
||||
})();
|
||||
var eventName = parseSseSwap(el);
|
||||
return eventSourceListen(source, eventName, function(data) { return bindSseSwap(el, data); });
|
||||
})() : 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});
|
||||
})(); });
|
||||
var bindSseSwap = function(el, data) { return (function() {
|
||||
var target = resolveTarget(el);
|
||||
var swapSpec = parseSwapSpec(domGetAttr(el, "sx-swap"), domHasClass(domBody(), "sx-transitions"));
|
||||
var swapStyle = get(swapSpec, "style");
|
||||
var useTransition = get(swapSpec, "transition");
|
||||
var trimmed = trim(data);
|
||||
return (isSxTruthy(!isEmpty(trimmed)) ? (isSxTruthy(startsWith(trimmed, "(")) ? (function() {
|
||||
var rendered = sxRender(trimmed);
|
||||
var container = domCreateElement("div", NIL);
|
||||
domAppend(container, rendered);
|
||||
return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); });
|
||||
})() : withTransition(useTransition, function() { return swapHtmlString(target, trimmed, swapStyle); })) : NIL);
|
||||
})(); };
|
||||
|
||||
// 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 bindInlineHandlers = function(root) { return forEach(function(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); };
|
||||
var body = nth(attr, 1);
|
||||
return (isSxTruthy(startsWith(name, "sx-on:")) ? (function() {
|
||||
var eventName = slice(name, 6);
|
||||
return (isSxTruthy(!isProcessed(el, (String("on:") + String(eventName)))) ? (markProcessed(el, (String("on:") + String(eventName))), bindInlineHandler(el, eventName, body)) : NIL);
|
||||
})() : NIL);
|
||||
})(); }, domAttrList(el)); }, domQueryAll(sxOr(root, domBody()), "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:load]")); };
|
||||
|
||||
// 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);
|
||||
var bindPreloadFor = function(el) { return (function() {
|
||||
var preloadAttr = domGetAttr(el, "sx-preload");
|
||||
return (isSxTruthy(preloadAttr) ? (function() {
|
||||
var info = getVerbInfo(el);
|
||||
return (isSxTruthy(info) ? (function() {
|
||||
var url = get(info, "url");
|
||||
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
|
||||
var events = (isSxTruthy((preloadAttr == "mousedown")) ? ["mousedown", "touchstart"] : ["mouseover"]);
|
||||
var debounceMs = (isSxTruthy((preloadAttr == "mousedown")) ? 0 : 100);
|
||||
return bindPreload(el, events, debounceMs, function() { return doPreload(url, headers); });
|
||||
})() : NIL);
|
||||
})() : NIL);
|
||||
})(); });
|
||||
})() : NIL); };
|
||||
|
||||
// do-preload
|
||||
var doPreload = function(url) { return (function() {
|
||||
var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash);
|
||||
return fetchPreload(url, headers, _preloadCache);
|
||||
})(); };
|
||||
|
||||
// do-preload
|
||||
var doPreload = function(url, headers) { return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? fetchPreload(url, headers, _preloadCache) : NIL); };
|
||||
|
||||
// VERB_SELECTOR
|
||||
var VERB_SELECTOR = "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]";
|
||||
var VERB_SELECTOR = (String("[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);
|
||||
var els = domQueryAll(sxOr(root, domBody()), VERB_SELECTOR);
|
||||
return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "verb")) ? (markProcessed(el, "verb"), processOne(el)) : NIL); }, els);
|
||||
})(); };
|
||||
|
||||
// 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 processOne = function(el) { return (function() {
|
||||
var verbInfo = getVerbInfo(el);
|
||||
return (isSxTruthy(verbInfo) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL);
|
||||
})()) : NIL) : NIL); };
|
||||
return (isSxTruthy(verbInfo) ? (isSxTruthy(!domHasAttr(el, "sx-disable")) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : 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";
|
||||
var url = browserLocationHref();
|
||||
return (isSxTruthy(main) ? (function() {
|
||||
var headers = buildRequestHeaders(main, loadedComponentNames(), _cssHash);
|
||||
return fetchAndRestore(main, url, headers, scrollY);
|
||||
})());
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// engine-init
|
||||
@@ -1756,11 +1720,9 @@
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Engine (browser-only)
|
||||
// Platform interface — Engine pure logic (browser + node compatible)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserLocationHref() {
|
||||
return typeof location !== "undefined" ? location.href : "";
|
||||
}
|
||||
@@ -1784,6 +1746,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
try {
|
||||
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
||||
return JSON.parse(s);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Orchestration (browser-only)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserNavigate(url) {
|
||||
if (typeof location !== "undefined") location.assign(url);
|
||||
}
|
||||
@@ -1812,16 +1791,6 @@
|
||||
return r === null ? NIL : r;
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
try {
|
||||
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
||||
return JSON.parse(s);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function csrfToken() {
|
||||
if (!_hasDom) return NIL;
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
@@ -2504,7 +2473,7 @@
|
||||
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)"
|
||||
_version: "ref-2.0 (dom+engine+orchestration, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1400,6 +1400,9 @@
|
||||
// parse-sse-swap
|
||||
var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); };
|
||||
|
||||
|
||||
// === Transpiled from orchestration ===
|
||||
|
||||
// _preload-cache
|
||||
var _preloadCache = {};
|
||||
|
||||
@@ -1409,15 +1412,15 @@
|
||||
// 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);
|
||||
return (isSxTruthy(parsed) ? forEach(function(key) { return domDispatch(el, key, get(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() {
|
||||
var trimmed = trim(name);
|
||||
return (isSxTruthy(!isEmpty(trimmed)) ? domDispatch(el, trimmed, {}) : NIL);
|
||||
})(); }, split(headerVal, ",")));
|
||||
})() : NIL); };
|
||||
|
||||
// init-css-tracking
|
||||
var initCssTracking = function() { return (function() {
|
||||
var meta = domQuery("meta[name=\"sx-css-classes\"]");
|
||||
var meta = domQuery("meta[name=\"sx-css-hash\"]");
|
||||
return (isSxTruthy(meta) ? (function() {
|
||||
var content = domGetAttr(meta, "content");
|
||||
return (isSxTruthy(content) ? (_cssHash = content) : NIL);
|
||||
@@ -1426,65 +1429,58 @@
|
||||
|
||||
// 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");
|
||||
}
|
||||
var info = sxOr(verbInfo, getVerbInfo(el));
|
||||
return (isSxTruthy(isNil(info)) ? promiseResolve(NIL) : (function() {
|
||||
var verb = get(info, "method");
|
||||
var url = get(info, "url");
|
||||
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));
|
||||
var confirmMsg = domGetAttr(el, "sx-confirm");
|
||||
return (isSxTruthy(confirmMsg) && !browserConfirm(confirmMsg));
|
||||
})()) ? 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));
|
||||
var promptVal = (isSxTruthy(promptMsg) ? browserPrompt(promptMsg) : NIL);
|
||||
return (isSxTruthy((isSxTruthy(promptMsg) && isNil(promptVal))) ? promiseResolve(NIL) : (isSxTruthy(!validateForRequest(el)) ? promiseResolve(NIL) : doFetch(el, verb, verb, url, (isSxTruthy(promptVal) ? assoc(sxOr(extraParams, {}), "SX-Prompt", promptVal) : extraParams))));
|
||||
})()));
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// 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")))) {
|
||||
var sync = domGetAttr(el, "sx-sync");
|
||||
if (isSxTruthy((sync == "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 body = get(bodyInfo, "body");
|
||||
var ct = get(bodyInfo, "content-type");
|
||||
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
|
||||
var csrf = csrfToken();
|
||||
if (isSxTruthy(extraParams)) {
|
||||
{ var _c = keys(extraParams); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; headers[k] = get(extraParams, k); } }
|
||||
}
|
||||
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() {
|
||||
if (isSxTruthy(csrf)) {
|
||||
headers["X-CSRFToken"] = csrf;
|
||||
}
|
||||
return (function() {
|
||||
var cached = preloadCacheGet(_preloadCache, finalUrl);
|
||||
var optimisticState = applyOptimistic(el);
|
||||
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)); });
|
||||
})()));
|
||||
})();
|
||||
domAddClass(el, "sx-request");
|
||||
domSetAttr(el, "aria-busy", "true");
|
||||
domDispatch(el, "sx:beforeRequest", {["url"]: finalUrl, ["method"]: method});
|
||||
return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["cross-origin"]: isCrossOrigin(finalUrl), ["preloaded"]: cached}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status, ["text"]: text}), handleRetry(el, verb, method, finalUrl, extraParams)) : (domDispatch(el, "sx:afterRequest", {["status"]: status}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!isAbortError(err)) ? domDispatch(el, "sx:requestError", {["error"]: err}) : NIL)); });
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
@@ -1492,257 +1488,225 @@
|
||||
|
||||
// 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"))); });
|
||||
var respHeaders = processResponseHeaders(getHeader);
|
||||
(function() {
|
||||
var newHash = get(respHeaders, "css-hash");
|
||||
return (isSxTruthy(newHash) ? (_cssHash = newHash) : NIL);
|
||||
})();
|
||||
dispatchTriggerEvents(el, get(respHeaders, "trigger"));
|
||||
return (isSxTruthy(get(respHeaders, "redirect")) ? browserNavigate(get(respHeaders, "redirect")) : (isSxTruthy(get(respHeaders, "refresh")) ? browserReload() : (isSxTruthy(get(respHeaders, "location")) ? fetchLocation(get(respHeaders, "location")) : (function() {
|
||||
var targetEl = (isSxTruthy(get(respHeaders, "retarget")) ? domQuery(get(respHeaders, "retarget")) : resolveTarget(el));
|
||||
var swapSpec = parseSwapSpec(sxOr(get(respHeaders, "reswap"), domGetAttr(el, "sx-swap")), domHasClass(domBody(), "sx-transitions"));
|
||||
var swapStyle = get(swapSpec, "style");
|
||||
var useTransition = get(swapSpec, "transition");
|
||||
var ct = sxOr(get(respHeaders, "content-type"), "");
|
||||
(isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, targetEl, text, swapStyle, useTransition) : handleHtmlResponse(el, targetEl, text, swapStyle, useTransition));
|
||||
dispatchTriggerEvents(el, get(respHeaders, "trigger-swap"));
|
||||
handleHistory(el, url, respHeaders);
|
||||
if (isSxTruthy(get(respHeaders, "trigger-settle"))) {
|
||||
setTimeout_(function() { return dispatchTriggerEvents(el, get(respHeaders, "trigger-settle")); }, 20);
|
||||
}
|
||||
return domDispatch(el, "sx:afterSwap", {["target"]: targetEl, ["swap"]: swapStyle});
|
||||
})())));
|
||||
})(); };
|
||||
|
||||
// handle-sx-response
|
||||
var handleSxResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var handleSxResponse = function(el, target, text, swapStyle, useTransition) { 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 final = extractResponseCss(cleaned);
|
||||
return (function() {
|
||||
var trimmed = trim(final);
|
||||
return (isSxTruthy(!isEmpty(trimmed)) ? (function() {
|
||||
var rendered = sxRender(trimmed);
|
||||
var container = domCreateElement("div", NIL);
|
||||
domAppend(container, dom);
|
||||
domAppend(container, rendered);
|
||||
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);
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
|
||||
return withTransition(useTransition, function() { return swapDomNodes(target, content, swapStyle); });
|
||||
})();
|
||||
})() : NIL);
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// handle-html-response
|
||||
var handleHtmlResponse = function(el, target, swap, selectSel, text) { return (function() {
|
||||
var handleHtmlResponse = function(el, target, text, swapStyle, useTransition) { 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);
|
||||
})();
|
||||
return (isSxTruthy(doc) ? (function() {
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
return (isSxTruthy(selectSel) ? (function() {
|
||||
var html = selectHtmlFromDoc(doc, selectSel);
|
||||
return withTransition(useTransition, function() { return swapHtmlString(target, html, swapStyle); });
|
||||
})() : (function() {
|
||||
var container = domCreateElement("div", NIL);
|
||||
domSetInnerHtml(container, domBodyInnerHtml(doc));
|
||||
processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); });
|
||||
hoistHeadElements(container);
|
||||
return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); });
|
||||
})());
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// handle-retry
|
||||
var handleRetry = function(el, verbInfo, extraParams) { return (function() {
|
||||
var handleRetry = function(el, verb, method, url, 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);
|
||||
return (isSxTruthy(spec) ? (function() {
|
||||
var currentMs = sxOr(domGetAttr(el, "data-sx-retry-ms"), get(spec, "start-ms"));
|
||||
return (function() {
|
||||
var ms = parseInt_(currentMs, get(spec, "start-ms"));
|
||||
domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(ms, get(spec, "cap-ms")))));
|
||||
return setTimeout_(function() { return doFetch(el, verb, method, url, extraParams); }, ms);
|
||||
})();
|
||||
})() : 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)))));
|
||||
var triggers = sxOr(parseTriggerSpec(domGetAttr(el, "sx-trigger")), defaultTrigger(domTagName(el)));
|
||||
return forEach(function(trigger) { return (function() {
|
||||
var kind = classifyTrigger(trigger);
|
||||
var mods = get(trigger, "modifiers");
|
||||
return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "interval")) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy((kind == "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL)))));
|
||||
})(); }, 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 bindEvent = function(el, eventName, mods, verbInfo) { return (function() {
|
||||
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")});
|
||||
var listenTarget = (isSxTruthy(get(mods, "from")) ? domQuery(get(mods, "from")) : el);
|
||||
return (isSxTruthy(listenTarget) ? domAddListener(listenTarget, eventName, function(e) { return (function() {
|
||||
var shouldFire = true;
|
||||
if (isSxTruthy(get(mods, "changed"))) {
|
||||
(function() {
|
||||
var val = elementValue(el);
|
||||
return (isSxTruthy((val == lastVal)) ? (shouldFire = false) : (lastVal = val));
|
||||
})();
|
||||
}
|
||||
return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL))) : NIL);
|
||||
})(); }, (isSxTruthy(get(mods, "once")) ? {["once"]: true} : NIL)) : NIL);
|
||||
})(); };
|
||||
|
||||
// post-swap
|
||||
var postSwap = function(root) { return (activateScripts(root), sxProcessScripts(root), sxHydrate(root), processElements(root)); };
|
||||
var postSwap = function(root) { return activateScripts(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);
|
||||
})(); };
|
||||
var activateScripts = function(root) { return (isSxTruthy(root) ? (function() {
|
||||
var scripts = domQueryAll(root, "script");
|
||||
return forEach(function(dead) { return (isSxTruthy((isSxTruthy(!domHasAttr(dead, "data-components")) && !domHasAttr(dead, "data-sx-activated"))) ? (function() {
|
||||
var live = createScriptClone(dead);
|
||||
domSetAttr(live, "data-sx-activated", "true");
|
||||
return domReplaceChild(domParent(dead), live, dead);
|
||||
})() : NIL); }, scripts);
|
||||
})() : NIL); };
|
||||
|
||||
// process-oob-swaps
|
||||
var processOobSwaps = function(container, swapFn) { return forEach(function(attr) { return (function() {
|
||||
var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]")));
|
||||
var processOobSwaps = function(container, swapFn) { return (function() {
|
||||
var oobs = findOobSwaps(container);
|
||||
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 targetId = get(oob, "target-id");
|
||||
var target = domQueryById(targetId);
|
||||
return (isSxTruthy(target) ? swapFn(target, oob, swapType) : NIL);
|
||||
})() : NIL);
|
||||
})(); }, oobEls);
|
||||
})(); }, ["sx-swap-oob", "hx-swap-oob"]); };
|
||||
var oobEl = get(oob, "element");
|
||||
var swapType = get(oob, "swap-type");
|
||||
if (isSxTruthy(domParent(oobEl))) {
|
||||
domRemoveChild(domParent(oobEl), oobEl);
|
||||
}
|
||||
return (isSxTruthy(target) ? swapFn(target, oobEl, swapType) : NIL);
|
||||
})(); }, oobs);
|
||||
})(); };
|
||||
|
||||
// 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);
|
||||
})(); };
|
||||
var hoistHeadElements = function(container) { return forEach(function(style) { return (isSxTruthy(domParent(style)) ? domRemoveChild(domParent(style), style) : NIL); }, domQueryAll(container, "style[data-sx-css]")); };
|
||||
|
||||
// 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);
|
||||
})(); };
|
||||
var processBoosted = function(root) { return forEach(function(container) { return boostDescendants(container); }, domQueryAll(sxOr(root, domBody()), "[sx-boost]")); };
|
||||
|
||||
// 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);
|
||||
})()); };
|
||||
var boostDescendants = function(container) { return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), (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-push-url")) ? domSetAttr(link, "sx-push-url", "true") : NIL), bindBoostLink(link, domGetAttr(link, "href"))) : NIL); }, domQueryAll(container, "a[href]")); };
|
||||
|
||||
// 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);
|
||||
})(); };
|
||||
var processSse = function(root) { return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), bindSse(el)) : NIL); }, domQueryAll(sxOr(root, domBody()), "[sx-sse]")); };
|
||||
|
||||
// bind-sse
|
||||
var bindSse = function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), (function() {
|
||||
var bindSse = function(el) { return (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);
|
||||
})();
|
||||
var eventName = parseSseSwap(el);
|
||||
return eventSourceListen(source, eventName, function(data) { return bindSseSwap(el, data); });
|
||||
})() : 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});
|
||||
})(); });
|
||||
var bindSseSwap = function(el, data) { return (function() {
|
||||
var target = resolveTarget(el);
|
||||
var swapSpec = parseSwapSpec(domGetAttr(el, "sx-swap"), domHasClass(domBody(), "sx-transitions"));
|
||||
var swapStyle = get(swapSpec, "style");
|
||||
var useTransition = get(swapSpec, "transition");
|
||||
var trimmed = trim(data);
|
||||
return (isSxTruthy(!isEmpty(trimmed)) ? (isSxTruthy(startsWith(trimmed, "(")) ? (function() {
|
||||
var rendered = sxRender(trimmed);
|
||||
var container = domCreateElement("div", NIL);
|
||||
domAppend(container, rendered);
|
||||
return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); });
|
||||
})() : withTransition(useTransition, function() { return swapHtmlString(target, trimmed, swapStyle); })) : NIL);
|
||||
})(); };
|
||||
|
||||
// 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 bindInlineHandlers = function(root) { return forEach(function(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); };
|
||||
var body = nth(attr, 1);
|
||||
return (isSxTruthy(startsWith(name, "sx-on:")) ? (function() {
|
||||
var eventName = slice(name, 6);
|
||||
return (isSxTruthy(!isProcessed(el, (String("on:") + String(eventName)))) ? (markProcessed(el, (String("on:") + String(eventName))), bindInlineHandler(el, eventName, body)) : NIL);
|
||||
})() : NIL);
|
||||
})(); }, domAttrList(el)); }, domQueryAll(sxOr(root, domBody()), "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:load]")); };
|
||||
|
||||
// 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);
|
||||
var bindPreloadFor = function(el) { return (function() {
|
||||
var preloadAttr = domGetAttr(el, "sx-preload");
|
||||
return (isSxTruthy(preloadAttr) ? (function() {
|
||||
var info = getVerbInfo(el);
|
||||
return (isSxTruthy(info) ? (function() {
|
||||
var url = get(info, "url");
|
||||
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
|
||||
var events = (isSxTruthy((preloadAttr == "mousedown")) ? ["mousedown", "touchstart"] : ["mouseover"]);
|
||||
var debounceMs = (isSxTruthy((preloadAttr == "mousedown")) ? 0 : 100);
|
||||
return bindPreload(el, events, debounceMs, function() { return doPreload(url, headers); });
|
||||
})() : NIL);
|
||||
})() : NIL);
|
||||
})(); });
|
||||
})() : NIL); };
|
||||
|
||||
// do-preload
|
||||
var doPreload = function(url) { return (function() {
|
||||
var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash);
|
||||
return fetchPreload(url, headers, _preloadCache);
|
||||
})(); };
|
||||
|
||||
// do-preload
|
||||
var doPreload = function(url, headers) { return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? fetchPreload(url, headers, _preloadCache) : NIL); };
|
||||
|
||||
// VERB_SELECTOR
|
||||
var VERB_SELECTOR = "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]";
|
||||
var VERB_SELECTOR = (String("[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);
|
||||
var els = domQueryAll(sxOr(root, domBody()), VERB_SELECTOR);
|
||||
return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "verb")) ? (markProcessed(el, "verb"), processOne(el)) : NIL); }, els);
|
||||
})(); };
|
||||
|
||||
// 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 processOne = function(el) { return (function() {
|
||||
var verbInfo = getVerbInfo(el);
|
||||
return (isSxTruthy(verbInfo) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL);
|
||||
})()) : NIL) : NIL); };
|
||||
return (isSxTruthy(verbInfo) ? (isSxTruthy(!domHasAttr(el, "sx-disable")) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : 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";
|
||||
var url = browserLocationHref();
|
||||
return (isSxTruthy(main) ? (function() {
|
||||
var headers = buildRequestHeaders(main, loadedComponentNames(), _cssHash);
|
||||
return fetchAndRestore(main, url, headers, scrollY);
|
||||
})());
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// engine-init
|
||||
@@ -1904,11 +1868,9 @@
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Engine (browser-only)
|
||||
// Platform interface — Engine pure logic (browser + node compatible)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserLocationHref() {
|
||||
return typeof location !== "undefined" ? location.href : "";
|
||||
}
|
||||
@@ -1932,6 +1894,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
try {
|
||||
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
||||
return JSON.parse(s);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Orchestration (browser-only)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserNavigate(url) {
|
||||
if (typeof location !== "undefined") location.assign(url);
|
||||
}
|
||||
@@ -1960,16 +1939,6 @@
|
||||
return r === null ? NIL : r;
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
try {
|
||||
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
||||
return JSON.parse(s);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function csrfToken() {
|
||||
if (!_hasDom) return NIL;
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
@@ -2670,7 +2639,7 @@
|
||||
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)"
|
||||
_version: "ref-2.0 (dom+engine+html+orchestration+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -705,14 +705,15 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
|
||||
|
||||
|
||||
ADAPTER_FILES = {
|
||||
"html": ("adapter-html.sx", "adapter-html"),
|
||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||
"dom": ("adapter-dom.sx", "adapter-dom"),
|
||||
"engine": ("engine.sx", "engine"),
|
||||
"html": ("adapter-html.sx", "adapter-html"),
|
||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||
"dom": ("adapter-dom.sx", "adapter-dom"),
|
||||
"engine": ("engine.sx", "engine"),
|
||||
"orchestration": ("orchestration.sx","orchestration"),
|
||||
}
|
||||
|
||||
# Dependencies: engine requires dom
|
||||
ADAPTER_DEPS = {"engine": ["dom"]}
|
||||
# Dependencies: orchestration requires engine+dom, engine requires dom
|
||||
ADAPTER_DEPS = {"engine": ["dom"], "orchestration": ["engine", "dom"]}
|
||||
|
||||
|
||||
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
@@ -728,8 +729,9 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
|
||||
# Platform JS blocks keyed by adapter name
|
||||
adapter_platform = {
|
||||
"dom": PLATFORM_DOM_JS,
|
||||
"engine": PLATFORM_ENGINE_JS,
|
||||
"dom": PLATFORM_DOM_JS,
|
||||
"engine": PLATFORM_ENGINE_PURE_JS,
|
||||
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
||||
}
|
||||
|
||||
# Resolve adapter set
|
||||
@@ -750,7 +752,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
("eval.sx", "eval"),
|
||||
("render.sx", "render (core)"),
|
||||
]
|
||||
for name in ("html", "sx", "dom", "engine"):
|
||||
for name in ("html", "sx", "dom", "engine", "orchestration"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
|
||||
@@ -769,6 +771,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
has_sx = "sx" in adapter_set
|
||||
has_dom = "dom" in adapter_set
|
||||
has_engine = "engine" in adapter_set
|
||||
has_orch = "orchestration" in adapter_set
|
||||
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||
|
||||
parts = []
|
||||
@@ -784,12 +787,12 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
# Platform JS for selected adapters
|
||||
if not has_dom:
|
||||
parts.append("\n var _hasDom = false;\n")
|
||||
for name in ("dom", "engine"):
|
||||
for name in ("dom", "engine", "orchestration"):
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label))
|
||||
parts.append(EPILOGUE)
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -1441,13 +1444,11 @@ PLATFORM_DOM_JS = """
|
||||
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
|
||||
"""
|
||||
|
||||
PLATFORM_ENGINE_JS = """
|
||||
PLATFORM_ENGINE_PURE_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — Engine (browser-only)
|
||||
// Platform interface — Engine pure logic (browser + node compatible)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserLocationHref() {
|
||||
return typeof location !== "undefined" ? location.href : "";
|
||||
}
|
||||
@@ -1471,6 +1472,24 @@ PLATFORM_ENGINE_JS = """
|
||||
}
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
try {
|
||||
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
||||
return JSON.parse(s);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
"""
|
||||
|
||||
PLATFORM_ORCHESTRATION_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — Orchestration (browser-only)
|
||||
// =========================================================================
|
||||
|
||||
// --- Browser/Network ---
|
||||
|
||||
function browserNavigate(url) {
|
||||
if (typeof location !== "undefined") location.assign(url);
|
||||
}
|
||||
@@ -1499,16 +1518,6 @@ PLATFORM_ENGINE_JS = """
|
||||
return r === null ? NIL : r;
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
try {
|
||||
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
||||
return JSON.parse(s);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function csrfToken() {
|
||||
if (!_hasDom) return NIL;
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
@@ -2059,7 +2068,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label):
|
||||
# Parser is always included
|
||||
parser = r'''
|
||||
// =========================================================================
|
||||
@@ -2255,6 +2264,7 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
|
||||
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
|
||||
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
|
||||
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
|
||||
if has_orch:
|
||||
api_lines.append(' process: typeof processElements === "function" ? processElements : null,')
|
||||
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
|
||||
api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,')
|
||||
@@ -2263,7 +2273,7 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
api_lines.append('')
|
||||
if has_engine:
|
||||
if has_orch:
|
||||
api_lines.append('''
|
||||
// --- Popstate listener ---
|
||||
if (typeof window !== "undefined") {
|
||||
|
||||
@@ -1,41 +1,15 @@
|
||||
;; ==========================================================================
|
||||
;; engine.sx — SxEngine specification
|
||||
;; engine.sx — SxEngine pure logic
|
||||
;;
|
||||
;; Fetch/swap/history engine for browser-side SX. Like HTMX but native
|
||||
;; to the SX rendering pipeline.
|
||||
;;
|
||||
;; This file specifies the LOGIC of the engine in s-expressions.
|
||||
;; Browser-specific APIs (fetch, DOM, history, events) are declared as
|
||||
;; platform interface at the bottom.
|
||||
;; This file specifies the pure LOGIC of the engine in s-expressions:
|
||||
;; parsing trigger specs, morph algorithm, swap dispatch, header building,
|
||||
;; retry logic, target resolution, etc.
|
||||
;;
|
||||
;; The engine processes elements with sx-* attributes:
|
||||
;; sx-get, sx-post, sx-put, sx-delete, sx-patch — HTTP verb + URL
|
||||
;; sx-trigger — when to fire (click, submit, change, every 5s, ...)
|
||||
;; sx-target — where to swap response (#selector, "this", "closest")
|
||||
;; sx-swap — how to swap (innerHTML, outerHTML, afterend, ...)
|
||||
;; sx-select — filter response (CSS selector)
|
||||
;; sx-confirm — confirmation dialog before request
|
||||
;; sx-prompt — prompt dialog, sends result as SX-Prompt header
|
||||
;; sx-validate — form validation before request
|
||||
;; sx-encoding — "json" for JSON body instead of form-encoded
|
||||
;; sx-params — filter form fields (include, exclude, none)
|
||||
;; sx-include — include extra inputs from other elements
|
||||
;; sx-vals — extra key-value pairs to send
|
||||
;; sx-headers — extra request headers
|
||||
;; sx-indicator — show/hide loading indicator
|
||||
;; sx-disabled-elt — disable elements during request
|
||||
;; sx-push-url — push to browser history
|
||||
;; sx-replace-url — replace browser history
|
||||
;; sx-sync — abort previous request ("replace")
|
||||
;; sx-media — only fire if media query matches
|
||||
;; sx-preload — preload on mousedown/mouseover
|
||||
;; sx-boost — auto-boost links and forms in container
|
||||
;; sx-sse — connect to Server-Sent Events
|
||||
;; sx-retry — retry on failure (exponential:startMs:capMs)
|
||||
;; sx-optimistic — optimistic update (remove, disable, add-class:name)
|
||||
;; sx-preserve — don't morph this element during swap
|
||||
;; sx-ignore — skip morphing entirely
|
||||
;; sx-on:* — inline event handlers (beforeRequest, afterSwap, ...)
|
||||
;; Orchestration (binding events, executing requests, processing elements)
|
||||
;; lives in orchestration.sx, which depends on this file.
|
||||
;;
|
||||
;; Depends on:
|
||||
;; adapter-dom.sx — render-to-dom (for SX response rendering)
|
||||
@@ -662,834 +636,29 @@
|
||||
(or (dom-get-attr el "sx-sse-swap") "message")))
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
;; Engine orchestration
|
||||
;;
|
||||
;; The following functions define the runtime behavior of the engine:
|
||||
;; request execution, trigger binding, post-swap lifecycle, boost, SSE,
|
||||
;; and main processing. Browser-specific mechanics (fetch, addEventListener,
|
||||
;; IntersectionObserver, EventSource, etc.) are declared as platform
|
||||
;; interface at the bottom.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Engine state
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define _preload-cache (dict))
|
||||
(define _css-hash "")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Event dispatch helpers
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dispatch-trigger-events
|
||||
(fn (el header-val)
|
||||
;; Parse and dispatch SX-Trigger header events.
|
||||
;; Value: JSON object, JSON string, or comma-separated names.
|
||||
(when header-val
|
||||
(let ((parsed (try-parse-json header-val)))
|
||||
(if (and parsed (dict? parsed))
|
||||
(for-each
|
||||
(fn (key) (dom-dispatch el key (dict-get parsed key)))
|
||||
(keys parsed))
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((n (trim name)))
|
||||
(when (not (= n ""))
|
||||
(dom-dispatch el n (dict)))))
|
||||
(split header-val ",")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; CSS tracking
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define init-css-tracking
|
||||
(fn ()
|
||||
;; Read CSS hash from <meta name="sx-css-classes">
|
||||
(let ((meta (dom-query "meta[name=\"sx-css-classes\"]")))
|
||||
(when meta
|
||||
(let ((content (dom-get-attr meta "content")))
|
||||
(when content
|
||||
(set! _css-hash content)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Request execution
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define execute-request
|
||||
(fn (el verb-info extra-params)
|
||||
;; Pre-flight gate logic: media, confirm, prompt.
|
||||
;; Returns a promise.
|
||||
(let ((current-verb (get-verb-info el))
|
||||
(verb (if current-verb current-verb verb-info))
|
||||
(method (get verb "method"))
|
||||
(url (get verb "url")))
|
||||
;; Reset retry backoff on fresh requests
|
||||
(when (not (dom-has-class? el "sx-error"))
|
||||
(dom-remove-attr el "data-sx-retry-ms"))
|
||||
;; Gate: media query
|
||||
(if (let ((media (dom-get-attr el "sx-media")))
|
||||
(and media (not (browser-media-matches? media))))
|
||||
(promise-resolve nil)
|
||||
;; Gate: confirm dialog
|
||||
(if (let ((msg (dom-get-attr el "sx-confirm")))
|
||||
(and msg (not (browser-confirm msg))))
|
||||
(promise-resolve nil)
|
||||
;; Gate: prompt dialog
|
||||
(let ((prompt-msg (dom-get-attr el "sx-prompt"))
|
||||
(params extra-params))
|
||||
(if prompt-msg
|
||||
(let ((prompt-val (browser-prompt prompt-msg)))
|
||||
(if (nil? prompt-val)
|
||||
(promise-resolve nil)
|
||||
(do
|
||||
(set! params (or params (dict)))
|
||||
(dict-set! params "promptValue" prompt-val)
|
||||
(do-fetch el verb method url params))))
|
||||
(do-fetch el verb method url params))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Fetch pipeline
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define do-fetch
|
||||
(fn (el verb method url extra-params)
|
||||
;; Build request, execute fetch, handle response.
|
||||
;; Returns a promise.
|
||||
(let ((sync-attr (dom-get-attr el "sx-sync")))
|
||||
(when (and sync-attr (contains? sync-attr "replace"))
|
||||
(abort-previous el))
|
||||
(let ((ctrl (new-abort-controller)))
|
||||
(track-controller el ctrl)
|
||||
(let ((headers (build-request-headers el
|
||||
(loaded-component-names) _css-hash)))
|
||||
;; Prompt header
|
||||
(when (and extra-params (dict-has? extra-params "promptValue"))
|
||||
(dict-set! headers "SX-Prompt"
|
||||
(get extra-params "promptValue")))
|
||||
;; CSRF for mutating same-origin
|
||||
(when (and (not (= method "GET")) (browser-same-origin? url))
|
||||
(let ((csrf (csrf-token)))
|
||||
(when csrf
|
||||
(dict-set! headers "X-CSRFToken" csrf))))
|
||||
;; Build request body
|
||||
(let ((body-info (build-request-body el method url)))
|
||||
(let ((body (get body-info "body"))
|
||||
(final-url (get body-info "url"))
|
||||
(ct (get body-info "content-type")))
|
||||
(when ct (dict-set! headers "Content-Type" ct))
|
||||
;; Lifecycle: beforeRequest
|
||||
(if (not (dom-dispatch el "sx:beforeRequest"
|
||||
(dict "method" method "url" final-url)))
|
||||
(promise-resolve nil)
|
||||
(do
|
||||
;; Loading state
|
||||
(dom-add-class el "sx-request")
|
||||
(dom-set-attr el "aria-busy" "true")
|
||||
(let ((indicator (show-indicator el))
|
||||
(disabled-elts (disable-elements el))
|
||||
(preloaded (if (= method "GET")
|
||||
(preload-cache-get _preload-cache final-url)
|
||||
nil)))
|
||||
;; Platform fetch with callbacks
|
||||
(fetch-request
|
||||
(dict "url" final-url "method" method
|
||||
"headers" headers "body" body
|
||||
"signal" (controller-signal ctrl)
|
||||
"preloaded" preloaded
|
||||
"cross-origin" (cross-origin? final-url))
|
||||
;; Success: (fn (resp-ok status get-header text) ...)
|
||||
(fn (resp-ok status get-header text)
|
||||
(do
|
||||
(clear-loading-state el indicator disabled-elts)
|
||||
(if (not resp-ok)
|
||||
(do
|
||||
(dom-dispatch el "sx:responseError"
|
||||
(dict "status" status))
|
||||
(handle-retry el verb extra-params))
|
||||
(do
|
||||
(dom-dispatch el "sx:afterRequest" (dict))
|
||||
(handle-fetch-success el final-url verb
|
||||
extra-params get-header text)))))
|
||||
;; Error: (fn (err) ...)
|
||||
(fn (err)
|
||||
(do
|
||||
(clear-loading-state el indicator disabled-elts)
|
||||
(when (not (abort-error? err))
|
||||
(do
|
||||
(dom-dispatch el "sx:sendError"
|
||||
(dict "error" err))
|
||||
(handle-retry el verb extra-params))))))))))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Response handling
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-fetch-success
|
||||
(fn (el url verb extra-params get-header text)
|
||||
;; Process a successful fetch response.
|
||||
(let ((headers (process-response-headers get-header)))
|
||||
;; Redirect — skip swap
|
||||
(if (get headers "redirect")
|
||||
(browser-navigate (get headers "redirect"))
|
||||
;; Refresh — skip swap
|
||||
(if (= (get headers "refresh") "true")
|
||||
(browser-reload)
|
||||
(do
|
||||
;; Trigger events from header
|
||||
(dispatch-trigger-events el (get headers "trigger"))
|
||||
;; Determine swap target and strategy
|
||||
(let ((raw-swap (or (dom-get-attr el "sx-swap") DEFAULT_SWAP))
|
||||
(target (resolve-target el))
|
||||
(select-sel (dom-get-attr el "sx-select")))
|
||||
;; Server overrides
|
||||
(when (get headers "retarget")
|
||||
(set! target (or (dom-query (get headers "retarget")) target)))
|
||||
(when (get headers "reswap")
|
||||
(set! raw-swap (get headers "reswap")))
|
||||
;; Parse swap spec
|
||||
(let ((swap (parse-swap-spec raw-swap false))
|
||||
(ct (or (get headers "content-type") "")))
|
||||
;; Dispatch by content type
|
||||
(if (contains? ct "text/sx")
|
||||
(handle-sx-response el target swap select-sel text)
|
||||
(handle-html-response el target swap select-sel text))
|
||||
;; SX-Location
|
||||
(when (get headers "location")
|
||||
(fetch-location (get headers "location")))
|
||||
;; History
|
||||
(handle-history el url headers)
|
||||
;; After-swap lifecycle
|
||||
(dom-dispatch el "sx:afterSwap" (dict "target" target))
|
||||
(dispatch-trigger-events el (get headers "trigger-swap"))
|
||||
(request-animation-frame
|
||||
(fn ()
|
||||
(do
|
||||
(dom-dispatch el "sx:afterSettle"
|
||||
(dict "target" target))
|
||||
(dispatch-trigger-events el
|
||||
(get headers "trigger-settle")))))))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; SX response handler
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-sx-response
|
||||
(fn (el target swap select-sel text)
|
||||
;; Process text/sx response: extract components, CSS, render, swap.
|
||||
(let ((cleaned (strip-component-scripts text))
|
||||
(cleaned2 (extract-response-css cleaned)))
|
||||
(let ((source (trim cleaned2)))
|
||||
(when (and source (not (= source "")))
|
||||
(let ((dom (sx-render source))
|
||||
(container (dom-create-element "div" nil)))
|
||||
(dom-append container dom)
|
||||
;; OOB processing on live DOM nodes
|
||||
(process-oob-swaps container
|
||||
(fn (t oob s) (swap-dom-nodes t oob s)))
|
||||
;; Select filtering
|
||||
(let ((selected (if select-sel
|
||||
(select-from-container container select-sel)
|
||||
(children-to-fragment container))))
|
||||
;; Main swap
|
||||
(when (and (not (= (get swap "style") "none")) target)
|
||||
(with-transition (get swap "transition")
|
||||
(fn ()
|
||||
(do
|
||||
(swap-dom-nodes target selected (get swap "style"))
|
||||
(hoist-head-elements target))))))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; HTML response handler
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-html-response
|
||||
(fn (el target swap select-sel text)
|
||||
;; Process HTML response: parse, scripts, OOB, swap.
|
||||
(let ((doc (dom-parse-html-document text)))
|
||||
;; Process sx scripts
|
||||
(sx-process-scripts doc)
|
||||
;; OOB processing
|
||||
(process-oob-swaps doc
|
||||
(fn (t oob s)
|
||||
(swap-html-string t (dom-outer-html oob) s)))
|
||||
;; Build content
|
||||
(let ((content (if select-sel
|
||||
(select-html-from-doc doc select-sel)
|
||||
(or (dom-body-inner-html doc) text))))
|
||||
;; Main swap
|
||||
(when (and (not (= (get swap "style") "none")) target)
|
||||
(with-transition (get swap "transition")
|
||||
(fn ()
|
||||
(do
|
||||
(swap-html-string target content (get swap "style"))
|
||||
(hoist-head-elements target)))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Retry handling
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-retry
|
||||
(fn (el verb-info extra-params)
|
||||
;; Retry failed request with exponential backoff.
|
||||
(let ((retry-attr (dom-get-attr el "sx-retry")))
|
||||
(when retry-attr
|
||||
(let ((spec (parse-retry-spec retry-attr))
|
||||
(current-ms (or (parse-int
|
||||
(dom-get-attr el "data-sx-retry-ms") 0)
|
||||
(get spec "start-ms"))))
|
||||
(dom-add-class el "sx-error")
|
||||
(dom-remove-class el "sx-loading")
|
||||
(set-timeout
|
||||
(fn ()
|
||||
(do
|
||||
(dom-remove-class el "sx-error")
|
||||
(dom-add-class el "sx-loading")
|
||||
(dom-set-attr el "data-sx-retry-ms"
|
||||
(str (next-retry-ms current-ms (get spec "cap-ms"))))
|
||||
(execute-request el verb-info extra-params)))
|
||||
current-ms))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Trigger binding
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-triggers
|
||||
(fn (el verb-info)
|
||||
;; Parse triggers and bind event handlers.
|
||||
(let ((trigger-spec (dom-get-attr el "sx-trigger"))
|
||||
(triggers (if trigger-spec
|
||||
(parse-trigger-spec trigger-spec)
|
||||
(default-trigger (dom-tag-name el)))))
|
||||
(for-each
|
||||
(fn (trig)
|
||||
(let ((kind (classify-trigger trig)))
|
||||
(cond
|
||||
(= kind "poll")
|
||||
(set-interval
|
||||
(fn () (execute-request el verb-info nil))
|
||||
(or (get (get trig "modifiers") "interval") 1000))
|
||||
(= kind "intersect")
|
||||
(observe-intersection el
|
||||
(fn () (execute-request el verb-info nil))
|
||||
(get (get trig "modifiers") "once")
|
||||
(get (get trig "modifiers") "delay"))
|
||||
(= kind "load")
|
||||
(set-timeout
|
||||
(fn () (execute-request el verb-info nil)) 0)
|
||||
(= kind "revealed")
|
||||
(observe-intersection el
|
||||
(fn () (execute-request el verb-info nil))
|
||||
true nil)
|
||||
:else
|
||||
(bind-event el verb-info trig))))
|
||||
triggers))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Event binding with modifiers
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-event
|
||||
(fn (el verb-info trig)
|
||||
;; Bind a single event with modifiers (once, delay, changed, from).
|
||||
(let ((event-name (get trig "event"))
|
||||
(mods (get trig "modifiers"))
|
||||
(listen-target (if (get mods "from")
|
||||
(or (dom-query (get mods "from")) el)
|
||||
el))
|
||||
(timer nil)
|
||||
(last-val nil))
|
||||
(dom-add-listener listen-target event-name
|
||||
(fn (e)
|
||||
(do
|
||||
;; Prevent defaults
|
||||
(when (= event-name "submit") (prevent-default e))
|
||||
(when (and (= event-name "click") (= (dom-tag-name el) "A"))
|
||||
(prevent-default e))
|
||||
;; Validation gate
|
||||
(if (not (validate-for-request el))
|
||||
(dom-dispatch el "sx:validationFailed" (dict))
|
||||
;; Changed modifier gate
|
||||
(if (and (get mods "changed")
|
||||
(not (nil? (element-value el)))
|
||||
(= (element-value el) last-val))
|
||||
nil
|
||||
(do
|
||||
(when (get mods "changed")
|
||||
(set! last-val (element-value el)))
|
||||
;; Apply optimistic update
|
||||
(let ((opt-state (apply-optimistic el))
|
||||
(exec-fn
|
||||
(fn ()
|
||||
(let ((p (execute-request el verb-info nil)))
|
||||
(when (and opt-state p)
|
||||
(promise-catch p
|
||||
(fn (_) (revert-optimistic opt-state))))))))
|
||||
;; Delay modifier
|
||||
(if (get mods "delay")
|
||||
(do
|
||||
(clear-timeout timer)
|
||||
(set! timer
|
||||
(set-timeout exec-fn (get mods "delay"))))
|
||||
(exec-fn))))))))
|
||||
(dict "once" (get mods "once"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Post-swap lifecycle
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define post-swap
|
||||
(fn (root)
|
||||
;; Post-swap: activate scripts, load components, hydrate, bind engine.
|
||||
(do
|
||||
(activate-scripts root)
|
||||
(sx-process-scripts root)
|
||||
(sx-hydrate root)
|
||||
(process-elements root))))
|
||||
|
||||
(define activate-scripts
|
||||
(fn (root)
|
||||
;; Scripts inserted via innerHTML don't execute.
|
||||
;; Replace dead scripts with live clones so the browser runs them.
|
||||
(let ((dead (dom-query-all root
|
||||
"script:not([type]), script[type='text/javascript']")))
|
||||
(for-each
|
||||
(fn (d)
|
||||
(let ((live (create-script-clone d)))
|
||||
(dom-replace-child (dom-parent d) live d)))
|
||||
dead))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Out-of-band swap processing (orchestration variant)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-oob-swaps
|
||||
(fn (container swap-fn)
|
||||
;; Find elements with sx-swap-oob/hx-swap-oob and swap to targets.
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((oob-els (dom-query-all container (str "[" attr "]"))))
|
||||
(for-each
|
||||
(fn (oob)
|
||||
(let ((swap-type (or (dom-get-attr oob attr) "outerHTML"))
|
||||
(target-id (dom-id oob)))
|
||||
(dom-remove-attr oob attr)
|
||||
(when (dom-parent oob)
|
||||
(dom-remove-child (dom-parent oob) oob))
|
||||
(when target-id
|
||||
(let ((target (dom-query-by-id target-id)))
|
||||
(when target
|
||||
(swap-fn target oob swap-type))))))
|
||||
oob-els)))
|
||||
(list "sx-swap-oob" "hx-swap-oob"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Head element hoisting
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define hoist-head-elements
|
||||
(fn (root)
|
||||
;; Move <style> and <link rel=stylesheet> from swapped content to <head>.
|
||||
(let ((styles (dom-query-all root "style[data-sx-css]"))
|
||||
(links (dom-query-all root "link[rel='stylesheet']")))
|
||||
(for-each
|
||||
(fn (el)
|
||||
(when (dom-parent el)
|
||||
(dom-remove-child (dom-parent el) el))
|
||||
(dom-append-to-head el))
|
||||
styles)
|
||||
(for-each
|
||||
(fn (el)
|
||||
(when (dom-parent el)
|
||||
(dom-remove-child (dom-parent el) el))
|
||||
(dom-append-to-head el))
|
||||
links))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Boost processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-boosted
|
||||
(fn (root)
|
||||
;; Find sx-boost containers and boost their links/forms.
|
||||
(let ((containers (dom-query-all root "[sx-boost]")))
|
||||
(when (dom-matches? root "[sx-boost]")
|
||||
(boost-descendants root))
|
||||
(for-each boost-descendants containers))))
|
||||
|
||||
(define boost-descendants
|
||||
(fn (container)
|
||||
;; Boost links and forms inside a container.
|
||||
(do
|
||||
;; Boost links
|
||||
(let ((links (dom-query-all container "a[href]")))
|
||||
(for-each
|
||||
(fn (link)
|
||||
(when (and (not (is-processed? link "boost"))
|
||||
(should-boost-link? link))
|
||||
(mark-processed! link "boost")
|
||||
(bind-boost-link link (dom-get-attr link "href"))
|
||||
;; Default attrs for boosted links
|
||||
(when (not (dom-has-attr? link "sx-target"))
|
||||
(dom-set-attr link "sx-target" "#main-panel"))
|
||||
(when (not (dom-has-attr? link "sx-swap"))
|
||||
(dom-set-attr link "sx-swap" "innerHTML"))
|
||||
(when (not (dom-has-attr? link "sx-select"))
|
||||
(dom-set-attr link "sx-select" "#main-panel"))))
|
||||
links))
|
||||
;; Boost forms
|
||||
(let ((forms (dom-query-all container "form")))
|
||||
(for-each
|
||||
(fn (form)
|
||||
(when (and (not (is-processed? form "boost"))
|
||||
(should-boost-form? form))
|
||||
(mark-processed! form "boost")
|
||||
(bind-boost-form form
|
||||
(or (upper (dom-get-attr form "method")) "GET")
|
||||
(or (dom-get-attr form "action")
|
||||
(browser-location-href)))
|
||||
(when (not (dom-has-attr? form "sx-target"))
|
||||
(dom-set-attr form "sx-target" "#main-panel"))
|
||||
(when (not (dom-has-attr? form "sx-swap"))
|
||||
(dom-set-attr form "sx-swap" "innerHTML"))))
|
||||
forms)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; SSE (Server-Sent Events)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-sse
|
||||
(fn (root)
|
||||
;; Find elements with sx-sse and bind EventSource connections.
|
||||
(let ((sse-els (dom-query-all root "[sx-sse]")))
|
||||
(when (dom-matches? root "[sx-sse]")
|
||||
(bind-sse root))
|
||||
(for-each bind-sse sse-els))))
|
||||
|
||||
(define bind-sse
|
||||
(fn (el)
|
||||
;; Connect to EventSource and bind swap handlers.
|
||||
(when (not (is-processed? el "sse"))
|
||||
(mark-processed! el "sse")
|
||||
(let ((url (dom-get-attr el "sx-sse")))
|
||||
(when url
|
||||
(let ((source (event-source-connect url el)))
|
||||
(let ((swap-els (dom-query-all el "[sx-sse-swap]")))
|
||||
(when (dom-has-attr? el "sx-sse-swap")
|
||||
(bind-sse-swap el source))
|
||||
(for-each
|
||||
(fn (child) (bind-sse-swap child source))
|
||||
swap-els))))))))
|
||||
|
||||
(define bind-sse-swap
|
||||
(fn (el source)
|
||||
;; Bind SSE event handler for swap.
|
||||
(let ((event-name (parse-sse-swap el)))
|
||||
(event-source-listen source event-name
|
||||
(fn (data)
|
||||
(let ((target (or (resolve-target el) el))
|
||||
(swap-style (or (dom-get-attr el "sx-swap") "innerHTML")))
|
||||
(if (starts-with? (trim data) "(")
|
||||
;; SX response — render to DOM
|
||||
(let ((dom (sx-render data)))
|
||||
(swap-dom-nodes target dom swap-style))
|
||||
;; HTML response
|
||||
(swap-html-string target data swap-style))
|
||||
(post-swap target)
|
||||
(dom-dispatch el "sx:sseMessage"
|
||||
(dict "data" data "event" event-name))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Inline event handlers
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-inline-handlers
|
||||
(fn (el)
|
||||
;; Bind sx-on:* inline event handlers.
|
||||
(when (not (is-processed? el "on"))
|
||||
(mark-processed! el "on")
|
||||
(let ((attrs (dom-attr-list el)))
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((name (first attr))
|
||||
(val (nth attr 1)))
|
||||
(when (starts-with? name "sx-on:")
|
||||
(bind-inline-handler el (slice name 6) val))))
|
||||
attrs)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Preload
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-preload-for
|
||||
(fn (el)
|
||||
;; Set up preload listeners on the element.
|
||||
(when (dom-has-attr? el "sx-preload")
|
||||
(let ((mode (or (dom-get-attr el "sx-preload") "mousedown"))
|
||||
(events (if (= mode "mouseover")
|
||||
(list "mouseenter" "focusin")
|
||||
(list "mousedown" "focusin")))
|
||||
(debounce-ms (if (= mode "mouseover") 100 0)))
|
||||
(bind-preload el events debounce-ms
|
||||
(fn ()
|
||||
(let ((verb (get-verb-info el)))
|
||||
(when verb
|
||||
(let ((url (get verb "url")))
|
||||
(when (nil? (preload-cache-get _preload-cache url))
|
||||
(do-preload url)))))))))))
|
||||
|
||||
(define do-preload
|
||||
(fn (url)
|
||||
;; Preload a URL into the cache.
|
||||
(let ((headers (build-request-headers nil
|
||||
(loaded-component-names) _css-hash)))
|
||||
(fetch-preload url headers _preload-cache))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Main processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define VERB_SELECTOR
|
||||
"[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]")
|
||||
|
||||
(define process-elements
|
||||
(fn (root)
|
||||
;; Main engine processing: find and bind all sx-* elements.
|
||||
(let ((root (or root (dom-body))))
|
||||
(when root
|
||||
;; Process root itself
|
||||
(when (dom-matches? root VERB_SELECTOR)
|
||||
(process-one root))
|
||||
;; Process descendants
|
||||
(let ((elements (dom-query-all root VERB_SELECTOR)))
|
||||
(for-each process-one elements))
|
||||
;; Boost, SSE, inline handlers
|
||||
(process-boosted root)
|
||||
(process-sse root)
|
||||
(let ((on-els (dom-query-all root
|
||||
"[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:responseError]")))
|
||||
(for-each bind-inline-handlers on-els))))))
|
||||
|
||||
(define process-one
|
||||
(fn (el)
|
||||
;; Process a single element: bind triggers and preload.
|
||||
(when (not (is-processed? el "bound"))
|
||||
;; Skip disabled elements
|
||||
(when (not (or (dom-has-attr? el "sx-disable")
|
||||
(dom-closest el "[sx-disable]")))
|
||||
(mark-processed! el "bound")
|
||||
(let ((verb-info (get-verb-info el)))
|
||||
(when verb-info
|
||||
(bind-triggers el verb-info)
|
||||
(bind-preload-for el)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; History: popstate handling
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-popstate
|
||||
(fn (scroll-y)
|
||||
;; Handle browser back/forward navigation.
|
||||
(let ((url (browser-location-href))
|
||||
(main (dom-query-by-id "main-panel")))
|
||||
(if (not main)
|
||||
(browser-reload)
|
||||
(let ((headers (build-request-headers nil
|
||||
(loaded-component-names) _css-hash)))
|
||||
(dict-set! headers "SX-History-Restore" "true")
|
||||
(fetch-and-restore main url headers scroll-y))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Engine initialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define engine-init
|
||||
(fn ()
|
||||
;; Initialize: CSS tracking, scripts, hydrate, process.
|
||||
(do
|
||||
(init-css-tracking)
|
||||
(sx-process-scripts nil)
|
||||
(sx-hydrate nil)
|
||||
(process-elements nil))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — Engine
|
||||
;; Platform interface — Engine (pure logic)
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; === Browser/Network ===
|
||||
;; From adapter-dom.sx:
|
||||
;; dom-get-attr, dom-set-attr, dom-remove-attr, dom-has-attr?, dom-attr-list
|
||||
;; dom-query, dom-query-all, dom-id, dom-parent, dom-first-child,
|
||||
;; dom-next-sibling, dom-child-list, dom-node-type, dom-node-name,
|
||||
;; dom-text-content, dom-set-text-content, dom-is-fragment?,
|
||||
;; dom-is-child-of?, dom-is-active-element?, dom-is-input-element?,
|
||||
;; dom-create-element, dom-append, dom-prepend, dom-insert-before,
|
||||
;; dom-insert-after, dom-remove-child, dom-replace-child, dom-clone,
|
||||
;; dom-get-style, dom-set-style, dom-get-prop, dom-set-prop,
|
||||
;; dom-add-class, dom-remove-class, dom-set-inner-html,
|
||||
;; dom-insert-adjacent-html
|
||||
;;
|
||||
;; Browser/Network:
|
||||
;; (browser-location-href) → current URL string
|
||||
;; (browser-same-origin? url) → boolean
|
||||
;; (browser-push-state url) → void (history.pushState)
|
||||
;; (browser-replace-state url) → void (history.replaceState)
|
||||
;; (browser-navigate url) → void (location.assign)
|
||||
;; (browser-reload) → void (location.reload)
|
||||
;; (browser-scroll-to x y) → void
|
||||
;; (browser-media-matches? query) → boolean (matchMedia)
|
||||
;; (browser-confirm msg) → boolean
|
||||
;; (browser-prompt msg) → string or nil
|
||||
;;
|
||||
;; Parsing:
|
||||
;; (parse-header-value s) → parsed dict from header string
|
||||
;; (now-ms) → current timestamp in milliseconds
|
||||
;; (csrf-token) → string from meta[name=csrf-token]
|
||||
;; (cross-origin? url) → boolean (needs credentials:include)
|
||||
;;
|
||||
;; === Promises ===
|
||||
;; (promise-resolve val) → resolved Promise
|
||||
;; (promise-catch p fn) → p.catch(fn)
|
||||
;;
|
||||
;; === Abort controllers ===
|
||||
;; (abort-previous el) → abort + remove controller for element
|
||||
;; (track-controller el ctrl) → store controller for element
|
||||
;; (new-abort-controller) → new AbortController()
|
||||
;; (controller-signal ctrl) → ctrl.signal
|
||||
;; (abort-error? err) → boolean (err.name === "AbortError")
|
||||
;;
|
||||
;; === Timers ===
|
||||
;; (set-timeout fn ms) → timer id
|
||||
;; (set-interval fn ms) → timer id
|
||||
;; (clear-timeout id) → void
|
||||
;; (request-animation-frame fn) → void
|
||||
;;
|
||||
;; === Fetch ===
|
||||
;; (fetch-request config success-fn error-fn) → Promise
|
||||
;; config: dict with url, method, headers, body, signal, preloaded,
|
||||
;; cross-origin
|
||||
;; success-fn: (fn (resp-ok status get-header text) ...)
|
||||
;; error-fn: (fn (err) ...)
|
||||
;; (fetch-location url) → fetch URL and swap to #main-panel
|
||||
;; (fetch-and-restore main url headers scroll-y) → popstate fetch+swap
|
||||
;; (fetch-preload url headers cache) → preload into cache
|
||||
;;
|
||||
;; === Request body ===
|
||||
;; (build-request-body el method url) → dict with body, url, content-type
|
||||
;; Handles FormData, JSON encoding, sx-params, sx-include, sx-vals
|
||||
;;
|
||||
;; === Loading state ===
|
||||
;; (show-indicator el) → indicator state (or nil)
|
||||
;; (disable-elements el) → list of disabled elements
|
||||
;; (clear-loading-state el indicator-state disabled-elts) → void
|
||||
;;
|
||||
;; === DOM query (extended) ===
|
||||
;; (dom-query sel) → Element or nil
|
||||
;; (dom-query-all root sel) → list of Elements
|
||||
;; (dom-query-by-id id) → Element or nil
|
||||
;; (dom-id el) → string id or nil
|
||||
;; (dom-parent el) → parent Element
|
||||
;; (dom-first-child el) → first child node
|
||||
;; (dom-next-sibling el) → next sibling node
|
||||
;; (dom-child-list el) → list of child nodes
|
||||
;; (dom-tag-name el) → uppercase tag name
|
||||
;; (dom-matches? el sel) → boolean
|
||||
;; (dom-closest el sel) → Element or nil
|
||||
;; (dom-body) → document.body
|
||||
;;
|
||||
;; === DOM mutation ===
|
||||
;; (dom-create-element tag ns) → Element
|
||||
;; (dom-append parent child) → void
|
||||
;; (dom-prepend parent child) → void
|
||||
;; (dom-insert-before parent node ref) → void
|
||||
;; (dom-insert-after ref node) → void
|
||||
;; (dom-remove-child parent child) → void
|
||||
;; (dom-replace-child parent new old) → void
|
||||
;; (dom-clone node) → deep clone
|
||||
;; (dom-append-to-head el) → void
|
||||
;;
|
||||
;; === DOM attributes ===
|
||||
;; (dom-get-attr el name) → string or nil
|
||||
;; (dom-set-attr el name val) → void
|
||||
;; (dom-remove-attr el name) → void
|
||||
;; (dom-has-attr? el name) → boolean
|
||||
;; (dom-attr-list el) → list of (name value) pairs
|
||||
;;
|
||||
;; === DOM properties/style ===
|
||||
;; (dom-get-prop el name) → value
|
||||
;; (dom-set-prop el name val) → void
|
||||
;; (dom-get-style el prop) → string
|
||||
;; (dom-set-style el prop val) → void
|
||||
;; (dom-add-class el cls) → void
|
||||
;; (dom-remove-class el cls) → void
|
||||
;; (dom-has-class? el cls) → boolean
|
||||
;;
|
||||
;; === DOM inspection ===
|
||||
;; (dom-node-type el) → number
|
||||
;; (dom-node-name el) → string
|
||||
;; (dom-text-content el) → string
|
||||
;; (dom-set-text-content el s) → void
|
||||
;; (dom-is-fragment? el) → boolean
|
||||
;; (dom-is-child-of? child parent) → boolean
|
||||
;; (dom-is-active-element? el) → boolean
|
||||
;; (dom-is-input-element? el) → boolean
|
||||
;;
|
||||
;; === DOM content ===
|
||||
;; (dom-set-inner-html el html) → void
|
||||
;; (dom-insert-adjacent-html el pos html) → void
|
||||
;; (dom-parse-html-document text) → parsed document (DOMParser)
|
||||
;; (dom-outer-html el) → string
|
||||
;; (dom-body-inner-html doc) → string
|
||||
;;
|
||||
;; === Events ===
|
||||
;; (dom-dispatch el name detail) → boolean (dispatchEvent)
|
||||
;; (dom-add-listener el event fn opts) → void
|
||||
;; (prevent-default e) → void
|
||||
;; (element-value el) → el.value or nil
|
||||
;;
|
||||
;; === Event binding (platform-level) ===
|
||||
;; (bind-boost-link el href) → void (click handler + pushState)
|
||||
;; (bind-boost-form form method action) → void (submit handler)
|
||||
;; (bind-inline-handler el event-name body) → void (new Function)
|
||||
;; (bind-preload el events debounce-ms fn) → void (preload listeners)
|
||||
;; (observe-intersection el fn once? delay) → void (IntersectionObserver)
|
||||
;; (event-source-connect url el) → EventSource (with cleanup)
|
||||
;; (event-source-listen source event fn) → void
|
||||
;; (validate-for-request el) → boolean (form validation)
|
||||
;;
|
||||
;; === Processing markers ===
|
||||
;; (mark-processed! el key) → void
|
||||
;; (is-processed? el key) → boolean
|
||||
;;
|
||||
;; === Script handling ===
|
||||
;; (create-script-clone script) → live script Element
|
||||
;;
|
||||
;; === SX API (references to Sx object) ===
|
||||
;; (sx-render source) → DOM nodes (Sx.render)
|
||||
;; (sx-process-scripts root) → void (Sx.processScripts)
|
||||
;; (sx-hydrate root) → void (Sx.hydrate)
|
||||
;; (loaded-component-names) → list of ~name strings
|
||||
;;
|
||||
;; === Response processing ===
|
||||
;; (strip-component-scripts text) → cleaned text (regex strip + load)
|
||||
;; (extract-response-css text) → cleaned text (regex strip + inject)
|
||||
;; (select-from-container el sel) → DocumentFragment
|
||||
;; (children-to-fragment el) → DocumentFragment
|
||||
;; (select-html-from-doc doc sel) → HTML string
|
||||
;; (with-transition enabled fn) → void (View Transition API)
|
||||
;;
|
||||
;; === Parsing ===
|
||||
;; (parse-header-value s) → dict
|
||||
;; (try-parse-json s) → parsed value or nil
|
||||
;;
|
||||
;; === Misc ===
|
||||
;; (dict-has? d key) → boolean
|
||||
;; (dict-delete! d key) → void
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
815
shared/sx/ref/orchestration.sx
Normal file
815
shared/sx/ref/orchestration.sx
Normal file
@@ -0,0 +1,815 @@
|
||||
;; ==========================================================================
|
||||
;; orchestration.sx — Engine orchestration (browser wiring)
|
||||
;;
|
||||
;; Binds the pure engine logic to actual browser events, fetch, DOM
|
||||
;; processing, and lifecycle management. This is the runtime that makes
|
||||
;; the engine go.
|
||||
;;
|
||||
;; Dependency is one-way: orchestration → engine, never reverse.
|
||||
;;
|
||||
;; Depends on:
|
||||
;; engine.sx — parse-trigger-spec, get-verb-info, build-request-headers,
|
||||
;; process-response-headers, parse-swap-spec, parse-retry-spec,
|
||||
;; next-retry-ms, resolve-target, apply-optimistic,
|
||||
;; revert-optimistic, find-oob-swaps, swap-dom-nodes,
|
||||
;; swap-html-string, morph-children, handle-history,
|
||||
;; preload-cache-get, preload-cache-set, classify-trigger,
|
||||
;; should-boost-link?, should-boost-form?, parse-sse-swap,
|
||||
;; default-trigger, filter-params, PRELOAD_TTL
|
||||
;; adapter-dom.sx — render-to-dom
|
||||
;; render.sx — shared registries
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Engine state
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define _preload-cache (dict))
|
||||
(define _css-hash "")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Event dispatch helpers
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dispatch-trigger-events
|
||||
(fn (el header-val)
|
||||
;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers.
|
||||
;; Value can be JSON object (name → detail) or comma-separated names.
|
||||
(when header-val
|
||||
(let ((parsed (try-parse-json header-val)))
|
||||
(if parsed
|
||||
;; JSON object: keys are event names, values are detail
|
||||
(for-each
|
||||
(fn (key)
|
||||
(dom-dispatch el key (get parsed key)))
|
||||
(keys parsed))
|
||||
;; Comma-separated event names
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((trimmed (trim name)))
|
||||
(when (not (empty? trimmed))
|
||||
(dom-dispatch el trimmed (dict)))))
|
||||
(split header-val ",")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; CSS tracking
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define init-css-tracking
|
||||
(fn ()
|
||||
;; Read initial CSS hash from meta tag
|
||||
(let ((meta (dom-query "meta[name=\"sx-css-hash\"]")))
|
||||
(when meta
|
||||
(let ((content (dom-get-attr meta "content")))
|
||||
(when content
|
||||
(set! _css-hash content)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Request execution
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define execute-request
|
||||
(fn (el verbInfo extraParams)
|
||||
;; Gate checks then delegate to do-fetch.
|
||||
;; verbInfo: dict with "method" and "url" (or nil to read from element).
|
||||
;; Returns a promise.
|
||||
(let ((info (or verbInfo (get-verb-info el))))
|
||||
(if (nil? info)
|
||||
(promise-resolve nil)
|
||||
(let ((verb (get info "method"))
|
||||
(url (get info "url")))
|
||||
;; Media query gate
|
||||
(if (let ((media (dom-get-attr el "sx-media")))
|
||||
(and media (not (browser-media-matches? media))))
|
||||
(promise-resolve nil)
|
||||
;; Confirm gate
|
||||
(if (let ((confirm-msg (dom-get-attr el "sx-confirm")))
|
||||
(and confirm-msg (not (browser-confirm confirm-msg))))
|
||||
(promise-resolve nil)
|
||||
;; Prompt
|
||||
(let ((prompt-msg (dom-get-attr el "sx-prompt"))
|
||||
(prompt-val (if prompt-msg (browser-prompt prompt-msg) nil)))
|
||||
(if (and prompt-msg (nil? prompt-val))
|
||||
(promise-resolve nil)
|
||||
;; Validation gate
|
||||
(if (not (validate-for-request el))
|
||||
(promise-resolve nil)
|
||||
(do-fetch el verb verb url
|
||||
(if prompt-val
|
||||
(assoc (or extraParams (dict)) "SX-Prompt" prompt-val)
|
||||
extraParams))))))))))))
|
||||
|
||||
|
||||
(define do-fetch
|
||||
(fn (el verb method url extraParams)
|
||||
;; Execute the actual fetch. Manages abort, headers, body, loading state.
|
||||
(let ((sync (dom-get-attr el "sx-sync")))
|
||||
;; Abort previous if sync mode
|
||||
(when (= sync "replace")
|
||||
(abort-previous el))
|
||||
|
||||
(let ((ctrl (new-abort-controller)))
|
||||
(track-controller el ctrl)
|
||||
|
||||
;; Build request
|
||||
(let ((body-info (build-request-body el method url))
|
||||
(final-url (get body-info "url"))
|
||||
(body (get body-info "body"))
|
||||
(ct (get body-info "content-type"))
|
||||
(headers (build-request-headers el
|
||||
(loaded-component-names) _css-hash))
|
||||
(csrf (csrf-token)))
|
||||
|
||||
;; Merge extra params as headers
|
||||
(when extraParams
|
||||
(for-each
|
||||
(fn (k) (dict-set! headers k (get extraParams k)))
|
||||
(keys extraParams)))
|
||||
|
||||
;; Content-Type
|
||||
(when ct
|
||||
(dict-set! headers "Content-Type" ct))
|
||||
|
||||
;; CSRF
|
||||
(when csrf
|
||||
(dict-set! headers "X-CSRFToken" csrf))
|
||||
|
||||
;; Preload cache check
|
||||
(let ((cached (preload-cache-get _preload-cache final-url))
|
||||
(optimistic-state (apply-optimistic el))
|
||||
(indicator (show-indicator el))
|
||||
(disabled-elts (disable-elements el)))
|
||||
|
||||
;; Loading indicators
|
||||
(dom-add-class el "sx-request")
|
||||
(dom-set-attr el "aria-busy" "true")
|
||||
(dom-dispatch el "sx:beforeRequest" (dict "url" final-url "method" method))
|
||||
|
||||
;; Fetch
|
||||
(fetch-request
|
||||
(dict "url" final-url
|
||||
"method" method
|
||||
"headers" headers
|
||||
"body" body
|
||||
"signal" (controller-signal ctrl)
|
||||
"cross-origin" (cross-origin? final-url)
|
||||
"preloaded" cached)
|
||||
;; Success callback
|
||||
(fn (resp-ok status get-header text)
|
||||
(do
|
||||
(clear-loading-state el indicator disabled-elts)
|
||||
(revert-optimistic optimistic-state)
|
||||
(if (not resp-ok)
|
||||
(do
|
||||
(dom-dispatch el "sx:responseError"
|
||||
(dict "status" status "text" text))
|
||||
(handle-retry el verb method final-url extraParams))
|
||||
(do
|
||||
(dom-dispatch el "sx:afterRequest"
|
||||
(dict "status" status))
|
||||
(handle-fetch-success el final-url verb extraParams
|
||||
get-header text)))))
|
||||
;; Error callback
|
||||
(fn (err)
|
||||
(do
|
||||
(clear-loading-state el indicator disabled-elts)
|
||||
(revert-optimistic optimistic-state)
|
||||
(when (not (abort-error? err))
|
||||
(dom-dispatch el "sx:requestError"
|
||||
(dict "error" err))))))))))))
|
||||
|
||||
|
||||
(define handle-fetch-success
|
||||
(fn (el url verb extraParams get-header text)
|
||||
;; Route a successful response through the appropriate handler.
|
||||
(let ((resp-headers (process-response-headers get-header)))
|
||||
;; CSS hash update
|
||||
(let ((new-hash (get resp-headers "css-hash")))
|
||||
(when new-hash (set! _css-hash new-hash)))
|
||||
|
||||
;; Triggers (before swap)
|
||||
(dispatch-trigger-events el (get resp-headers "trigger"))
|
||||
|
||||
(cond
|
||||
;; Redirect
|
||||
(get resp-headers "redirect")
|
||||
(browser-navigate (get resp-headers "redirect"))
|
||||
|
||||
;; Refresh
|
||||
(get resp-headers "refresh")
|
||||
(browser-reload)
|
||||
|
||||
;; Location (SX-Location header)
|
||||
(get resp-headers "location")
|
||||
(fetch-location (get resp-headers "location"))
|
||||
|
||||
;; Normal response — route by content type
|
||||
:else
|
||||
(let ((target-el (if (get resp-headers "retarget")
|
||||
(dom-query (get resp-headers "retarget"))
|
||||
(resolve-target el)))
|
||||
(swap-spec (parse-swap-spec
|
||||
(or (get resp-headers "reswap")
|
||||
(dom-get-attr el "sx-swap"))
|
||||
(dom-has-class? (dom-body) "sx-transitions")))
|
||||
(swap-style (get swap-spec "style"))
|
||||
(use-transition (get swap-spec "transition"))
|
||||
(ct (or (get resp-headers "content-type") "")))
|
||||
|
||||
;; Dispatch by content type
|
||||
(if (contains? ct "text/sx")
|
||||
(handle-sx-response el target-el text swap-style use-transition)
|
||||
(handle-html-response el target-el text swap-style use-transition))
|
||||
|
||||
;; Post-swap triggers
|
||||
(dispatch-trigger-events el (get resp-headers "trigger-swap"))
|
||||
|
||||
;; History
|
||||
(handle-history el url resp-headers)
|
||||
|
||||
;; Settle triggers (after small delay)
|
||||
(when (get resp-headers "trigger-settle")
|
||||
(set-timeout
|
||||
(fn () (dispatch-trigger-events el
|
||||
(get resp-headers "trigger-settle")))
|
||||
20))
|
||||
|
||||
;; Lifecycle event
|
||||
(dom-dispatch el "sx:afterSwap"
|
||||
(dict "target" target-el "swap" swap-style)))))))
|
||||
|
||||
|
||||
(define handle-sx-response
|
||||
(fn (el target text swap-style use-transition)
|
||||
;; Handle SX-format response: strip components, extract CSS, render, swap.
|
||||
(let ((cleaned (strip-component-scripts text)))
|
||||
(let ((final (extract-response-css cleaned)))
|
||||
(let ((trimmed (trim final)))
|
||||
(when (not (empty? trimmed))
|
||||
(let ((rendered (sx-render trimmed))
|
||||
(container (dom-create-element "div" nil)))
|
||||
(dom-append container rendered)
|
||||
;; Process OOB swaps
|
||||
(process-oob-swaps container
|
||||
(fn (t oob s)
|
||||
(swap-dom-nodes t oob s)
|
||||
(sx-hydrate t)
|
||||
(process-elements t)))
|
||||
;; Select if specified
|
||||
(let ((select-sel (dom-get-attr el "sx-select"))
|
||||
(content (if select-sel
|
||||
(select-from-container container select-sel)
|
||||
(children-to-fragment container))))
|
||||
;; Swap
|
||||
(with-transition use-transition
|
||||
(fn ()
|
||||
(swap-dom-nodes target content swap-style)
|
||||
(post-swap target)))))))))))
|
||||
|
||||
|
||||
(define handle-html-response
|
||||
(fn (el target text swap-style use-transition)
|
||||
;; Handle HTML-format response: parse, OOB, select, swap.
|
||||
(let ((doc (dom-parse-html-document text)))
|
||||
(when doc
|
||||
(let ((select-sel (dom-get-attr el "sx-select")))
|
||||
(if select-sel
|
||||
;; Select from parsed document
|
||||
(let ((html (select-html-from-doc doc select-sel)))
|
||||
(with-transition use-transition
|
||||
(fn ()
|
||||
(swap-html-string target html swap-style)
|
||||
(post-swap target))))
|
||||
;; Full body content
|
||||
(let ((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
;; Process OOB swaps
|
||||
(process-oob-swaps container
|
||||
(fn (t oob s)
|
||||
(swap-dom-nodes t oob s)
|
||||
(post-swap t)))
|
||||
;; Hoist head elements
|
||||
(hoist-head-elements container)
|
||||
;; Swap remaining content
|
||||
(with-transition use-transition
|
||||
(fn ()
|
||||
(swap-dom-nodes target (children-to-fragment container) swap-style)
|
||||
(post-swap target))))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Retry
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-retry
|
||||
(fn (el verb method url extraParams)
|
||||
;; Handle retry on failure if sx-retry is configured
|
||||
(let ((retry-attr (dom-get-attr el "sx-retry"))
|
||||
(spec (parse-retry-spec retry-attr)))
|
||||
(when spec
|
||||
(let ((current-ms (or (dom-get-attr el "data-sx-retry-ms")
|
||||
(get spec "start-ms"))))
|
||||
(let ((ms (parse-int current-ms (get spec "start-ms"))))
|
||||
(dom-set-attr el "data-sx-retry-ms"
|
||||
(str (next-retry-ms ms (get spec "cap-ms"))))
|
||||
(set-timeout
|
||||
(fn () (do-fetch el verb method url extraParams))
|
||||
ms)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Trigger binding
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-triggers
|
||||
(fn (el verbInfo)
|
||||
;; Bind triggers from sx-trigger attribute (or defaults)
|
||||
(let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger"))
|
||||
(default-trigger (dom-tag-name el)))))
|
||||
(for-each
|
||||
(fn (trigger)
|
||||
(let ((kind (classify-trigger trigger))
|
||||
(mods (get trigger "modifiers")))
|
||||
(cond
|
||||
(= kind "poll")
|
||||
(set-interval
|
||||
(fn () (execute-request el nil nil))
|
||||
(get mods "interval"))
|
||||
|
||||
(= kind "intersect")
|
||||
(observe-intersection el
|
||||
(fn () (execute-request el nil nil))
|
||||
false (get mods "delay"))
|
||||
|
||||
(= kind "load")
|
||||
(set-timeout
|
||||
(fn () (execute-request el nil nil))
|
||||
(or (get mods "delay") 0))
|
||||
|
||||
(= kind "revealed")
|
||||
(observe-intersection el
|
||||
(fn () (execute-request el nil nil))
|
||||
true (get mods "delay"))
|
||||
|
||||
(= kind "event")
|
||||
(bind-event el (get trigger "event") mods verbInfo))))
|
||||
triggers))))
|
||||
|
||||
|
||||
(define bind-event
|
||||
(fn (el event-name mods verbInfo)
|
||||
;; Bind a standard DOM event trigger.
|
||||
;; Handles delay, once, changed, optimistic, preventDefault.
|
||||
(let ((timer nil)
|
||||
(last-val nil)
|
||||
(listen-target (if (get mods "from")
|
||||
(dom-query (get mods "from"))
|
||||
el)))
|
||||
(when listen-target
|
||||
(dom-add-listener listen-target event-name
|
||||
(fn (e)
|
||||
(let ((should-fire true))
|
||||
;; Changed modifier: skip if value unchanged
|
||||
(when (get mods "changed")
|
||||
(let ((val (element-value el)))
|
||||
(if (= val last-val)
|
||||
(set! should-fire false)
|
||||
(set! last-val val))))
|
||||
|
||||
(when should-fire
|
||||
;; Prevent default for submit/click on links
|
||||
(when (or (= event-name "submit")
|
||||
(and (= event-name "click")
|
||||
(dom-has-attr? el "href")))
|
||||
(prevent-default e))
|
||||
|
||||
;; Delay modifier
|
||||
(if (get mods "delay")
|
||||
(do
|
||||
(clear-timeout timer)
|
||||
(set! timer
|
||||
(set-timeout
|
||||
(fn () (execute-request el verbInfo nil))
|
||||
(get mods "delay"))))
|
||||
(execute-request el verbInfo nil)))))
|
||||
(if (get mods "once") (dict "once" true) nil))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Post-swap lifecycle
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define post-swap
|
||||
(fn (root)
|
||||
;; Run lifecycle after swap: activate scripts, process SX, hydrate, process
|
||||
(activate-scripts root)
|
||||
(sx-process-scripts root)
|
||||
(sx-hydrate root)
|
||||
(process-elements root)))
|
||||
|
||||
|
||||
(define activate-scripts
|
||||
(fn (root)
|
||||
;; Re-activate scripts in swapped content.
|
||||
;; Scripts inserted via innerHTML are inert — clone to make them execute.
|
||||
(when root
|
||||
(let ((scripts (dom-query-all root "script")))
|
||||
(for-each
|
||||
(fn (dead)
|
||||
;; Skip already-processed or data-components scripts
|
||||
(when (and (not (dom-has-attr? dead "data-components"))
|
||||
(not (dom-has-attr? dead "data-sx-activated")))
|
||||
(let ((live (create-script-clone dead)))
|
||||
(dom-set-attr live "data-sx-activated" "true")
|
||||
(dom-replace-child (dom-parent dead) live dead))))
|
||||
scripts)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; OOB swap processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-oob-swaps
|
||||
(fn (container swap-fn)
|
||||
;; Find and process out-of-band swaps in container.
|
||||
;; swap-fn is (fn (target oob-element swap-type) ...).
|
||||
(let ((oobs (find-oob-swaps container)))
|
||||
(for-each
|
||||
(fn (oob)
|
||||
(let ((target-id (get oob "target-id"))
|
||||
(target (dom-query-by-id target-id))
|
||||
(oob-el (get oob "element"))
|
||||
(swap-type (get oob "swap-type")))
|
||||
;; Remove from source container
|
||||
(when (dom-parent oob-el)
|
||||
(dom-remove-child (dom-parent oob-el) oob-el))
|
||||
;; Swap into target
|
||||
(when target
|
||||
(swap-fn target oob-el swap-type))))
|
||||
oobs))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Head element hoisting
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define hoist-head-elements
|
||||
(fn (container)
|
||||
;; Move style[data-sx-css] and link[rel=stylesheet] to <head>
|
||||
;; so they take effect globally.
|
||||
(for-each
|
||||
(fn (style)
|
||||
(when (dom-parent style)
|
||||
(dom-remove-child (dom-parent style) style))
|
||||
(dom-append-to-head style))
|
||||
(dom-query-all container "style[data-sx-css]"))
|
||||
(for-each
|
||||
(fn (link)
|
||||
(when (dom-parent link)
|
||||
(dom-remove-child (dom-parent link) link))
|
||||
(dom-append-to-head link))
|
||||
(dom-query-all container "link[rel=\"stylesheet\"]"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Boost processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-boosted
|
||||
(fn (root)
|
||||
;; Find [sx-boost] containers and boost their descendants
|
||||
(for-each
|
||||
(fn (container)
|
||||
(boost-descendants container))
|
||||
(dom-query-all (or root (dom-body)) "[sx-boost]"))))
|
||||
|
||||
|
||||
(define boost-descendants
|
||||
(fn (container)
|
||||
;; Boost links and forms within a container
|
||||
;; Links get sx-get, forms get sx-post/sx-get
|
||||
(for-each
|
||||
(fn (link)
|
||||
(when (and (not (is-processed? link "boost"))
|
||||
(should-boost-link? link))
|
||||
(mark-processed! link "boost")
|
||||
;; Set default sx-target if not specified
|
||||
(when (not (dom-has-attr? link "sx-target"))
|
||||
(dom-set-attr link "sx-target" "#main-panel"))
|
||||
(when (not (dom-has-attr? link "sx-swap"))
|
||||
(dom-set-attr link "sx-swap" "innerHTML"))
|
||||
(when (not (dom-has-attr? link "sx-push-url"))
|
||||
(dom-set-attr link "sx-push-url" "true"))
|
||||
(bind-boost-link link (dom-get-attr link "href"))))
|
||||
(dom-query-all container "a[href]"))
|
||||
(for-each
|
||||
(fn (form)
|
||||
(when (and (not (is-processed? form "boost"))
|
||||
(should-boost-form? form))
|
||||
(mark-processed! form "boost")
|
||||
(let ((method (upper (or (dom-get-attr form "method") "GET")))
|
||||
(action (or (dom-get-attr form "action")
|
||||
(browser-location-href))))
|
||||
(when (not (dom-has-attr? form "sx-target"))
|
||||
(dom-set-attr form "sx-target" "#main-panel"))
|
||||
(when (not (dom-has-attr? form "sx-swap"))
|
||||
(dom-set-attr form "sx-swap" "innerHTML"))
|
||||
(bind-boost-form form method action))))
|
||||
(dom-query-all container "form"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; SSE processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-sse
|
||||
(fn (root)
|
||||
;; Find and bind SSE elements
|
||||
(for-each
|
||||
(fn (el)
|
||||
(when (not (is-processed? el "sse"))
|
||||
(mark-processed! el "sse")
|
||||
(bind-sse el)))
|
||||
(dom-query-all (or root (dom-body)) "[sx-sse]"))))
|
||||
|
||||
|
||||
(define bind-sse
|
||||
(fn (el)
|
||||
;; Connect to SSE endpoint and bind swap handler
|
||||
(let ((url (dom-get-attr el "sx-sse")))
|
||||
(when url
|
||||
(let ((source (event-source-connect url el))
|
||||
(event-name (parse-sse-swap el)))
|
||||
(event-source-listen source event-name
|
||||
(fn (data)
|
||||
(bind-sse-swap el data))))))))
|
||||
|
||||
|
||||
(define bind-sse-swap
|
||||
(fn (el data)
|
||||
;; Handle an SSE event: swap data into element
|
||||
(let ((target (resolve-target el))
|
||||
(swap-spec (parse-swap-spec
|
||||
(dom-get-attr el "sx-swap")
|
||||
(dom-has-class? (dom-body) "sx-transitions")))
|
||||
(swap-style (get swap-spec "style"))
|
||||
(use-transition (get swap-spec "transition"))
|
||||
(trimmed (trim data)))
|
||||
(when (not (empty? trimmed))
|
||||
(if (starts-with? trimmed "(")
|
||||
;; SX response
|
||||
(let ((rendered (sx-render trimmed))
|
||||
(container (dom-create-element "div" nil)))
|
||||
(dom-append container rendered)
|
||||
(with-transition use-transition
|
||||
(fn ()
|
||||
(swap-dom-nodes target (children-to-fragment container) swap-style)
|
||||
(post-swap target))))
|
||||
;; HTML response
|
||||
(with-transition use-transition
|
||||
(fn ()
|
||||
(swap-html-string target trimmed swap-style)
|
||||
(post-swap target))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Inline event handlers
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-inline-handlers
|
||||
(fn (root)
|
||||
;; Find elements with sx-on:* attributes and bind handlers
|
||||
(for-each
|
||||
(fn (el)
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((name (first attr))
|
||||
(body (nth attr 1)))
|
||||
(when (starts-with? name "sx-on:")
|
||||
(let ((event-name (slice name 6)))
|
||||
(when (not (is-processed? el (str "on:" event-name)))
|
||||
(mark-processed! el (str "on:" event-name))
|
||||
(bind-inline-handler el event-name body))))))
|
||||
(dom-attr-list el)))
|
||||
(dom-query-all (or root (dom-body)) "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:load]"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Preload
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define bind-preload-for
|
||||
(fn (el)
|
||||
;; Bind preload event listeners based on sx-preload attribute
|
||||
(let ((preload-attr (dom-get-attr el "sx-preload")))
|
||||
(when preload-attr
|
||||
(let ((info (get-verb-info el)))
|
||||
(when info
|
||||
(let ((url (get info "url"))
|
||||
(headers (build-request-headers el
|
||||
(loaded-component-names) _css-hash))
|
||||
(events (if (= preload-attr "mousedown")
|
||||
(list "mousedown" "touchstart")
|
||||
(list "mouseover")))
|
||||
(debounce-ms (if (= preload-attr "mousedown") 0 100)))
|
||||
(bind-preload el events debounce-ms
|
||||
(fn () (do-preload url headers))))))))))
|
||||
|
||||
|
||||
(define do-preload
|
||||
(fn (url headers)
|
||||
;; Execute a preload fetch into the cache
|
||||
(when (nil? (preload-cache-get _preload-cache url))
|
||||
(fetch-preload url headers _preload-cache))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Main element processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define VERB_SELECTOR
|
||||
(str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"))
|
||||
|
||||
(define process-elements
|
||||
(fn (root)
|
||||
;; Find all elements with sx-* verb attributes and process them.
|
||||
(let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR)))
|
||||
(for-each
|
||||
(fn (el)
|
||||
(when (not (is-processed? el "verb"))
|
||||
(mark-processed! el "verb")
|
||||
(process-one el)))
|
||||
els))
|
||||
;; Also process boost, SSE, inline handlers
|
||||
(process-boosted root)
|
||||
(process-sse root)
|
||||
(bind-inline-handlers root)))
|
||||
|
||||
|
||||
(define process-one
|
||||
(fn (el)
|
||||
;; Process a single element with an sx-* verb attribute
|
||||
(let ((verb-info (get-verb-info el)))
|
||||
(when verb-info
|
||||
;; Check for disabled
|
||||
(when (not (dom-has-attr? el "sx-disable"))
|
||||
(bind-triggers el verb-info)
|
||||
(bind-preload-for el))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; History: popstate handler
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-popstate
|
||||
(fn (scrollY)
|
||||
;; Handle browser back/forward navigation
|
||||
(let ((main (dom-query-by-id "main-panel"))
|
||||
(url (browser-location-href)))
|
||||
(when main
|
||||
(let ((headers (build-request-headers main
|
||||
(loaded-component-names) _css-hash)))
|
||||
(fetch-and-restore main url headers scrollY))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Initialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define engine-init
|
||||
(fn ()
|
||||
;; Initialize: CSS tracking, scripts, hydrate, process.
|
||||
(do
|
||||
(init-css-tracking)
|
||||
(sx-process-scripts nil)
|
||||
(sx-hydrate nil)
|
||||
(process-elements nil))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — Orchestration
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; From engine.sx (pure logic):
|
||||
;; parse-trigger-spec, default-trigger, get-verb-info, classify-trigger,
|
||||
;; build-request-headers, process-response-headers, parse-swap-spec,
|
||||
;; parse-retry-spec, next-retry-ms, resolve-target, apply-optimistic,
|
||||
;; revert-optimistic, find-oob-swaps, swap-dom-nodes, swap-html-string,
|
||||
;; morph-children, handle-history, preload-cache-get, preload-cache-set,
|
||||
;; should-boost-link?, should-boost-form?, parse-sse-swap, filter-params,
|
||||
;; PRELOAD_TTL
|
||||
;;
|
||||
;; === Promises ===
|
||||
;; (promise-resolve val) → resolved Promise
|
||||
;; (promise-catch p fn) → p.catch(fn)
|
||||
;;
|
||||
;; === Abort controllers ===
|
||||
;; (abort-previous el) → abort + remove controller for element
|
||||
;; (track-controller el ctrl) → store controller for element
|
||||
;; (new-abort-controller) → new AbortController()
|
||||
;; (controller-signal ctrl) → ctrl.signal
|
||||
;; (abort-error? err) → boolean (err.name === "AbortError")
|
||||
;;
|
||||
;; === Timers ===
|
||||
;; (set-timeout fn ms) → timer id
|
||||
;; (set-interval fn ms) → timer id
|
||||
;; (clear-timeout id) → void
|
||||
;; (request-animation-frame fn) → void
|
||||
;;
|
||||
;; === Fetch ===
|
||||
;; (fetch-request config success-fn error-fn) → Promise
|
||||
;; config: dict with url, method, headers, body, signal, preloaded,
|
||||
;; cross-origin
|
||||
;; success-fn: (fn (resp-ok status get-header text) ...)
|
||||
;; error-fn: (fn (err) ...)
|
||||
;; (fetch-location url) → fetch URL and swap to #main-panel
|
||||
;; (fetch-and-restore main url headers scroll-y) → popstate fetch+swap
|
||||
;; (fetch-preload url headers cache) → preload into cache
|
||||
;;
|
||||
;; === Request body ===
|
||||
;; (build-request-body el method url) → dict with body, url, content-type
|
||||
;;
|
||||
;; === Loading state ===
|
||||
;; (show-indicator el) → indicator state (or nil)
|
||||
;; (disable-elements el) → list of disabled elements
|
||||
;; (clear-loading-state el indicator disabled-elts) → void
|
||||
;;
|
||||
;; === DOM extras (beyond adapter-dom.sx) ===
|
||||
;; (dom-query-by-id id) → Element or nil
|
||||
;; (dom-matches? el sel) → boolean
|
||||
;; (dom-closest el sel) → Element or nil
|
||||
;; (dom-body) → document.body
|
||||
;; (dom-has-class? el cls) → boolean
|
||||
;; (dom-append-to-head el) → void
|
||||
;; (dom-parse-html-document text) → parsed document (DOMParser)
|
||||
;; (dom-outer-html el) → string
|
||||
;; (dom-body-inner-html doc) → string
|
||||
;; (dom-tag-name el) → uppercase tag name
|
||||
;;
|
||||
;; === Events ===
|
||||
;; (dom-dispatch el name detail) → boolean (dispatchEvent)
|
||||
;; (dom-add-listener el event fn opts) → void
|
||||
;; (prevent-default e) → void
|
||||
;; (element-value el) → el.value or nil
|
||||
;;
|
||||
;; === Validation ===
|
||||
;; (validate-for-request el) → boolean
|
||||
;;
|
||||
;; === View Transitions ===
|
||||
;; (with-transition enabled fn) → void
|
||||
;;
|
||||
;; === IntersectionObserver ===
|
||||
;; (observe-intersection el fn once? delay) → void
|
||||
;;
|
||||
;; === EventSource ===
|
||||
;; (event-source-connect url el) → EventSource (with cleanup)
|
||||
;; (event-source-listen source event fn) → void
|
||||
;;
|
||||
;; === Boost bindings ===
|
||||
;; (bind-boost-link el href) → void (click handler + pushState)
|
||||
;; (bind-boost-form form method action) → void (submit handler)
|
||||
;;
|
||||
;; === Inline handlers ===
|
||||
;; (bind-inline-handler el event-name body) → void (new Function)
|
||||
;;
|
||||
;; === Preload ===
|
||||
;; (bind-preload el events debounce-ms fn) → void
|
||||
;;
|
||||
;; === Processing markers ===
|
||||
;; (mark-processed! el key) → void
|
||||
;; (is-processed? el key) → boolean
|
||||
;;
|
||||
;; === Script handling ===
|
||||
;; (create-script-clone script) → live script Element
|
||||
;;
|
||||
;; === SX API (references to Sx/SxRef object) ===
|
||||
;; (sx-render source) → DOM nodes
|
||||
;; (sx-process-scripts root) → void
|
||||
;; (sx-hydrate root) → void
|
||||
;; (loaded-component-names) → list of ~name strings
|
||||
;;
|
||||
;; === Response processing ===
|
||||
;; (strip-component-scripts text) → cleaned text
|
||||
;; (extract-response-css text) → cleaned text
|
||||
;; (select-from-container el sel) → DocumentFragment
|
||||
;; (children-to-fragment el) → DocumentFragment
|
||||
;; (select-html-from-doc doc sel) → HTML string
|
||||
;;
|
||||
;; === Parsing ===
|
||||
;; (try-parse-json s) → parsed value or nil
|
||||
;;
|
||||
;; === Browser (via engine.sx) ===
|
||||
;; (browser-location-href) → current URL string
|
||||
;; (browser-navigate url) → void
|
||||
;; (browser-reload) → void
|
||||
;; (browser-media-matches? query) → boolean
|
||||
;; (browser-confirm msg) → boolean
|
||||
;; (browser-prompt msg) → string or nil
|
||||
;; (csrf-token) → string
|
||||
;; (cross-origin? url) → boolean
|
||||
;; (now-ms) → timestamp ms
|
||||
;; --------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user