219 lines
6.4 KiB
JavaScript
219 lines
6.4 KiB
JavaScript
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:";
|
||
}
|
||
|
||
/**
|
||
* 请求父页面打开 URL(iframe 内 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;
|
||
}
|
||
};
|
||
}
|