init
This commit is contained in:
218
src/iview/urlEmbed/urlEmbedClient.js
Normal file
218
src/iview/urlEmbed/urlEmbedClient.js
Normal 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:";
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求父页面打开 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user