// --------------------------------------------------------------------------- // Shared embed document builder // // Single source of truth for the srcdoc injected into every embed iframe, // used by both the editor (live preview) and the publisher (static HTML). // // Exports: // - buildEmbedSrcdoc(html, opts) – assemble the full HTML doc // - EMBED_MESSAGE_TYPES – { resize, setTheme, updatePrimaryColor } // - DEFAULT_EMBED_HEIGHT – fallback pixel height when none stored // // Why one shared builder? // Before, the editor and publisher each had their own COLOR_PALETTES / height // reporter / theme listener, with subtle divergences (e.g. only the publisher // supported setTheme hot-swap). Keeping them in sync by hand was a footgun. // --------------------------------------------------------------------------- export const DEFAULT_EMBED_HEIGHT = 400; export const EMBED_MESSAGE_TYPES = { resize: "embedResize", setTheme: "setTheme", updatePrimaryColor: "updatePrimaryColor", ready: "embedReady", } as const; /** ColorPalettes polyfill – mirrors the research-article-template API. */ const COLOR_PALETTES_POLYFILL = ` (function(){ var palettes = { categorical: ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ac'], sequential: ['#084594','#2171b5','#4292c6','#6baed6','#9ecae1','#c6dbef','#deebf7','#f7fbff'], diverging: ['#d73027','#f46d43','#fdae61','#fee090','#ffffbf','#e0f3f8','#abd9e9','#74add1','#4575b4'] }; window.ColorPalettes = { getColors: function(type, n) { var p = palettes[type] || palettes.categorical; return p.slice(0, n != null ? n : p.length); }, getPrimary: function() { return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#4e79a7'; }, refresh: function() {}, getTextStyleForBackground: function() { return { fill: '#ffffff' }; } }; })(); `.trim(); /** * Height reporter: * - Observes documentElement + body (ResizeObserver) to catch all reflows * - Observes body's subtree (MutationObserver) to catch async chart mounts * - Throttles via requestAnimationFrame and only posts when delta >= 1px * - Safety retries at load, +300ms, +1000ms, +3000ms for slow async charts * - Exposes window.__embedReady(optionalHeight) so charts can opt-in to a * deterministic "I'm done" signal (useful for CSV fetches > 3s) */ const HEIGHT_REPORTER = ` (function(){ var lastH = 0; var scheduled = false; // IMPORTANT: only measure
. document.documentElement.scrollHeight is // clamped to at least the iframe's viewport height, so it would never allow // the iframe to shrink (it would always report >= current iframe height). // body.scrollHeight reflects the actual content height plus body padding. function measure(){ var b = document.body; if (!b) return 0; // getBoundingClientRect().bottom accounts for padding/margin precisely; // fall back to scrollHeight if the rect reports 0 (detached layout). var rectBottom = Math.ceil(b.getBoundingClientRect().bottom); var scroll = Math.ceil(b.scrollHeight); // body margin may push the rect down; scrollHeight ignores it. Take the // max so we never under-report. return Math.max(rectBottom, scroll); } function post(h){ try { window.parent.postMessage({ type: 'embedResize', height: h }, '*'); } catch(e){} } function schedule(){ if (scheduled) return; scheduled = true; requestAnimationFrame(function(){ scheduled = false; var h = measure(); if (h > 0 && Math.abs(h - lastH) >= 1) { lastH = h; post(h); } }); } if (typeof ResizeObserver !== 'undefined') { var ro = new ResizeObserver(schedule); ro.observe(document.documentElement); if (document.body) ro.observe(document.body); } if (typeof MutationObserver !== 'undefined' && document.body) { var mo = new MutationObserver(schedule); mo.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true }); } window.addEventListener('load', function(){ schedule(); setTimeout(schedule, 300); setTimeout(schedule, 1000); setTimeout(schedule, 3000); }); // Opt-in explicit ready signal for async charts window.__embedReady = function(){ schedule(); try { window.parent.postMessage({ type: 'embedReady' }, '*'); } catch(e){} }; })(); `.trim(); /** * Synchronous pre-paint theme bootstrap. * * Runs BEFORE the iframe's own `, `