/** * 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 AKSRT */ (function (window, document) { "use strict"; var ROOT_ID = "aksrt-modal-runtime-root"; var active = null; 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 = "aksrt-modal-runtime-host"; document.body.appendChild(root); return root; } function escapeHtml(value) { return String(value == null ? "" : value) .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function lockScroll(lock) { document.documentElement.classList.toggle("aksrt-modal-runtime-open", !!lock); document.body.classList.toggle("aksrt-modal-runtime-open", !!lock); } function skeleton(options) { return (options && options.skeleton) || '
' + '' + '' + '' + '' + '
'; } function telemetryStart(kind) { if (kind === false) return ""; return window.AksrtModalTelemetry.start(kind || "generic"); } function telemetryMark(session, event) { if (!session || !session.telemetryId) return; window.AksrtModalTelemetry.mark(session.telemetryId, event); } 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-aksrt-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-aksrt-modal-close], .aksrt-modal-runtime__close"; } function bindChrome(session) { var shell = session.shell; shell.addEventListener("click", function (event) { var closeButton = event.target.closest(closeSelector()); if (closeButton) { event.preventDefault(); close("control"); return; } if (event.target === shell || event.target.classList.contains("aksrt-modal-runtime__overlay")) { close("overlay"); return; } var retry = event.target.closest("[data-aksrt-modal-retry]"); if (retry) { event.preventDefault(); refresh(); } }); shell.addEventListener("keydown", function (event) { 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 titleId = "aksrt-modal-title-" + String(sequence + 1); var titleHtml = options.title ? '

' + escapeHtml(options.title) + '

' : ""; shell.className = "aksrt-modal-runtime" + (bottom ? " aksrt-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 = '
' + '
' + titleHtml + '
' + initialHtml + "
"; var session = { id: String(++sequence), shell: shell, content: shell.querySelector(".aksrt-modal-runtime__content"), options: options, state: window.AksrtModalState.create(options.kind || "generic"), focusReturn: document.activeElement, controller: null, flight: null, closed: false, telemetryId: telemetryStart(options.telemetry === false ? false : options.kind), }; root.appendChild(shell); bindChrome(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("aksrt: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 close(reason, preserveRequest) { if (!active) return; var session = active; active = null; session.closed = true; if (!preserveRequest) abortSessionRequest(session); session.shell.classList.remove("is-open"); if (!preserveRequest) lockScroll(false); document.dispatchEvent(new CustomEvent("aksrt:modal:close", { detail: { shell: session.shell, reason: reason || "close", session: session }, })); var node = session.shell; window.setTimeout(function () { if (node && node.parentNode) node.parentNode.removeChild(node); var target = session.focusReturn; if (!preserveRequest && !active && target && target !== node && document.contains(target) && typeof target.focus === "function") { try { target.focus(); } catch (error) {} } }, 220); } 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 (!session || session.closed || active !== session) return; var unauthorized = error && error.code === "unauthorized"; setState(session, unauthorized ? "unauthorized" : "error", error); telemetryMark(session, unauthorized ? "unauthorized" : "error"); session.content.innerHTML = '
提示
' + '
' + escapeHtml(errorMessage(error)) + '
'; notifyContentLoaded(session); if (typeof session.options.onError === "function") session.options.onError(error, session); } function renderResult(session, result) { if (!session || session.closed || active !== 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 = '
暂无内容
'; 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 = '
暂无内容
'; 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.AksrtRequest.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.AksrtRequest.url(session.options.url, session.options.query); var cache = window.AksrtModalCache; 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 (!session.closed && active === session) renderResult(session, result); return session.shell; }).catch(function (error) { var aborted = error && (error.name === "AbortError" || error.code === "ABORT_ERR"); if (!aborted && !session.closed && active === 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 || {}; close("replace"); 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.AksrtRequest.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); close("replace", !!sameRequest); 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.AksrtModalCache.invalidate(tags); } window.AksrtModalRuntime = { 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("aksrt:modal-runtime:ready")); // legacy 契约(只绑定一次):modal 展示/内容注入后调用全局 auto_fun 重扫。 if (window.jQuery) { window.jQuery(document).on( "loaded.bs.modal show.bs.modal", ".modal, .aksrt-modal-runtime", function () { if (typeof window.auto_fun === "function") window.auto_fun(); } ); } })(window, document);