Spaces:
Paused
Paused
File size: 5,480 Bytes
55bd140 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
<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}
|