fix(html-embed): correct stale iframe height and damp the resize loop
Address the non-test code-review findings on the htmlEmbed sandbox change (test-coverage gaps are tracked in issue #99): - html-embed-view: track the iframe's reported content height even while a fixed height is set, so clearing the height (fixed -> auto) without editing the source no longer leaves the frame pinned to the stale value. Derive the fixed-height predicate once; seed autoHeight to the default. - html-embed-view: drop width/border from the iframe inline style (the .htmlEmbedFrame CSS class already provides them). - html-embed-sandbox: coalesce height reports via requestAnimationFrame and skip <=1px deltas to damp the self-measure feedback loop; fix the misleading bootstrap comment. - tracker-settings: add an aria-label to the snippet Textarea (a11y). - CHANGELOG: note the removal of server-side role-based HTML-embed stripping.
This commit is contained in:
@@ -22,6 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
when the workspace HTML-embed toggle is on, can be inserted by any member
|
||||
(previously admin-only). Turning the toggle off hides existing embeds and
|
||||
stops serving them on public share pages.
|
||||
- Remove the server-side role-based stripping of HTML-embed blocks from the
|
||||
write paths (collab/REST/MCP, page create/duplicate, import, transclusion
|
||||
unsync); sandboxing makes per-write gating unnecessary. The only remaining
|
||||
server-side strip is the public-share read path, which still honors the
|
||||
workspace HTML-embed toggle.
|
||||
|
||||
## [0.91.0] - 2026-06-18
|
||||
|
||||
|
||||
@@ -22,20 +22,42 @@ export function buildSandboxSrcdoc(source: string): string {
|
||||
const bootstrap = `
|
||||
<script>
|
||||
(function () {
|
||||
function reportHeight() {
|
||||
var lastSent = -1;
|
||||
var scheduled = false;
|
||||
function measure() {
|
||||
var doc = document.documentElement;
|
||||
var body = document.body;
|
||||
var height = Math.max(
|
||||
return Math.max(
|
||||
doc ? doc.scrollHeight : 0,
|
||||
body ? body.scrollHeight : 0
|
||||
);
|
||||
}
|
||||
function flush() {
|
||||
scheduled = false;
|
||||
var height = measure();
|
||||
// Only report when the height actually changed by more than 1px. This
|
||||
// damps the iframe self-measure feedback loop: content sized to the iframe
|
||||
// viewport would otherwise oscillate as the parent resizes the frame in
|
||||
// response to each report.
|
||||
if (Math.abs(height - lastSent) <= 1) return;
|
||||
lastSent = height;
|
||||
parent.postMessage(
|
||||
{ type: ${JSON.stringify(HTML_EMBED_HEIGHT_MESSAGE)}, height: height },
|
||||
"*"
|
||||
);
|
||||
}
|
||||
function reportHeight() {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
requestAnimationFrame(flush);
|
||||
} else {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
window.addEventListener("load", reportHeight);
|
||||
// Report immediately too, in case load already fired.
|
||||
// Report an initial height now (runs during parse, before load/images
|
||||
// settle); the load handler and ResizeObserver refine it as content changes.
|
||||
reportHeight();
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
try {
|
||||
|
||||
@@ -62,21 +62,24 @@ export default function HtmlEmbedView(props: NodeViewProps) {
|
||||
const [draft, setDraft] = useState<string>(source || "");
|
||||
const [draftHeight, setDraftHeight] = useState<number | "">(height ?? "");
|
||||
|
||||
// Auto-resize height tracked in state (used only when no fixed height is set).
|
||||
const [autoHeight, setAutoHeight] = useState<number>(
|
||||
typeof height === "number" && Number.isFinite(height)
|
||||
? height
|
||||
: DEFAULT_IFRAME_HEIGHT,
|
||||
);
|
||||
// True when the author pinned an explicit height; otherwise we auto-resize to
|
||||
// the iframe's reported content height.
|
||||
const hasFixedHeight = typeof height === "number" && Number.isFinite(height);
|
||||
|
||||
// Auto-resize height tracked in state. Seeded to the default and updated from
|
||||
// the iframe's postMessage reports (see effect below) regardless of mode, so
|
||||
// switching a fixed-height embed back to auto immediately reflects the last
|
||||
// reported content height instead of staying pinned to the old fixed value.
|
||||
const [autoHeight, setAutoHeight] = useState<number>(DEFAULT_IFRAME_HEIGHT);
|
||||
|
||||
const srcdoc = useMemo(() => buildSandboxSrcdoc(source || ""), [source]);
|
||||
|
||||
// Auto-resize: accept height messages ONLY from this iframe's own content
|
||||
// window. The sandboxed srcdoc has an opaque ("null") origin, so we cannot
|
||||
// match by event.origin — we match by event.source instead. No-op when a
|
||||
// fixed height is configured.
|
||||
// match by event.origin — we match by event.source instead. We track the
|
||||
// reported height even while a fixed height is in effect, so toggling back to
|
||||
// auto shows the current content height with no iframe reload.
|
||||
useEffect(() => {
|
||||
if (typeof height === "number" && Number.isFinite(height)) return;
|
||||
function onMessage(event: MessageEvent) {
|
||||
if (event.source !== iframeRef.current?.contentWindow) return;
|
||||
const data = event.data as { type?: string; height?: number };
|
||||
@@ -87,12 +90,9 @@ export default function HtmlEmbedView(props: NodeViewProps) {
|
||||
}
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => window.removeEventListener("message", onMessage);
|
||||
}, [height]);
|
||||
}, []);
|
||||
|
||||
const effectiveHeight =
|
||||
typeof height === "number" && Number.isFinite(height)
|
||||
? clampHeight(height)
|
||||
: autoHeight;
|
||||
const effectiveHeight = hasFixedHeight ? clampHeight(height) : autoHeight;
|
||||
|
||||
const openEditor = useCallback(() => {
|
||||
setDraft(source || "");
|
||||
@@ -157,7 +157,7 @@ export default function HtmlEmbedView(props: NodeViewProps) {
|
||||
srcDoc={srcdoc}
|
||||
title={t("HTML embed")}
|
||||
referrerPolicy="no-referrer"
|
||||
style={{ width: "100%", border: "none", height: effectiveHeight }}
|
||||
style={{ height: effectiveHeight }}
|
||||
/>
|
||||
) : canEdit ? (
|
||||
<div className={classes.htmlEmbedPlaceholder} onClick={openEditor}>
|
||||
|
||||
@@ -78,6 +78,7 @@ export default function TrackerSettings() {
|
||||
autosize
|
||||
minRows={6}
|
||||
maxRows={20}
|
||||
aria-label={t("Analytics / tracker")}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
placeholder={t("<script>...</script>")}
|
||||
|
||||
Reference in New Issue
Block a user