Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
64 lines
2.4 KiB
TypeScript
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"
|
|
/>
|
|
);
|
|
}
|