Files
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

536 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Canonical frontend modal runtime.
*
* Owns the modal shell while adding cancellable loaders,
* short in-memory caching, request deduplication, retryable errors, focus
* restoration, and legacy `loaded.bs.modal` content lifecycle (auto_fun rescan).
*
* @package Jio
*/
(function (window, document) {
"use strict";
var ROOT_ID = "jio-modal-runtime-root";
var active = null;
var sessions = [];
var sequence = 0;
var loaders = Object.create(null);
function isMobile() {
return window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
}
function ensureRoot() {
var root = document.getElementById(ROOT_ID);
if (root) return root;
root = document.createElement("div");
root.id = ROOT_ID;
root.className = "jio-modal-runtime-host";
document.body.appendChild(root);
return root;
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// 参考主题 Bootstrap Modal setScrollbar/resetScrollbar 等价物:
// overflow:hidden 会去掉页面滚动条,版心与 fixed 顶栏随之变宽而横向跳动,
// 因此锁定前测量滚动条宽度,给 body 与 fixed 元素补同宽 padding-right,解锁时还原。
var SCROLLBAR_FIXED = ".nav-fixed .header, .float-right";
var scrollLock = null;
function padRight(element, gap) {
var inline = element.style.paddingRight;
var computed = parseFloat(window.getComputedStyle(element).paddingRight) || 0;
element.style.paddingRight = computed + gap + "px";
return { element: element, inline: inline };
}
function lockScroll(lock) {
var html = document.documentElement;
var body = document.body;
if (lock) {
if (scrollLock) return;
var gap = window.innerWidth - html.clientWidth;
scrollLock = [];
if (gap > 0) {
scrollLock.push(padRight(body, gap));
document.querySelectorAll(SCROLLBAR_FIXED).forEach(function (element) {
scrollLock.push(padRight(element, gap));
});
}
html.classList.add("jio-modal-runtime-open");
body.classList.add("jio-modal-runtime-open");
return;
}
if (!scrollLock) return;
html.classList.remove("jio-modal-runtime-open");
body.classList.remove("jio-modal-runtime-open");
scrollLock.forEach(function (entry) {
entry.element.style.paddingRight = entry.inline;
});
scrollLock = null;
}
function skeleton(options) {
if (options && options.loading === "spinner") {
return '<div class="jio-modal-runtime__loading flex jc"><div class="em2x opacity5"><svg class="loading-circle" viewBox="0 0 50 50"><circle cx="25" cy="25" r="20"></circle></svg></div></div>';
}
return (options && options.skeleton) ||
'<div class="jio-modal-runtime__skeleton" aria-live="polite" aria-busy="true">' +
'<span class="jio-modal-runtime__skeleton-line is-wide"></span>' +
'<span class="jio-modal-runtime__skeleton-line"></span>' +
'<span class="jio-modal-runtime__skeleton-line is-short"></span>' +
'<span class="jio-modal-runtime__skeleton-block"></span>' +
'</div>';
}
function telemetryStart(kind) {
if (kind === false) return "";
return window.JioModalTelemetry.start(kind || "generic");
}
function telemetryMark(session, event) {
if (!session || !session.telemetryId) return;
window.JioModalTelemetry.mark(session.telemetryId, event);
}
function isLive(session) {
return !!session && !session.closed && sessions.indexOf(session) !== -1;
}
function setState(session, status, error) {
session.state.status = status;
session.state.error = error || null;
session.state.updatedAt = Date.now();
if (session.shell) session.shell.setAttribute("data-jio-modal-state", status);
if (typeof session.options.onState === "function") {
session.options.onState(status, session);
}
}
function focusable(shell) {
return shell.querySelectorAll("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])");
}
function closeSelector() {
return "[data-jio-modal-close], .jio-modal-runtime__close";
}
function bindChrome(session) {
var shell = session.shell;
shell.addEventListener("click", function (event) {
if (active !== session) return;
var closeButton = event.target.closest(closeSelector());
if (closeButton) {
event.preventDefault();
close("control");
return;
}
if (event.target === shell || event.target.classList.contains("jio-modal-runtime__overlay")) {
close("overlay");
return;
}
var retry = event.target.closest("[data-jio-modal-retry]");
if (retry) {
event.preventDefault();
refresh();
}
});
shell.addEventListener("keydown", function (event) {
if (active !== session) return;
if (event.key === "Escape") {
event.preventDefault();
close("escape");
return;
}
if (event.key !== "Tab") return;
var nodes = focusable(shell);
if (!nodes.length) {
event.preventDefault();
shell.focus();
return;
}
var first = nodes[0];
var last = nodes[nodes.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
}
function createSession(options) {
options = options || {};
var root = ensureRoot();
var shell = document.createElement("div");
var bottom = options.bottom === true || (options.mobileBottom !== false && isMobile());
var heightStyle = options.height ? "min-height:" + parseInt(options.height, 10) + "px;" : "";
var initialHtml = options.html !== undefined ? options.html : skeleton(options);
var swipeHandle = bottom && options.swipeClose ? '<div class="touch-close" aria-hidden="true"></div>' : "";
var titleId = "jio-modal-title-" + String(sequence + 1);
var titleHtml = options.title
? '<header class="jio-modal-runtime__header"><h3 class="jio-modal-runtime__title" id="' + titleId + '">' + escapeHtml(options.title) + '</h3><button type="button" class="jio-modal-runtime__close" data-jio-modal-close aria-label="关闭">&times;</button></header>'
: "";
shell.className =
"jio-modal-runtime" +
(active ? " jio-modal-runtime--child" : "") +
(bottom ? " jio-modal-runtime--bottom" : "") +
(options.className ? " " + options.className : "");
shell.setAttribute("role", "dialog");
shell.setAttribute("aria-modal", "true");
if (options.title) shell.setAttribute("aria-labelledby", titleId);
shell.setAttribute("tabindex", "-1");
shell.innerHTML =
'<div class="jio-modal-runtime__overlay" data-jio-modal-close></div>' +
'<div class="jio-modal-runtime__dialog" style="' + heightStyle + '">' +
titleHtml +
'<div class="jio-modal-runtime__content">' + initialHtml + "</div>" + swipeHandle + "</div>";
var session = {
id: String(++sequence),
shell: shell,
content: shell.querySelector(".jio-modal-runtime__content"),
options: options,
state: window.JioModalState.create(options.kind || "generic"),
focusReturn: options.focusReturn !== undefined ? options.focusReturn : document.activeElement,
controller: null,
flight: null,
closed: false,
telemetryId: telemetryStart(options.telemetry === false ? false : options.kind),
};
if (active) {
active.shell.classList.add("jio-modal-runtime--parent");
active.shell.setAttribute("aria-modal", "false");
active.shell.setAttribute("aria-hidden", "true");
active.shell.setAttribute("inert", "");
}
root.appendChild(shell);
bindChrome(session);
if (bottom && options.swipeClose && window.jQuery && window.jQuery.fn && typeof window.jQuery.fn.minitouch === "function") {
window.jQuery(shell).minitouch({
direction: "bottom",
selector: ".jio-modal-runtime__dialog",
start_selector: ".modal-colorful-header,.touch-close,.touch",
depreciation: 50,
stop: function () { return active !== session; },
onEnd: function () {
if (active === session) close("swipe");
},
});
}
sessions.push(session);
active = session;
lockScroll(true);
void shell.offsetWidth;
shell.classList.add("is-open");
setState(session, options.html !== undefined ? "ready" : "loading");
telemetryMark(session, "shell");
var nodes = focusable(shell);
try {
(nodes.length ? nodes[0] : shell).focus();
} catch (error) {}
document.dispatchEvent(new CustomEvent("jio:modal:open", {
detail: { shell: shell, options: options, session: session },
}));
if (options.html !== undefined) {
notifyContentLoaded(session);
telemetryMark(session, "first-content");
telemetryMark(session, "ready");
if (typeof options.onContent === "function") options.onContent(options.html, session);
}
return session;
}
// legacy 契约:modal 内容注入后触发 loaded.bs.modal,
// 由全局委托(见文件底部)驱动 auto_fun 重扫,不做模块内扫描。
function notifyContentLoaded(session) {
if (!session || !session.shell || !window.jQuery) return;
window.jQuery(session.shell).trigger("loaded.bs.modal");
}
function abortSessionRequest(session) {
if (!session) return;
if (session.flight && typeof session.flight.release === "function") session.flight.release();
session.flight = null;
if (session.controller) {
try { session.controller.abort(); } catch (error) {}
}
session.controller = null;
}
function removeActive(reason, preserveRequest, resumeParent) {
if (!active) return;
var session = active;
if (sessions[sessions.length - 1] !== session) return;
sessions.pop();
active = sessions[sessions.length - 1] || null;
session.closed = true;
if (!preserveRequest) abortSessionRequest(session);
session.shell.classList.remove("is-open");
if (active && resumeParent) {
active.shell.classList.remove("jio-modal-runtime--parent");
active.shell.setAttribute("aria-modal", "true");
active.shell.removeAttribute("aria-hidden");
active.shell.removeAttribute("inert");
}
if (!active && !preserveRequest) lockScroll(false);
document.dispatchEvent(new CustomEvent("jio:modal:close", {
detail: { shell: session.shell, reason: reason || "close", session: session },
}));
var node = session.shell;
var exitDelay = 220;
var dialog = node.querySelector(".jio-modal-runtime__dialog");
if (dialog) {
var durations = window.getComputedStyle(dialog).transitionDuration || "";
durations.split(",").forEach(function (duration) {
var milliseconds = parseFloat(duration) * (duration.trim().slice(-2) === "ms" ? 1 : 1000);
if (milliseconds > exitDelay) exitDelay = milliseconds;
});
}
window.setTimeout(function () {
if (node && node.parentNode) node.parentNode.removeChild(node);
var target = session.focusReturn;
if (!preserveRequest && resumeParent && active) {
var canRestoreTarget = target && target !== node && active.shell.contains(target) && typeof target.focus === "function";
var fallbackNodes = canRestoreTarget ? [] : focusable(active.shell);
var fallback = fallbackNodes.length ? fallbackNodes[0] : active.shell;
try { (canRestoreTarget ? target : fallback).focus(); } catch (error) {}
} else if (!preserveRequest && resumeParent && !active && target && target !== node && document.contains(target) && typeof target.focus === "function") {
try { target.focus(); } catch (error) {}
}
}, exitDelay);
}
function close(reason, preserveRequest) {
removeActive(reason, preserveRequest, true);
}
function responseData(result) {
if (!result) return null;
if (result.data !== undefined && result.data !== null) return result.data;
if (result.json && result.json.data !== undefined) return result.json.data;
return result.json;
}
function responseHtml(result) {
var data = responseData(result);
if (data && typeof data.html === "string") return data.html;
if (result && result.json && typeof result.json.html === "string") return result.json.html;
return result && typeof result.text === "string" ? result.text : "";
}
function errorMessage(error) {
if (error && error.code === "unauthorized") return "请先登录";
return error && error.message ? String(error.message) : "加载失败,请重试";
}
function renderError(session, error) {
if (!isLive(session)) return;
var unauthorized = error && error.code === "unauthorized";
setState(session, unauthorized ? "unauthorized" : "error", error);
telemetryMark(session, unauthorized ? "unauthorized" : "error");
session.content.innerHTML =
'<div class="jio-modal-runtime__error touch border-title"><b>提示</b></div>' +
'<div class="jio-modal-runtime__error-body">' + escapeHtml(errorMessage(error)) +
'</div><div class="but-average modal-buts">' +
'<button type="button" class="but" data-jio-modal-retry>重试</button>' +
'<button type="button" class="but" data-jio-modal-close>关闭</button></div>';
notifyContentLoaded(session);
if (typeof session.options.onError === "function") session.options.onError(error, session);
}
function renderResult(session, result) {
if (!isLive(session)) return;
if (session.options.responseType === "json" || typeof session.options.renderData === "function") {
var data = responseData(result);
if (data === null || data === undefined) {
setState(session, "empty");
telemetryMark(session, "first-content");
telemetryMark(session, "ready");
session.content.innerHTML = '<div class="jio-modal-runtime__error-body">暂无内容</div>';
return;
}
if (typeof session.options.renderData === "function") {
session.options.renderData(data, session, result);
}
setState(session, "ready");
telemetryMark(session, "first-content");
notifyContentLoaded(session);
telemetryMark(session, "ready");
if (typeof session.options.onData === "function") session.options.onData(data, session, result);
return;
}
var html = responseHtml(result);
if (!html) {
setState(session, "empty");
telemetryMark(session, "first-content");
telemetryMark(session, "ready");
session.content.innerHTML = '<div class="jio-modal-runtime__error-body">暂无内容</div>';
return;
}
session.content.innerHTML = html;
setState(session, "ready");
telemetryMark(session, "first-content");
notifyContentLoaded(session);
telemetryMark(session, "ready");
if (typeof session.options.onContent === "function") session.options.onContent(html, session);
}
function load(session, loadOptions) {
loadOptions = loadOptions || {};
if (!session || session.closed || !session.options.url) return Promise.resolve(session && session.shell);
abortSessionRequest(session);
setState(session, "loading");
session.content.innerHTML = skeleton(session.options);
session.controller = window.AbortController ? new window.AbortController() : null;
var requestOptions = {
method: session.options.method || "GET",
headers: session.options.headers || {},
body: session.options.body,
query: session.options.query,
signal: session.controller ? session.controller.signal : undefined,
};
if (requestOptions.body === undefined) delete requestOptions.body;
if (!requestOptions.signal) delete requestOptions.signal;
var request = function () {
return window.JioRequest.request(session.options.url, requestOptions);
};
var method = String(session.options.method || "GET").toUpperCase();
var cacheable = method === "GET" && session.options.cache !== false;
var cacheKey = session.options.cacheKey ||
method + ":" + window.JioRequest.url(session.options.url, session.options.query);
var cache = window.JioModalCache;
var acquireOptions = {
ttl: Number(session.options.cacheTtl || 15000),
tags: session.options.cacheTags || session.options.tags || [],
skipCache: !!loadOptions.force || !cacheable,
store: cacheable,
cancel: function () {
if (session.controller) {
try { session.controller.abort(); } catch (error) {}
}
},
};
var flight = cacheable
? cache.acquire(cacheKey, request, acquireOptions)
: { promise: request(), release: function () {} };
session.flight = flight;
return flight.promise.then(function (result) {
if (isLive(session)) renderResult(session, result);
return session.shell;
}).catch(function (error) {
var aborted = error && (error.name === "AbortError" || error.code === "ABORT_ERR");
if (!aborted && isLive(session)) renderError(session, error);
return session.shell;
}).then(function (shell) {
// A refresh/replacement may have installed a newer flight on this session.
// Only the owner of this promise may release and clear the slot.
if (session.flight === flight) {
if (flight && typeof flight.release === "function") flight.release();
session.flight = null;
}
return shell;
});
}
function open(options) {
options = options || {};
if (!options.newModal || !active) {
if (active && !options.newModal) options.focusReturn = active.focusReturn;
removeActive("replace", false, false);
}
var session = createSession(options);
return session.shell;
}
function sessionKey(options) {
if (!options || !options.url) return "";
var method = String(options.method || "GET").toUpperCase();
var requestUrl = window.JioRequest.url(options.url, options.query);
return method + ":" + requestUrl;
}
function openRemote(options) {
options = Object.assign({}, options || {});
if (!options.url) return Promise.reject(new Error("missing url"));
var sameRequest = active && active.flight && sessionKey(active.options) === sessionKey(options);
if (!options.newModal || !active) {
if (active && !options.newModal) options.focusReturn = active.focusReturn;
removeActive("replace", !!sameRequest, false);
}
var session = createSession(options);
return load(session).then(function () { return session.shell; });
}
function refresh(options) {
if (!active || !active.options.url) return Promise.resolve(null);
if (options && typeof options === "object") active.options = Object.assign({}, active.options, options);
return load(active, { force: true });
}
function registerLoader(kind, loader) {
if (!kind || typeof loader !== "function") return;
loaders[String(kind)] = loader;
}
function openLoader(kind, context) {
var loader = loaders[String(kind)];
if (!loader) return Promise.reject(new Error("missing modal loader: " + kind));
return openRemote(loader(context || {}));
}
function invalidate(tags) {
return window.JioModalCache.invalidate(tags);
}
window.JioModalRuntime = {
open: open,
openRemote: openRemote,
close: close,
refresh: refresh,
registerLoader: registerLoader,
openLoader: openLoader,
invalidate: invalidate,
getActive: function () { return active ? active.shell : null; },
getState: function () { return active ? active.state : null; },
};
document.dispatchEvent(new CustomEvent("jio:modal-runtime:ready"));
// legacy 契约(只绑定一次):modal 展示/内容注入后调用全局 auto_fun 重扫。
if (window.jQuery) {
window.jQuery(document).on(
"loaded.bs.modal show.bs.modal",
".modal, .jio-modal-runtime",
function () {
if (typeof window.auto_fun === "function") window.auto_fun();
}
);
}
})(window, document);