/** * Canonical frontend modal runtime. * * Keeps the existing remote-modal shell while adding cancellable loaders, * short in-memory caching, request deduplication, retryable errors, focus * restoration, and modal-scoped rehydration. * * @package AKSRT */ (function (window, document) { "use strict"; var ROOT_ID = "aksrt-remote-modal-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-remote-modal-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-remote-modal-open", !!lock); document.body.classList.toggle("aksrt-remote-modal-open", !!lock); } function skeleton(options) { return (options && options.skeleton) || '
' + '' + '' + '' + '' + '
'; } function telemetryStart(kind) { if (kind === false) return ""; var telemetry = window.AksrtModalTelemetry; if (!telemetry || typeof telemetry.start !== "function") return ""; return telemetry.start(kind || "unknown"); } function telemetryMark(session, event) { var telemetry = window.AksrtModalTelemetry; if (!session || !session.telemetryId || !telemetry || typeof telemetry.mark !== "function") return; telemetry.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-remote-modal-close], [data-aksrt-modal-close], [data-dismiss='modal'], .aksrt-remote-modal__close, .aksrt-vip-modal-close, .payvip-modal .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-remote-modal__overlay")) { close("overlay"); return; } var retry = event.target.closest("[data-aksrt-remote-modal-retry], [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-remote-modal" + (bottom ? " aksrt-remote-modal--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-remote-modal__content"), options: options, state: window.AksrtModalState && typeof window.AksrtModalState.create === "function" ? window.AksrtModalState.create(options.kind || "generic") : { status: "idle", error: null }, 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:remote-modal:open", { detail: { shell: shell, options: options, session: session }, })); document.dispatchEvent(new CustomEvent("aksrt:modal:open", { detail: { shell: shell, options: options, session: session }, })); if (options.html !== undefined) { rehydrate(session); telemetryMark(session, "first-content"); telemetryMark(session, "ready"); if (typeof options.onContent === "function") options.onContent(options.html, session); } return session; } function rehydrate(session) { if (!session || !session.content) return; if (window.AksrtFrontend && typeof window.AksrtFrontend.rehydrate === "function") { window.AksrtFrontend.rehydrate(session.content, "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:remote-modal:close", { detail: { shell: session.shell, reason: reason || "close", session: session }, })); 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 responseHtml(result) { if (!result) return ""; var json = result.json; if (json) { if (typeof json.html === "string") return json.html; if (json.data && typeof json.data.html === "string") return json.data.html; if (json.data && typeof json.data.show_modal === "string") return json.data.show_modal; if (typeof json.data === "string") return json.data; } return 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)) + '
'; rehydrate(session); if (typeof session.options.onError === "function") session.options.onError(error, session); } function renderResult(session, result) { if (!session || session.closed || active !== session) 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"); rehydrate(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 = window.AksrtRequest && typeof window.AksrtRequest.request === "function" ? function () { return window.AksrtRequest.request(session.options.url, requestOptions); } : function () { return window.fetch(session.options.url, requestOptions).then(function (response) { return response.text().then(function (text) { return { ok: response.ok, status: response.status, text: text, json: null }; }); }); }; var method = String(session.options.method || "GET").toUpperCase(); var cacheable = method === "GET" && session.options.cache !== false; var cacheKey = session.options.cacheKey || method + ":" + (window.AksrtRequest && typeof window.AksrtRequest.url === "function" ? window.AksrtRequest.url(session.options.url, session.options.query) : session.options.url); 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) {} } }, }; session.flight = cacheable && cache && typeof cache.acquire === "function" ? cache.acquire(cacheKey, request, acquireOptions) : { promise: request(), release: function () {} }; return session.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) { if (session.flight && typeof session.flight.release === "function") session.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 && typeof window.AksrtRequest.url === "function" ? window.AksrtRequest.url(options.url, options.query) : String(options.url); 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) { if (window.AksrtModalCache && typeof window.AksrtModalCache.invalidate === "function") { return window.AksrtModalCache.invalidate(tags); } return 0; } 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")); })(window, document);