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, * }} [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; } }; }