Files
multica/packages/views/editor/utils/iframe-fragment-nav.ts
Naiyuan Qing c49c78b780 fix(editor): make in-iframe #fragment links scroll in HTML attachment preview (MUL-2417) (#2855)
HTML attachment previews mount the document inside a sandboxed
`<iframe srcdoc>` deliberately WITHOUT `allow-same-origin` — uploads are
untrusted user content. Chromium treats fragment-link clicks inside such an
opaque-origin srcdoc iframe as cross-origin frame navigation and silently
rejects them, so clicking a TOC entry never scrolls.

Append a tiny shim script to the srcdoc that intercepts `<a href="#...">`
clicks inside the iframe and calls `scrollIntoView` directly. The shim runs
in the iframe's own opaque origin under `allow-scripts` — no new
capabilities, no sandbox token changes; it cannot reach parent / cookies /
localStorage.

All three HTML attachment surfaces share the same helper:
  - inline 480px card  (html-attachment-preview.tsx)
  - full-screen modal  (attachment-preview-modal.tsx)
  - full-page route    (attachment-preview-page.tsx)

References: whatwg/html#3537, crbug 40191760.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 16:44:55 +08:00

53 lines
2.3 KiB
TypeScript

/**
* Fragment-navigation shim for sandboxed HTML attachment iframes.
*
* HTML attachment previews mount the user-supplied document inside a
* `<iframe sandbox="allow-scripts" srcdoc={...}>` — deliberately WITHOUT
* `allow-same-origin`, because the source is untrusted user upload and
* same-origin would let it reach cookies / localStorage / parent.document.
*
* That security posture has a side effect: in Chromium, a sandboxed srcdoc
* iframe sits in an opaque origin, and the browser treats clicks on
* `<a href="#section">` as cross-origin frame navigation — silently rejected,
* no scroll, no error. See whatwg/html#3537 and crbug 40191760; it's a spec +
* implementation consensus, not a bug we can wait out.
*
* The fix is in-iframe: append a tiny script to the document that listens for
* fragment-link clicks and calls `scrollIntoView` itself. The script runs in
* the iframe's own opaque origin — same capabilities the user's HTML already
* has under `allow-scripts`; it cannot reach the parent. The shim only
* intercepts `href="#..."` clicks, defers to any preventDefault handler the
* user's HTML installed, and stays out of the way when the target id is
* missing (so SPA / tab-style routers in the document can still handle it).
*/
const FRAGMENT_NAV_SHIM = `<script>
(function(){
document.addEventListener('click', function(e) {
if (e.defaultPrevented) return;
var t = e.target;
if (!t || typeof t.closest !== 'function') return;
var a = t.closest('a[href]');
if (!a) return;
var href = a.getAttribute('href');
if (!href || href.charAt(0) !== '#' || href === '#') return;
var id;
try { id = decodeURIComponent(href.slice(1)); } catch (_) { return; }
if (!id) return;
var dest = document.getElementById(id);
if (!dest && typeof CSS !== 'undefined' && CSS.escape) {
dest = document.querySelector('a[name="' + CSS.escape(id) + '"]');
}
if (!dest) return;
e.preventDefault();
dest.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
})();
</script>`;
export function withFragmentNavShim(html: string | undefined): string {
return (html ?? "") + FRAGMENT_NAV_SHIM;
}
/** Exposed for unit tests so they can assert the shim was appended verbatim. */
export const __FRAGMENT_NAV_SHIM__ = FRAGMENT_NAV_SHIM;