2026-09-22 12:50:28 +08:00
|
|
|
|
// packages/embed-loader/src/loader-v2.js
|
|
|
|
|
|
// PhotoLive 嵌入组件加载器 v2(UI-04 vertical-stream 照片流窗)——对外发布物
|
|
|
|
|
|
// (esbuild 打包为 dist/v2/loader.js,宿主以 <script src="{EMBED_ORIGIN}/
|
|
|
|
|
|
// embed/v2/loader.js" data-slot> 引入)。与 v1(src/loader.js)同约束:零依赖、
|
|
|
|
|
|
// 零全局(仅版本标记属性)、无 console、一切错误静默不抛(A11 不破坏宿主)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// v2 与 v1 的宿主侧差异(方案 §2/§7/§8):
|
|
|
|
|
|
// · 窗口硬性固定 195×280 CSS px(border-box,含 1px 边框);PC/手机同值;
|
|
|
|
|
|
// 仅在视口不足时按 min(195,可用宽)/min(280,可用高) 收缩,绝不 transform
|
|
|
|
|
|
// 缩放;resize 消息不改高度(iframe 内容自适应窗口,窗口不迁就内容);
|
|
|
|
|
|
// · 外壳:圆角 12、边框 1px rgba(255,255,255,0.80)(单圈)、阴影
|
|
|
|
|
|
// 0 4px 16px rgba(15,23,42,0.16)、底色 #10161E(加载/缺口不露宿主内容);
|
|
|
|
|
|
// · 默认停靠:桌面 right 16 / 手机 right 12,bottom 24 + safe-area-inset;
|
|
|
|
|
|
// offsetX 仅在 data-* 与后台都未配置时按视口取默认;
|
|
|
|
|
|
// · 收起把手:白底胶囊 88–104×44、圆角 22、蓝图标 + 12px「照片/Photos」;
|
|
|
|
|
|
// · 多槽同侧纵排间距 12;可用高度不足时后续槽收起(空间约束≠用户选择);
|
|
|
|
|
|
// · 窄屏(<480px)默认收起沿用 v1 策略;展开后两端尺寸一致(195×280)。
|
|
|
|
|
|
// 收起按钮本体在 iframe 内(viewer EmbedStream 渲染,§2.4),经 collapse 消息
|
|
|
|
|
|
// 请求宿主收起——宿主与 iframe 各自清理,语义同 v1。
|
|
|
|
|
|
import {
|
|
|
|
|
|
parseEmbedConfigV2,
|
|
|
|
|
|
buildEmbedUrlV2,
|
|
|
|
|
|
mergeBackendConfigV2,
|
|
|
|
|
|
parseConfigMessageV2,
|
|
|
|
|
|
MESSAGE_TYPES,
|
|
|
|
|
|
V2_WIDTH,
|
|
|
|
|
|
V2_HEIGHT,
|
|
|
|
|
|
} from './params.js';
|
|
|
|
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
try {
|
|
|
|
|
|
var w = window;
|
|
|
|
|
|
// v2 独立标记:同页引入 v1+v2(迁移期)互不干扰。
|
|
|
|
|
|
if (w.__photoliveEmbedV2) return;
|
|
|
|
|
|
w.__photoliveEmbedV2 = '1';
|
|
|
|
|
|
|
|
|
|
|
|
var COLLAPSE_KEY = 'pl-embed-collapsed:'; // 与 v1 共用(同槽同会话记忆一致)
|
|
|
|
|
|
var STACK_GAP = 12; // §8.1 多槽同侧纵排间距
|
|
|
|
|
|
var HANDLE_MIN = 44; // 收起把手高/最小宽(§2.4)
|
|
|
|
|
|
// 收缩下限:低于此尺寸无法保证照片窗可用 → 改收起入口(§2.1「小到无法
|
|
|
|
|
|
// 操作时改成收起入口,而不是缩成不可点」)。
|
|
|
|
|
|
var MIN_OPERABLE_WIDTH = 150;
|
|
|
|
|
|
var MIN_OPERABLE_HEIGHT = 200;
|
|
|
|
|
|
|
|
|
|
|
|
function readCollapsed(slot) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return w.sessionStorage.getItem(COLLAPSE_KEY + slot) === '1';
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
function writeCollapsed(slot, collapsed) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (collapsed) w.sessionStorage.setItem(COLLAPSE_KEY + slot, '1');
|
|
|
|
|
|
else w.sessionStorage.removeItem(COLLAPSE_KEY + slot);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* storage 异常:静默放弃记忆 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
function hasChoice(slot) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return w.sessionStorage.getItem(COLLAPSE_KEY + slot) !== null;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selfScript() {
|
|
|
|
|
|
if (document.currentScript && document.currentScript.src) return document.currentScript;
|
|
|
|
|
|
var all = document.querySelectorAll('script[src*="/embed/v2/loader.js"]');
|
|
|
|
|
|
return all.length ? all[all.length - 1] : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function originOf(el) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new URL(el.src, w.location.href).origin;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var instances = [];
|
|
|
|
|
|
var seenSlots = {};
|
|
|
|
|
|
var relayoutFrame = null;
|
|
|
|
|
|
|
|
|
|
|
|
function isZh(lang) {
|
|
|
|
|
|
if (lang === 'zh-CN') return true;
|
|
|
|
|
|
if (lang === 'auto') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return /^zh/i.test(w.navigator.language || '');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 17:38:02 +08:00
|
|
|
|
// 收起把手(§2.4;效果示例 .handle 精确值):100×44 白底胶囊、圆角 22、
|
|
|
|
|
|
// 1px #e3eaf2 边、蓝图标 16 + 12px 文案;全部 inline style(零宿主污染)。
|
2026-09-22 12:50:28 +08:00
|
|
|
|
function createHandle(inst) {
|
|
|
|
|
|
var h = document.createElement('div');
|
|
|
|
|
|
h.setAttribute('role', 'button');
|
|
|
|
|
|
h.setAttribute('tabindex', '0');
|
|
|
|
|
|
h.setAttribute('aria-label', isZh(inst.config.lang) ? '展开照片窗' : 'Show photo window');
|
|
|
|
|
|
var s = h.style;
|
|
|
|
|
|
s.position = 'fixed';
|
|
|
|
|
|
s.height = HANDLE_MIN + 'px';
|
2026-09-22 17:38:02 +08:00
|
|
|
|
s.width = '100px';
|
2026-09-22 12:50:28 +08:00
|
|
|
|
s.boxSizing = 'border-box';
|
|
|
|
|
|
s.borderRadius = '22px';
|
2026-09-22 17:38:02 +08:00
|
|
|
|
s.border = '1px solid #e3eaf2';
|
2026-09-22 12:50:28 +08:00
|
|
|
|
s.background = '#ffffff';
|
2026-09-22 17:38:02 +08:00
|
|
|
|
s.boxShadow = '0 4px 16px rgba(15,23,42,0.125)';
|
2026-09-22 12:50:28 +08:00
|
|
|
|
s.zIndex = String(inst.config.zIndex);
|
|
|
|
|
|
s.cursor = 'pointer';
|
|
|
|
|
|
s.display = 'flex';
|
|
|
|
|
|
s.alignItems = 'center';
|
|
|
|
|
|
s.justifyContent = 'center';
|
2026-09-22 17:38:02 +08:00
|
|
|
|
s.gap = '8px';
|
|
|
|
|
|
s.font = '400 12px/1 system-ui, -apple-system, "Segoe UI", "PingFang SC", sans-serif';
|
|
|
|
|
|
s.color = '#32618e';
|
2026-09-22 12:50:28 +08:00
|
|
|
|
s.whiteSpace = 'nowrap';
|
|
|
|
|
|
h.innerHTML =
|
2026-09-22 17:38:02 +08:00
|
|
|
|
'<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false" style="color:#1a73e8">' +
|
|
|
|
|
|
'<path fill="currentColor" d="M9 2 7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/></svg>' +
|
2026-09-22 12:50:28 +08:00
|
|
|
|
'<span>' + (isZh(inst.config.lang) ? '照片' : 'Photos') + '</span>';
|
|
|
|
|
|
function activate(ev) {
|
|
|
|
|
|
if (ev.type === 'keydown') {
|
|
|
|
|
|
var k = ev.key;
|
|
|
|
|
|
if (k !== 'Enter' && k !== ' ' && k !== 'Spacebar') return;
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
inst.userChoice = true;
|
|
|
|
|
|
expandInstance(inst);
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (inst.frame.contentWindow) {
|
|
|
|
|
|
inst.frame.contentWindow.postMessage(
|
|
|
|
|
|
{ photoliveEmbed: true, type: 'expand' },
|
|
|
|
|
|
inst.frameOrigin
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* 静默 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
h.addEventListener('click', activate);
|
|
|
|
|
|
h.addEventListener('keydown', activate);
|
|
|
|
|
|
return h;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 展开态盒高(收缩后可能 < 280);收起态计把手 44(堆叠依据)。 */
|
|
|
|
|
|
function effectiveHeight(inst) {
|
|
|
|
|
|
return inst.collapsed ? HANDLE_MIN : inst.height;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 17:38:02 +08:00
|
|
|
|
/** 默认水平偏移:未配置时桌面 16 / ≤599px 视口 12(§2.1;效果示例
|
|
|
|
|
|
* media(max-width:599px) 档)。data-* 显式或后台配置到达后
|
|
|
|
|
|
* (offsetConfigured)按配置值。 */
|
2026-09-22 12:50:28 +08:00
|
|
|
|
function effectiveOffsetX(inst) {
|
|
|
|
|
|
if (inst.config.explicit.offsetX || inst.offsetConfigured) return inst.config.offsetX;
|
2026-09-22 17:38:02 +08:00
|
|
|
|
return w.innerWidth <= 599 ? 12 : 16;
|
2026-09-22 12:50:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** wrapper 几何:W=min(195,可用宽)、H=min(280,可用高);不足操作下限时
|
|
|
|
|
|
* 由调用方收起(空间约束)。 */
|
|
|
|
|
|
function measureBox(inst) {
|
|
|
|
|
|
var offsetX = effectiveOffsetX(inst);
|
|
|
|
|
|
var availW = w.innerWidth - offsetX - 8;
|
|
|
|
|
|
var availH = w.innerHeight - inst.config.offsetBottom - 8;
|
|
|
|
|
|
return {
|
|
|
|
|
|
offsetX: offsetX,
|
|
|
|
|
|
width: Math.max(120, Math.min(V2_WIDTH, availW)),
|
|
|
|
|
|
height: Math.max(160, Math.min(V2_HEIGHT, availH)),
|
|
|
|
|
|
operable: availW >= MIN_OPERABLE_WIDTH && availH >= MIN_OPERABLE_HEIGHT,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyWrapperBox(inst) {
|
|
|
|
|
|
var box = measureBox(inst);
|
|
|
|
|
|
inst.height = box.height;
|
|
|
|
|
|
var side = inst.config.position === 'left' ? 'left' : 'right';
|
|
|
|
|
|
var other = side === 'left' ? 'right' : 'left';
|
|
|
|
|
|
var ws = inst.wrapper.style;
|
|
|
|
|
|
if (!inst.collapsed) {
|
|
|
|
|
|
ws.display = 'block';
|
|
|
|
|
|
ws.width = box.width + 'px';
|
|
|
|
|
|
ws.height = box.height + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
ws.zIndex = String(inst.config.zIndex);
|
|
|
|
|
|
ws[side] = box.offsetX + 'px';
|
|
|
|
|
|
ws[other] = '';
|
|
|
|
|
|
// 底部偏移 + 安全区(§2.1):不支持 env() 的旧浏览器整条声明失效回退
|
|
|
|
|
|
// 上一行设置的裸像素值(先写裸值再写 calc,双保险)。
|
|
|
|
|
|
ws.bottom = inst.config.offsetBottom + 'px';
|
|
|
|
|
|
ws.bottom =
|
|
|
|
|
|
'calc(' + inst.config.offsetBottom + 'px + env(safe-area-inset-bottom, 0px))';
|
|
|
|
|
|
if (inst.handle) {
|
|
|
|
|
|
var hs = inst.handle.style;
|
|
|
|
|
|
hs.zIndex = String(inst.config.zIndex);
|
|
|
|
|
|
hs[side] = box.offsetX + 'px';
|
|
|
|
|
|
hs[other] = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
return box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同侧多槽纵排(§8.1):登记顺序自下而上,间距 12;可用高度不足时后续
|
|
|
|
|
|
// 槽收起(不写用户偏好)。
|
|
|
|
|
|
function relayout() {
|
|
|
|
|
|
var sides = { left: [], right: [] };
|
|
|
|
|
|
for (var i = 0; i < instances.length; i++) {
|
|
|
|
|
|
var inst = instances[i];
|
|
|
|
|
|
sides[inst.config.position === 'left' ? 'left' : 'right'].push(inst);
|
|
|
|
|
|
}
|
|
|
|
|
|
var keys = ['left', 'right'];
|
|
|
|
|
|
for (var s = 0; s < keys.length; s++) {
|
|
|
|
|
|
var list = sides[keys[s]];
|
|
|
|
|
|
var cursorBottom = 0;
|
|
|
|
|
|
for (var j = 0; j < list.length; j++) {
|
|
|
|
|
|
var it = list[j];
|
|
|
|
|
|
var bottom = j === 0 ? it.config.offsetBottom : cursorBottom + effectiveHeight(list[j - 1]) + STACK_GAP;
|
|
|
|
|
|
if (j > 0 && bottom + V2_HEIGHT > w.innerHeight) setCollapsed(it, false);
|
|
|
|
|
|
it.wrapper.style.bottom = bottom + 'px';
|
|
|
|
|
|
if (it.handle) it.handle.style.bottom = bottom + 'px';
|
|
|
|
|
|
cursorBottom = bottom;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleRelayout() {
|
|
|
|
|
|
if (relayoutFrame !== null || typeof w.requestAnimationFrame !== 'function') {
|
|
|
|
|
|
relayout();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
relayoutFrame = w.requestAnimationFrame(function () {
|
|
|
|
|
|
relayoutFrame = null;
|
|
|
|
|
|
relayout();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 收起核心(不触发重排调度——relayout 空间不足收起时避免再排)。 */
|
|
|
|
|
|
function setCollapsed(inst, persist) {
|
|
|
|
|
|
if (inst.collapsed) return;
|
|
|
|
|
|
inst.collapsed = true;
|
|
|
|
|
|
inst.wrapper.style.display = 'none';
|
|
|
|
|
|
if (!inst.handle) {
|
|
|
|
|
|
inst.handle = createHandle(inst);
|
|
|
|
|
|
document.body.appendChild(inst.handle);
|
2026-09-23 11:24:47 +08:00
|
|
|
|
// 首次创建即定位(side/offsetX/zIndex)——漏掉会使 fixed 把手失去
|
|
|
|
|
|
// 水平定位而跑向左侧(v1 同位有此调用;2026-09-23 服务器实测发现)。
|
|
|
|
|
|
applyWrapperBox(inst);
|
2026-09-22 12:50:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
inst.handle.style.display = 'flex';
|
|
|
|
|
|
if (persist !== false) writeCollapsed(inst.config.slot, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collapseInstance(inst, persist) {
|
|
|
|
|
|
setCollapsed(inst, persist);
|
|
|
|
|
|
scheduleRelayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function expandInstance(inst) {
|
|
|
|
|
|
if (!inst.collapsed) return;
|
|
|
|
|
|
inst.collapsed = false;
|
|
|
|
|
|
applyWrapperBox(inst);
|
|
|
|
|
|
if (inst.handle) inst.handle.style.display = 'none';
|
|
|
|
|
|
writeCollapsed(inst.config.slot, false);
|
|
|
|
|
|
scheduleRelayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 服务端终止展示(停用/解绑/404/410):移除照片窗与把手,不留空壳。 */
|
|
|
|
|
|
function destroyInstance(inst) {
|
|
|
|
|
|
if (inst.destroyed) return;
|
|
|
|
|
|
inst.destroyed = true;
|
|
|
|
|
|
if (inst.handle && inst.handle.parentNode) inst.handle.parentNode.removeChild(inst.handle);
|
|
|
|
|
|
if (inst.wrapper.parentNode) inst.wrapper.parentNode.removeChild(inst.wrapper);
|
|
|
|
|
|
try {
|
|
|
|
|
|
w.sessionStorage.removeItem(COLLAPSE_KEY + inst.config.slot);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* 静默 */
|
|
|
|
|
|
}
|
|
|
|
|
|
var i = instances.indexOf(inst);
|
|
|
|
|
|
if (i >= 0) instances.splice(i, 1);
|
|
|
|
|
|
delete seenSlots[inst.config.slot];
|
|
|
|
|
|
if (instances.length === 0) w.removeEventListener('message', onMessage);
|
|
|
|
|
|
scheduleRelayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 后台设置应用(config 消息已过 parseConfigMessageV2 版本闸):非 data-*
|
|
|
|
|
|
* 显式字段立即生效;collapsedDefault 仅在无用户选择时应用。 */
|
|
|
|
|
|
function applyConfig(inst, version, settings) {
|
|
|
|
|
|
inst.backendVersion = version;
|
|
|
|
|
|
inst.offsetConfigured = true; // 后台 settings 恒为全集 → offsetX 已「配置」
|
|
|
|
|
|
inst.config = mergeBackendConfigV2(inst.config, settings);
|
|
|
|
|
|
applyWrapperBox(inst);
|
|
|
|
|
|
if (!inst.userChoice && !hasChoice(inst.config.slot)) {
|
|
|
|
|
|
if (settings.collapsedDefault) collapseInstance(inst, false);
|
|
|
|
|
|
else if (!spaceConstrained(inst)) expandInstance(inst);
|
|
|
|
|
|
}
|
|
|
|
|
|
scheduleRelayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 当前视口是否空间不足(不足时默认收起而非塞下不可用的窗口)。 */
|
|
|
|
|
|
function spaceConstrained(inst) {
|
|
|
|
|
|
return !measureBox(inst).operable;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createInstance(origin, config) {
|
|
|
|
|
|
var wrapper = document.createElement('div');
|
|
|
|
|
|
var ws = wrapper.style;
|
|
|
|
|
|
ws.position = 'fixed';
|
|
|
|
|
|
ws.width = V2_WIDTH + 'px';
|
|
|
|
|
|
ws.height = V2_HEIGHT + 'px';
|
|
|
|
|
|
ws.borderRadius = '12px';
|
|
|
|
|
|
ws.border = '1px solid rgba(255,255,255,0.80)';
|
|
|
|
|
|
ws.boxShadow = '0 4px 16px rgba(15,23,42,0.16)';
|
|
|
|
|
|
ws.background = '#10161E';
|
|
|
|
|
|
ws.overflow = 'hidden';
|
2026-09-22 18:56:38 +08:00
|
|
|
|
// 敌意宿主全局 CSS 反制(EMB-A12:*{box-sizing:content-box!important} 等
|
|
|
|
|
|
// 会翻转 195×280 盒模型/给 iframe 加边框)——几何关键属性以 inline
|
|
|
|
|
|
// important 压制宿主样式表 important(inline important 优先级最高)。
|
|
|
|
|
|
ws.setProperty('box-sizing', 'border-box', 'important');
|
2026-09-22 12:50:28 +08:00
|
|
|
|
|
|
|
|
|
|
var frame = document.createElement('iframe');
|
|
|
|
|
|
// sandbox 同 v1(含 allow-same-origin:iframe 保有项目域源,白名单按
|
|
|
|
|
|
// 源+source 双重严格校验;跨域隔离与链接新页安全不变)。
|
|
|
|
|
|
frame.setAttribute(
|
|
|
|
|
|
'sandbox',
|
|
|
|
|
|
'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin'
|
|
|
|
|
|
);
|
|
|
|
|
|
frame.setAttribute('src', buildEmbedUrlV2(origin, config.slot, config));
|
|
|
|
|
|
frame.setAttribute('referrerpolicy', 'no-referrer-when-downgrade');
|
|
|
|
|
|
frame.setAttribute('title', 'PhotoLive photos');
|
|
|
|
|
|
var fs = frame.style;
|
|
|
|
|
|
fs.width = '100%';
|
|
|
|
|
|
fs.height = '100%';
|
|
|
|
|
|
fs.border = '0';
|
|
|
|
|
|
fs.display = 'block';
|
2026-09-22 18:56:38 +08:00
|
|
|
|
fs.setProperty('border', '0', 'important');
|
|
|
|
|
|
fs.setProperty('width', '100%', 'important');
|
|
|
|
|
|
fs.setProperty('height', '100%', 'important');
|
|
|
|
|
|
fs.setProperty('display', 'block', 'important');
|
2026-09-22 12:50:28 +08:00
|
|
|
|
wrapper.appendChild(frame);
|
|
|
|
|
|
document.body.appendChild(wrapper);
|
|
|
|
|
|
|
|
|
|
|
|
var inst = {
|
|
|
|
|
|
config: config,
|
|
|
|
|
|
wrapper: wrapper,
|
|
|
|
|
|
frame: frame,
|
|
|
|
|
|
frameOrigin: originOf(frame) || origin,
|
|
|
|
|
|
height: V2_HEIGHT,
|
|
|
|
|
|
collapsed: false,
|
|
|
|
|
|
narrow: config.narrow,
|
|
|
|
|
|
handle: null,
|
|
|
|
|
|
userChoice: false,
|
|
|
|
|
|
backendVersion: 0,
|
|
|
|
|
|
offsetConfigured: config.explicit.offsetX,
|
|
|
|
|
|
destroyed: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
instances.push(inst);
|
|
|
|
|
|
var box = applyWrapperBox(inst);
|
|
|
|
|
|
// 收起记忆 / 默认(窄屏或空间不足默认收起;用户选择优先,不反复弹出)。
|
|
|
|
|
|
if (hasChoice(config.slot)) {
|
|
|
|
|
|
if (readCollapsed(config.slot)) collapseInstance(inst, false);
|
|
|
|
|
|
} else if (inst.narrow || !box.operable) {
|
|
|
|
|
|
collapseInstance(inst, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
return inst;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// postMessage 白名单:同 v1 双重严格校验(event.source 命中已登记 iframe
|
|
|
|
|
|
// 且 event.origin 严格等于该 iframe 源)。v2 忽略 resize(窗口固定)。
|
|
|
|
|
|
function onMessage(event) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var data = event.data;
|
|
|
|
|
|
if (!data || data.photoliveEmbed !== true) return;
|
|
|
|
|
|
var type = data.type;
|
|
|
|
|
|
if (MESSAGE_TYPES.indexOf(type) < 0) return;
|
|
|
|
|
|
for (var i = 0; i < instances.length; i++) {
|
|
|
|
|
|
var inst = instances[i];
|
|
|
|
|
|
if (!inst.frame.contentWindow || event.source !== inst.frame.contentWindow) continue;
|
|
|
|
|
|
if (event.origin !== inst.frameOrigin) return;
|
|
|
|
|
|
if (type === 'collapse') {
|
|
|
|
|
|
inst.userChoice = true;
|
|
|
|
|
|
collapseInstance(inst, true);
|
|
|
|
|
|
} else if (type === 'expand') {
|
|
|
|
|
|
inst.userChoice = true;
|
|
|
|
|
|
expandInstance(inst);
|
|
|
|
|
|
} else if (type === 'config') {
|
|
|
|
|
|
var parsed = parseConfigMessageV2(data, inst.backendVersion);
|
|
|
|
|
|
if (parsed !== null) applyConfig(inst, parsed.configVersion, parsed.settings);
|
|
|
|
|
|
} else if (type === 'destroy') {
|
|
|
|
|
|
destroyInstance(inst);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 'resize':v2 窗口固定,显式忽略(§7.3)。
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* 静默 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isNarrow() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return !!w.matchMedia && w.matchMedia('(max-width: 479px)').matches;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onViewportChange() {
|
|
|
|
|
|
var narrow = isNarrow();
|
|
|
|
|
|
for (var i = 0; i < instances.length; i++) {
|
|
|
|
|
|
var inst = instances[i];
|
|
|
|
|
|
if (inst.narrow !== narrow) {
|
|
|
|
|
|
inst.narrow = narrow;
|
|
|
|
|
|
if (narrow && !inst.userChoice && !hasChoice(inst.config.slot)) {
|
|
|
|
|
|
collapseInstance(inst, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
applyWrapperBox(inst);
|
|
|
|
|
|
}
|
|
|
|
|
|
scheduleRelayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function init() {
|
|
|
|
|
|
var self = selfScript();
|
|
|
|
|
|
if (!self) return;
|
|
|
|
|
|
var embedOrigin = originOf(self);
|
|
|
|
|
|
if (!embedOrigin) return;
|
|
|
|
|
|
var narrow = isNarrow();
|
|
|
|
|
|
var tags = document.querySelectorAll('script[src*="/embed/v2/loader.js"]');
|
|
|
|
|
|
for (var i = 0; i < tags.length; i++) {
|
|
|
|
|
|
var config = parseEmbedConfigV2(tags[i].dataset, { narrow: narrow });
|
|
|
|
|
|
if (!config.slot) continue;
|
|
|
|
|
|
if (seenSlots[config.slot]) continue;
|
|
|
|
|
|
seenSlots[config.slot] = true;
|
|
|
|
|
|
createInstance(embedOrigin, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
w.addEventListener('message', onMessage);
|
|
|
|
|
|
w.addEventListener('resize', onViewportChange);
|
|
|
|
|
|
w.addEventListener('orientationchange', onViewportChange);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (document.body) {
|
|
|
|
|
|
init();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* 静默:任何异常不得破坏宿主页 */
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|