| | <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} |
| |
|