feat(modal): add cancellable shared runtime

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-08-04 23:24:37 +08:00
1 parent 0647c501ba
commit d79b063bf9
12 files changed
+999 -392

No files matched your search

+57 -212
View File
@@ -1,232 +1,77 @@
/**
* AKSRT Remote Modal — zibll RefreshModal parity.
* AKSRT Remote Modal compatibility bridge.
*
* Open a center/bottom modal with static HTML or remote fetch.
* Contract:
* - data-toggle="AksrtRemoteModal" | data-aksrt-remote-modal
* - data-remote / href → fetch HTML
* - data-class → dialog class (modal-mini full-sm …)
* - data-height → min-height hint
* - mobile-bottom="true" → bottom sheet on narrow viewports
* The lifecycle implementation lives in AksrtModalRuntime. This file keeps
* the legacy trigger contract and the AksrtRemoteModal public alias stable.
*
* @package AKSRT
*/
(function (window, document) {
"use strict";
var ROOT_ID = "aksrt-remote-modal-root";
var active = null;
function isMobile() {
return window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
function runtime() {
return window.AksrtModalRuntime || null;
}
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 fromTrigger(element) {
var modalRuntime = runtime();
if (!modalRuntime || !element) return Promise.reject(new Error("modal runtime unavailable"));
function restHeaders() {
var h = { Accept: "text/html, application/json" };
if (window.aksrtData && aksrtData.restNonce) {
h["X-WP-Nonce"] = aksrtData.restNonce;
}
return h;
}
var url = element.getAttribute("data-remote") ||
element.getAttribute("data-aksrt-remote") ||
element.getAttribute("href") || "";
if (!url || url === "#") return Promise.resolve(null);
function lockScroll(lock) {
document.documentElement.classList.toggle("aksrt-remote-modal-open", !!lock);
document.body.classList.toggle("aksrt-remote-modal-open", !!lock);
}
var ttl = parseInt(element.getAttribute("data-modal-cache-ttl") || "15000", 10);
var tags = (element.getAttribute("data-modal-cache-tags") || "").split(",").map(function (tag) {
return tag.trim();
}).filter(Boolean);
function close() {
if (!active) return;
active.classList.remove("is-open");
lockScroll(false);
var node = active;
active = null;
window.setTimeout(function () {
if (node && node.parentNode) node.parentNode.removeChild(node);
}, 220);
document.dispatchEvent(new CustomEvent("aksrt:remote-modal:close"));
}
function bindChrome(shell) {
shell.addEventListener("click", function (e) {
if (e.target.closest("[data-aksrt-remote-modal-close], [data-dismiss='modal'], .aksrt-remote-modal__close")) {
e.preventDefault();
close();
return;
}
if (e.target === shell || e.target.classList.contains("aksrt-remote-modal__overlay")) {
close();
}
});
shell.addEventListener("keydown", function (e) {
if (e.key === "Escape") close();
});
}
function open(options) {
options = options || {};
close();
var root = ensureRoot();
var shell = document.createElement("div");
var bottom = options.bottom === true || (options.mobileBottom !== false && isMobile());
shell.className =
"aksrt-remote-modal" +
(bottom ? " aksrt-remote-modal--bottom" : "") +
(options.className ? " " + options.className : "");
shell.setAttribute("role", "dialog");
shell.setAttribute("aria-modal", "true");
var heightStyle = options.height ? "min-height:" + parseInt(options.height, 10) + "px;" : "";
shell.innerHTML =
'<div class="aksrt-remote-modal__overlay" data-aksrt-remote-modal-close></div>' +
'<div class="aksrt-remote-modal__dialog" style="' + heightStyle + '">' +
'<div class="aksrt-remote-modal__content">' +
(options.html || "") +
"</div></div>";
root.appendChild(shell);
bindChrome(shell);
active = shell;
lockScroll(true);
// Force reflow then open for CSS transition.
void shell.offsetWidth;
shell.classList.add("is-open");
var focusable = shell.querySelector(
"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
);
if (focusable) {
try {
focusable.focus();
} catch (err) {}
}
document.dispatchEvent(
new CustomEvent("aksrt:remote-modal:open", { detail: { shell: shell, options: options } })
);
return shell;
}
function openRemote(options) {
options = options || {};
var url = options.url || "";
if (!url) return Promise.reject(new Error("missing url"));
var loading = open({
html:
'<div class="aksrt-remote-modal__loading"><i class="fas fa-spinner fa-spin"></i> ' +
((window.aksrtData && aksrtData.ajaxLoading) || "加载中…") +
"</div>",
className: options.className || "modal-mini full-sm",
bottom: options.bottom,
mobileBottom: options.mobileBottom,
height: options.height,
});
return fetch(url, {
method: options.method || "GET",
credentials: "same-origin",
headers: Object.assign(restHeaders(), options.headers || {}),
body: options.body,
})
.then(function (res) {
var type = (res.headers.get("content-type") || "").toLowerCase();
if (type.indexOf("application/json") !== -1) {
return res.json().then(function (json) {
if (!res.ok || (json && json.success === false)) {
var msg =
(json && (json.message || (json.data && json.data.message))) ||
"加载失败";
throw new Error(msg);
}
var html =
(json && (json.html || (json.data && (json.data.html || json.data)))) || "";
if (typeof html !== "string") html = "";
return html;
});
}
return res.text().then(function (text) {
if (!res.ok) throw new Error("加载失败");
return text;
});
})
.then(function (html) {
if (loading && loading.parentNode) loading.parentNode.removeChild(loading);
if (active === loading) active = null;
return open({
html: html,
className: options.className || "modal-mini full-sm",
bottom: options.bottom,
mobileBottom: options.mobileBottom,
height: options.height,
});
})
.catch(function (err) {
if (loading && loading.parentNode) loading.parentNode.removeChild(loading);
if (active === loading) active = null;
open({
html:
'<div class="aksrt-remote-modal__error touch border-title"><b>提示</b></div>' +
'<div class="aksrt-remote-modal__error-body">' +
(err && err.message ? String(err.message) : "加载失败") +
'</div><div class="but-average modal-buts"><button type="button" class="but" data-aksrt-remote-modal-close>关闭</button></div>',
className: "modal-mini",
mobileBottom: true,
});
throw err;
});
}
function fromTrigger(el) {
if (!el) return null;
var url =
el.getAttribute("data-remote") ||
el.getAttribute("data-aksrt-remote") ||
el.getAttribute("href") ||
"";
if (!url || url === "#") return null;
var height = el.getAttribute("data-height") || el.getAttribute("data-aksrt-height");
return openRemote({
return modalRuntime.openRemote({
url: url,
className: el.getAttribute("data-class") || el.getAttribute("data-aksrt-class") || "modal-mini full-sm",
height: height ? parseInt(height, 10) : 0,
mobileBottom: el.getAttribute("mobile-bottom") !== "false",
bottom: el.getAttribute("data-bottom") === "true",
method: element.getAttribute("data-method") || "GET",
className: element.getAttribute("data-class") || element.getAttribute("data-aksrt-class") || "modal-mini full-sm",
height: parseInt(element.getAttribute("data-height") || element.getAttribute("data-aksrt-height") || "0", 10) || 0,
mobileBottom: element.getAttribute("mobile-bottom") !== "false",
bottom: element.getAttribute("data-bottom") === "true",
kind: element.getAttribute("data-modal-kind") || "generic",
cacheKey: element.getAttribute("data-modal-cache-key") || "",
cacheTtl: ttl > 0 ? ttl : 15000,
cacheTags: tags,
});
}
document.addEventListener("click", function (e) {
var trigger = e.target.closest(
'[data-toggle="AksrtRemoteModal"], [data-toggle="RefreshModal"], [data-aksrt-remote-modal], a.aksrt-remote-modal-link'
);
if (!trigger) return;
if (trigger.getAttribute("data-aksrt-remote-modal") === "off") return;
e.preventDefault();
fromTrigger(trigger);
});
function bind() {
if (window.__aksrtRemoteModalBound) return;
if (!runtime()) return;
window.__aksrtRemoteModalBound = true;
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && active) close();
});
document.addEventListener("click", function (event) {
var trigger = event.target.closest(
'[data-toggle="AksrtRemoteModal"], [data-toggle="RefreshModal"], [data-aksrt-remote-modal], a.aksrt-remote-modal-link'
);
if (!trigger || trigger.getAttribute("data-aksrt-remote-modal") === "off") return;
event.preventDefault();
fromTrigger(trigger);
});
}
window.AksrtRemoteModal = {
open: open,
openRemote: openRemote,
close: close,
fromTrigger: fromTrigger,
getActive: function () {
return active;
},
};
function expose() {
var modalRuntime = runtime();
if (!modalRuntime) return;
window.AksrtRemoteModal = {
open: modalRuntime.open,
openRemote: modalRuntime.openRemote,
close: modalRuntime.close,
refresh: modalRuntime.refresh,
fromTrigger: fromTrigger,
getActive: modalRuntime.getActive,
};
bind();
}
expose();
if (!window.AksrtModalRuntime) {
document.addEventListener("aksrt:modal-runtime:ready", expose, { once: true });
}
})(window, document);