| <script lang="ts"> |
| import { onDestroy, onMount, tick } from 'svelte'; |
|
|
| |
| export let src: string | null = null; |
| export let title = 'Embedded Content'; |
| export let initialHeight: number | null = null; |
|
|
| export let iframeClassName = 'w-full rounded-2xl'; |
|
|
| export let args = null; |
|
|
| export let allowScripts = true; |
| export let allowForms = false; |
|
|
| export let allowSameOrigin = false; |
| export let allowPopups = false; |
| export let allowDownloads = true; |
|
|
| export let referrerPolicy: HTMLIFrameElement['referrerPolicy'] = |
| 'strict-origin-when-cross-origin'; |
| export let allowFullscreen = true; |
|
|
| export let payload = null; |
|
|
| let iframe: HTMLIFrameElement | null = null; |
| let iframeSrc: string | null = null; |
| let iframeDoc: string | null = null; |
|
|
| |
| $: sandbox = |
| [ |
| allowScripts && 'allow-scripts', |
| allowForms && 'allow-forms', |
| allowSameOrigin && 'allow-same-origin', |
| allowPopups && 'allow-popups', |
| allowDownloads && 'allow-downloads' |
| ] |
| .filter(Boolean) |
| .join(' ') || undefined; |
|
|
| |
| $: 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; |
| } |
| }; |
|
|
| |
| 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[] = []; |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| const chartJsDirectives = ['new Chart(', 'Chart.']; |
| const hasChartJsDirectives = chartJsDirectives.some((dir) => html.includes(dir)); |
| if (hasChartJsDirectives) { |
| try { |
| |
| 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 (scriptTags.length === 0) return html; |
|
|
| const tags = scriptTags.join('\n'); |
|
|
| |
| 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}`; |
| } |
|
|
| |
| 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 { |
| |
| } |
| } |
|
|
| function onMessage(e: MessageEvent) { |
| if (!iframe || e.source !== iframe.contentWindow) return; |
|
|
| const data = e.data || {}; |
| if (data?.type === 'iframe:height' && typeof data.height === 'number') { |
| iframe.style.height = Math.max(0, data.height) + 'px'; |
| } |
|
|
| |
| if (data?.type === 'pong') { |
| console.log('Received pong from iframe:', data); |
|
|
| |
| iframe.contentWindow?.postMessage({ type: 'pong:ack' }, '*'); |
| } |
|
|
| |
| if (data?.type === 'payload') { |
| iframe.contentWindow?.postMessage( |
| { type: 'payload', requestId: data?.requestId ?? null, payload: payload }, |
| '*' |
| ); |
| } |
| } |
|
|
| |
| const onLoad = async () => { |
| requestAnimationFrame(resizeSameOrigin); |
|
|
| |
| if (args && iframe?.contentWindow) { |
| (iframe.contentWindow as any).args = args; |
| } |
| }; |
|
|
| |
| 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} |
|
|