This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/packages/ui-kit/src/ViewContainer.tsx
T

64 lines
2.4 KiB
TypeScript

/**
* 视图区容器 —— **净化渲染,无脚本执行权**(编码规范 §9.2;语义出处 AM 07)。
*
* 「视图区容器渲染活动推送的 HTML:沙箱 iframe(srcdoc + sandbox)或净化后注入,
* 二选一由 `ui-kit` 的 `ViewContainer` 统一实现,**feature 不得自行 dangerouslySetInnerHTML`。」
*
* 这里选沙箱 iframe:结构上就没有脚本执行权,不依赖净化算法的完备性。
*
* **高度随内容长**:HTML 报告既是独立文件,也是交互视图的组成部分 ——
* 它得像页面里的一段正文那样整篇铺开、跟着外层一起滚,而不是缩在一个定高小窗里自己滚。
* 定高会把长报告拦腰截断,人得在两层滚动条之间来回找。
*/
import { useCallback, useEffect, useRef, useState } from 'react';
export type ViewContainerProps = {
readonly html: string;
readonly title?: string;
};
/** 量不到高度时的兜底,避免塌成 0 高 */
const FALLBACK_HEIGHT = 420;
export function ViewContainer({ html, title = '活动视图' }: ViewContainerProps) {
const ref = useRef<HTMLIFrameElement>(null);
const [height, setHeight] = useState(FALLBACK_HEIGHT);
const measure = useCallback(() => {
const doc = ref.current?.contentDocument;
if (doc?.documentElement == null) return;
// 取 body 与 documentElement 的较大者:两者谁撑开高度随文档结构而异
const next = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
if (next > 0) setHeight(next);
}, []);
useEffect(() => {
const frame = ref.current;
const doc = frame?.contentDocument;
if (doc?.body == null) return undefined;
// 图片late-load、字体回流都会改高度 —— 盯着 body 尺寸变化重量一次
const observer = new ResizeObserver(measure);
observer.observe(doc.body);
return () => observer.disconnect();
}, [measure, html]);
return (
<iframe
ref={ref}
className="jc-view-frame"
title={title}
/*
* `allow-same-origin` **只为让外层量到内容高度**(否则 iframe 是不透明源,
* contentDocument 读不到,高度无从得知)。
* 关键在于**始终不给 `allow-scripts`** —— 里面一行脚本也跑不了,
* 「无脚本执行权」这条不变。两者同时给才是破沙箱,这里永远不会。
*/
sandbox="allow-same-origin"
srcDoc={html}
style={{ height }}
onLoad={measure}
scrolling="no"
/>
);
}