This commit is contained in:
iqudoo
2026-06-05 17:22:32 +08:00
commit eb4e8f1a04
90 changed files with 21224 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
import { URL_EMBED_MSG } from "./urlEmbedMessages.js";
import { isWeChatBrowser } from "./urlEmbedEnv.js";
import {
cancelUrlEmbedClientFocusBurst,
restoreUrlEmbedClientFocus,
scheduleUrlEmbedClientRestoreForEnv,
} from "./urlEmbedWechatFocus.js";
export {
restoreUrlEmbedClientFocus,
scheduleUrlEmbedClientRestoreForEnv,
} from "./urlEmbedWechatFocus.js";
function resolveUrl(url) {
try {
return new URL(url, typeof window !== "undefined" ? window.location.href : undefined);
} catch {
return null;
}
}
/** 是否运行在 urlEmbed 抽屉 iframe 内 */
export function isUrlEmbedFrame() {
if (typeof window === "undefined") return false;
try {
return window.parent !== window;
} catch {
return true;
}
}
function isSafeHttpUrl(url) {
const parsed = resolveUrl(url);
if (!parsed) return false;
return parsed.protocol === "http:" || parsed.protocol === "https:";
}
/**
* 请求父页面打开 URLiframe 内 window.open / target=_blank 常被拦截时使用)
* @param {string} url
* @param {{
* title?: string,
* showToolbar?: boolean,
* openInNewWindow?: boolean,
* showNewWindowButton?: boolean,
* }} [options]
* @returns {boolean} 是否已发出请求或已在顶层直接打开
*/
export function requestOpenUrl(url, options = {}) {
const nextUrl = typeof url === "string" ? url.trim() : "";
if (!nextUrl || !isSafeHttpUrl(nextUrl)) return false;
if (!isUrlEmbedFrame()) {
if (options.openInNewWindow === true) {
window.open(nextUrl, "_blank", "noopener,noreferrer");
return true;
}
window.location.href = nextUrl;
return true;
}
window.parent.postMessage(
{
type: URL_EMBED_MSG.OPEN_URL,
url: nextUrl,
title: options.title,
showToolbar: options.showToolbar,
openInNewWindow: options.openInNewWindow,
showNewWindowButton: options.showNewWindowButton,
},
"*",
);
return true;
}
function handleNavMessage(data, onNav) {
if (data.type !== URL_EMBED_MSG.NAV || typeof onNav !== "function") return;
if (data.action === "back") {
onNav("back");
return;
}
if (data.action === "forward") {
onNav("forward");
}
}
function createLinkClickHandler(getOpenOptions) {
return function onDocumentClick(ev) {
const anchor = ev.target?.closest?.("a[href]");
if (!anchor || !isUrlEmbedFrame()) return;
const href = anchor.getAttribute("href");
if (!href || href.startsWith("#") || href.startsWith("javascript:")) return;
const target = (anchor.getAttribute("target") || "").toLowerCase();
const opensExternally =
target === "_blank" || target === "_top" || target === "_parent";
if (!opensExternally) return;
const url = anchor.href;
if (!isSafeHttpUrl(url)) return;
ev.preventDefault();
ev.stopPropagation();
const extra = typeof getOpenOptions === "function" ? getOpenOptions(anchor) : {};
requestOpenUrl(url, {
openInNewWindow: target === "_blank" ? true : undefined,
...extra,
});
};
}
/**
* 嵌入页初始化:响应父页导航查询,并可选拦截链接 / window.open
* @param {{
* onNav?: (action: 'back' | 'forward') => void,
* interceptLinks?: boolean,
* interceptWindowOpen?: boolean,
* getOpenOptions?: (anchor: HTMLAnchorElement) => Record<string, unknown>,
* }} [options]
* @returns {() => void} 卸载监听
*/
export function initUrlEmbedClient(options = {}) {
if (typeof window === "undefined") return () => {};
const onNav = options.onNav;
const getOpenOptions = options.getOpenOptions;
function onWindowMessage(ev) {
const data = ev.data;
if (!data || typeof data !== "object") return;
if (data.type === URL_EMBED_MSG.QUERY_NAV) {
window.parent.postMessage({ type: URL_EMBED_MSG.NAV_AVAILABLE }, "*");
return;
}
if (data.type === URL_EMBED_MSG.RESTORE_FOCUS) {
scheduleUrlEmbedClientRestoreForEnv();
return;
}
handleNavMessage(data, onNav);
}
window.addEventListener("message", onWindowMessage);
if (isUrlEmbedFrame()) {
window.parent.postMessage({ type: URL_EMBED_MSG.NAV_AVAILABLE }, "*");
}
let removeTouchStartListener = () => {};
if (isWeChatBrowser() && document.body) {
const onTouchStart = () => {};
document.body.addEventListener("touchstart", onTouchStart, { passive: true, capture: true });
removeTouchStartListener = () => {
document.body.removeEventListener("touchstart", onTouchStart, { capture: true });
};
}
let removeWechatLifecycleListeners = () => {};
if (isWeChatBrowser() && isUrlEmbedFrame()) {
function onClientPageShow() {
scheduleUrlEmbedClientRestoreForEnv();
}
function onClientPopState() {
scheduleUrlEmbedClientRestoreForEnv();
}
function onClientVisibilityChange() {
if (document.visibilityState !== "visible") return;
scheduleUrlEmbedClientRestoreForEnv();
}
window.addEventListener("pageshow", onClientPageShow);
window.addEventListener("popstate", onClientPopState);
document.addEventListener("visibilitychange", onClientVisibilityChange);
removeWechatLifecycleListeners = () => {
window.removeEventListener("pageshow", onClientPageShow);
window.removeEventListener("popstate", onClientPopState);
document.removeEventListener("visibilitychange", onClientVisibilityChange);
cancelUrlEmbedClientFocusBurst();
};
}
let removeLinkListener = () => {};
if (options.interceptLinks === true) {
const onClick = createLinkClickHandler(getOpenOptions);
document.addEventListener("click", onClick, true);
removeLinkListener = () => document.removeEventListener("click", onClick, true);
}
let restoreWindowOpen = null;
if (options.interceptWindowOpen === true && isUrlEmbedFrame()) {
const nativeOpen = window.open.bind(window);
restoreWindowOpen = nativeOpen;
window.open = (url, target, features) => {
const nextUrl = typeof url === "string" ? url.trim() : "";
if (nextUrl && isSafeHttpUrl(nextUrl)) {
requestOpenUrl(nextUrl, { openInNewWindow: true });
return null;
}
return nativeOpen(url, target, features);
};
}
return function disposeUrlEmbedClient() {
window.removeEventListener("message", onWindowMessage);
removeTouchStartListener();
removeWechatLifecycleListeners();
removeLinkListener();
if (restoreWindowOpen) {
window.open = restoreWindowOpen;
}
};
}