Spaces:
Paused
Paused
| <script lang="ts"> | |
| import { onDestroy, onMount, tick } from 'svelte'; | |
| // Props | |
| export let src: string | null = null; // URL or raw HTML (auto-detected) | |
| export let title = 'Embedded Content'; | |
| export let initialHeight: number | null = null; // initial height in px, null = auto | |
| export let iframeClassName = 'w-full rounded-2xl'; | |
| export let args = null; | |
| export let allowScripts = true; | |
| export let allowForms = false; | |
| export let allowSameOrigin = false; // set to true only when you trust the content | |
| export let allowPopups = false; | |
| export let allowDownloads = true; | |
| export let referrerPolicy: HTMLIFrameElement['referrerPolicy'] = | |
| 'strict-origin-when-cross-origin'; | |
| export let allowFullscreen = true; | |
| let iframe: HTMLIFrameElement | null = null; | |
| let iframeSrc: string | null = null; | |
| let iframeDoc: string | null = null; | |
| // Derived: build sandbox attribute from flags | |
| $: sandbox = | |
| [ | |
| allowScripts && 'allow-scripts', | |
| allowForms && 'allow-forms', | |
| allowSameOrigin && 'allow-same-origin', | |
| allowPopups && 'allow-popups', | |
| allowDownloads && 'allow-downloads' | |
| ] | |
| .filter(Boolean) | |
| .join(' ') || undefined; | |
| // Detect URL vs raw HTML and prep src/srcdoc | |
| $: isUrl = typeof src === 'string' && /^(https?:)?\/\//i.test(src); | |
| $: if (src) { | |
| setIframeSrc(); | |
| } | |
| const setIframeSrc = async () => { | |
| await tick(); | |
| if (isUrl) { | |
| iframeSrc = src as string; | |
| iframeDoc = null; | |
| } else { | |
| iframeDoc = await processHtmlForDeps(src as string); | |
| iframeSrc = null; | |
| } | |
| }; | |
| // Alpine directives detection | |
| const alpineDirectives = [ | |
| 'x-data', | |
| 'x-init', | |
| 'x-show', | |
| 'x-bind', | |
| 'x-on', | |
| 'x-text', | |
| 'x-html', | |
| 'x-model', | |
| 'x-modelable', | |
| 'x-ref', | |
| 'x-for', | |
| 'x-if', | |
| 'x-effect', | |
| 'x-transition', | |
| 'x-cloak', | |
| 'x-ignore', | |
| 'x-teleport', | |
| 'x-id' | |
| ]; | |
| async function processHtmlForDeps(html: string): Promise<string> { | |
| if (!allowSameOrigin) return html; | |
| const scriptTags: string[] = []; | |
| // --- Alpine.js detection & injection --- | |
| const hasAlpineDirectives = alpineDirectives.some((dir) => html.includes(dir)); | |
| if (hasAlpineDirectives) { | |
| try { | |
| const { default: alpineCode } = await import('alpinejs/dist/cdn.min.js?raw'); | |
| const alpineBlob = new Blob([alpineCode], { type: 'text/javascript' }); | |
| const alpineUrl = URL.createObjectURL(alpineBlob); | |
| const alpineTag = `<script src="${alpineUrl}" defer><\/script>`; | |
| scriptTags.push(alpineTag); | |
| } catch (error) { | |
| console.error('Error processing Alpine for iframe:', error); | |
| } | |
| } | |
| // --- Chart.js detection & injection --- | |
| const chartJsDirectives = ['new Chart(', 'Chart.']; | |
| const hasChartJsDirectives = chartJsDirectives.some((dir) => html.includes(dir)); | |
| if (hasChartJsDirectives) { | |
| try { | |
| // import chartUrl from 'chart.js/auto?url'; | |
| const { default: Chart } = await import('chart.js/auto'); | |
| (window as any).Chart = Chart; | |
| const chartTag = `<script> | |
| window.Chart = parent.Chart; // Chart previously assigned on parent | |
| <\/script>`; | |
| scriptTags.push(chartTag); | |
| } catch (error) { | |
| console.error('Error processing Chart.js for iframe:', error); | |
| } | |
| } | |
| // If nothing to inject, return original HTML | |
| if (scriptTags.length === 0) return html; | |
| const tags = scriptTags.join('\n'); | |
| // Prefer injecting into <head>, then before </body>, otherwise prepend | |
| if (html.includes('</head>')) { | |
| return html.replace('</head>', `${tags}\n</head>`); | |
| } | |
| if (html.includes('</body>')) { | |
| return html.replace('</body>', `${tags}\n</body>`); | |
| } | |
| return `${tags}\n${html}`; | |
| } | |
| // Try to measure same-origin content safely | |
| function resizeSameOrigin() { | |
| if (!iframe) return; | |
| try { | |
| const doc = iframe.contentDocument || iframe.contentWindow?.document; | |
| console.log('iframe doc:', doc); | |
| if (!doc) return; | |
| const h = Math.max(doc.documentElement?.scrollHeight ?? 0, doc.body?.scrollHeight ?? 0); | |
| if (h > 0) iframe.style.height = h + 20 + 'px'; | |
| } catch { | |
| // Cross-origin → rely on postMessage from inside the iframe | |
| } | |
| } | |
| // Handle height messages from the iframe (we also verify the sender) | |
| function onMessage(e: MessageEvent) { | |
| if (!iframe || e.source !== iframe.contentWindow) return; | |
| const data = e.data as { type?: string; height?: number }; | |
| if (data?.type === 'iframe:height' && typeof data.height === 'number') { | |
| iframe.style.height = Math.max(0, data.height) + 'px'; | |
| } | |
| } | |
| // When the iframe loads, try same-origin resize (cross-origin will noop) | |
| const onLoad = async () => { | |
| requestAnimationFrame(resizeSameOrigin); | |
| // if arguments are provided, inject them into the iframe window | |
| if (args && iframe?.contentWindow) { | |
| (iframe.contentWindow as any).args = args; | |
| } | |
| }; | |
| // Ensure event listener bound only while component lives | |
| onMount(() => { | |
| window.addEventListener('message', onMessage); | |
| }); | |
| onDestroy(() => { | |
| window.removeEventListener('message', onMessage); | |
| }); | |
| </script> | |
| {#if iframeDoc} | |
| <iframe | |
| bind:this={iframe} | |
| srcdoc={iframeDoc} | |
| {title} | |
| class={iframeClassName} | |
| style={`${initialHeight ? `height:${initialHeight}px;` : ''}`} | |
| width="100%" | |
| frameborder="0" | |
| {sandbox} | |
| {allowFullscreen} | |
| on:load={onLoad} | |
| /> | |
| {:else if iframeSrc} | |
| <iframe | |
| bind:this={iframe} | |
| src={iframeSrc} | |
| {title} | |
| class={iframeClassName} | |
| style={`${initialHeight ? `height:${initialHeight}px;` : ''}`} | |
| width="100%" | |
| frameborder="0" | |
| {sandbox} | |
| referrerpolicy={referrerPolicy} | |
| {allowFullscreen} | |
| on:load={onLoad} | |
| /> | |
| {/if} | |