mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
145 lines
4.1 KiB
Svelte
145 lines
4.1 KiB
Svelte
|
|
<script lang="ts">
|
||
|
|
import { onDestroy, onMount } from 'svelte';
|
||
|
|
|
||
|
|
// Props
|
||
|
|
export let src: string | null = null; // URL or raw HTML (auto-detected)
|
||
|
|
export let title = 'Embedded Content';
|
||
|
|
export let initialHeight = 400; // fallback height if we can't measure
|
||
|
|
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;
|
||
|
|
|
||
|
|
// 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);
|
||
|
|
$: iframeSrc = isUrl ? (src as string) : null;
|
||
|
|
$: iframeDoc = !isUrl && src ? ensureAutosizer(src) : null;
|
||
|
|
|
||
|
|
// Try to measure same-origin content safely
|
||
|
|
function resizeSameOrigin() {
|
||
|
|
if (!iframe) return;
|
||
|
|
try {
|
||
|
|
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
||
|
|
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';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ensure event listener bound only while component lives
|
||
|
|
onMount(() => {
|
||
|
|
window.addEventListener('message', onMessage);
|
||
|
|
});
|
||
|
|
|
||
|
|
onDestroy(() => {
|
||
|
|
window.removeEventListener('message', onMessage);
|
||
|
|
});
|
||
|
|
|
||
|
|
// When the iframe loads, try same-origin resize (cross-origin will noop)
|
||
|
|
function onLoad() {
|
||
|
|
// schedule after layout
|
||
|
|
requestAnimationFrame(resizeSameOrigin);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* If user passes raw HTML, we inject a tiny autosizer that posts height.
|
||
|
|
* This helps both same-origin and "about:srcdoc" cases.
|
||
|
|
* (No effect if the caller already includes their own autosizer.)
|
||
|
|
*/
|
||
|
|
function ensureAutosizer(html: string): string {
|
||
|
|
const hasOurHook = /iframe:height/.test(html) || /postMessage\(.+height/i.test(html);
|
||
|
|
if (hasOurHook) return html;
|
||
|
|
|
||
|
|
// This script uses ResizeObserver to post the document height
|
||
|
|
const autosizer = `
|
||
|
|
<script>
|
||
|
|
(function () {
|
||
|
|
function send() {
|
||
|
|
try {
|
||
|
|
var h = Math.max(
|
||
|
|
document.documentElement.scrollHeight || 0,
|
||
|
|
document.body ? document.body.scrollHeight : 0
|
||
|
|
);
|
||
|
|
parent.postMessage({ type: 'iframe:height', height: h + 20 }, '*');
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
var ro = new ResizeObserver(function(){ send(); });
|
||
|
|
ro.observe(document.documentElement);
|
||
|
|
window.addEventListener('load', send);
|
||
|
|
// Also observe body if present
|
||
|
|
if (document.body) ro.observe(document.body);
|
||
|
|
// Periodic guard in case of late content
|
||
|
|
setTimeout(send, 0);
|
||
|
|
setTimeout(send, 250);
|
||
|
|
setTimeout(send, 1000);
|
||
|
|
})();
|
||
|
|
<\/script>`;
|
||
|
|
// inject before </body> if present, else append
|
||
|
|
return (
|
||
|
|
html.replace(/<\/body\s*>/i, autosizer + '</body>') +
|
||
|
|
(/<\/body\s*>/i.test(html) ? '' : autosizer)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
{#if iframeDoc}
|
||
|
|
<iframe
|
||
|
|
bind:this={iframe}
|
||
|
|
srcdoc={iframeDoc}
|
||
|
|
{title}
|
||
|
|
class="w-full rounded-xl"
|
||
|
|
style={`height:${initialHeight}px;`}
|
||
|
|
width="100%"
|
||
|
|
frameborder="0"
|
||
|
|
{sandbox}
|
||
|
|
referrerpolicy={referrerPolicy}
|
||
|
|
{allowFullscreen}
|
||
|
|
on:load={onLoad}
|
||
|
|
/>
|
||
|
|
{:else if iframeSrc}
|
||
|
|
<iframe
|
||
|
|
bind:this={iframe}
|
||
|
|
src={iframeSrc}
|
||
|
|
{title}
|
||
|
|
class="w-full rounded-xl"
|
||
|
|
style={`height:${initialHeight}px;`}
|
||
|
|
width="100%"
|
||
|
|
frameborder="0"
|
||
|
|
{sandbox}
|
||
|
|
referrerpolicy={referrerPolicy}
|
||
|
|
{allowFullscreen}
|
||
|
|
on:load={onLoad}
|
||
|
|
/>
|
||
|
|
{/if}
|