mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-03 19:59:42 -06:00
171 lines
5.4 KiB
TypeScript
171 lines
5.4 KiB
TypeScript
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
|
|
|
|
export type WebviewState = ComponentState & {
|
|
_type_: "Webview-builtin";
|
|
content: string; // Url or Html code
|
|
enable_pointer_events: boolean;
|
|
resize_to_fit_content: boolean;
|
|
};
|
|
|
|
export class WebviewComponent extends ComponentBase<WebviewState> {
|
|
private iframe: HTMLIFrameElement | null = null;
|
|
private resizeObserver: ResizeObserver | null = null;
|
|
private isInitialized = false;
|
|
|
|
createElement(): HTMLElement {
|
|
let element = document.createElement("div");
|
|
element.classList.add("rio-webview");
|
|
return element;
|
|
}
|
|
|
|
updateElement(
|
|
deltaState: DeltaState<WebviewState>,
|
|
latentComponents: Set<ComponentBase>
|
|
): void {
|
|
super.updateElement(deltaState, latentComponents);
|
|
|
|
if (deltaState.content !== undefined) {
|
|
// If the URL/HTML hasn't actually changed from last time, don't do
|
|
// anything. This is important so scripts don't get re-executed each
|
|
// time the component is updated.
|
|
if (
|
|
deltaState.content !== this.state.content ||
|
|
!this.isInitialized
|
|
) {
|
|
if (isUrl(deltaState.content)) {
|
|
this.element.innerHTML = "";
|
|
|
|
this.iframe = this.createIframe();
|
|
this.iframe.src = deltaState.content;
|
|
|
|
this.element.appendChild(this.iframe);
|
|
} else if (requiresIframe(deltaState.content)) {
|
|
this.element.innerHTML = "";
|
|
|
|
this.iframe = this.createIframe();
|
|
this.iframe.srcdoc = deltaState.content;
|
|
|
|
this.element.appendChild(this.iframe);
|
|
} else {
|
|
// Clean up stuff we no longer need
|
|
this.iframe = null;
|
|
this.resizeObserver = null;
|
|
|
|
// Load the HTML
|
|
this.element.innerHTML = deltaState.content;
|
|
|
|
// Just setting the innerHTML doesn't run scripts. Do that manually.
|
|
this.runScriptsInElement();
|
|
}
|
|
|
|
this.isInitialized = true;
|
|
}
|
|
}
|
|
|
|
if (deltaState.enable_pointer_events !== undefined) {
|
|
this.element.style.pointerEvents = deltaState.enable_pointer_events
|
|
? "auto"
|
|
: "none";
|
|
}
|
|
|
|
if (
|
|
deltaState.resize_to_fit_content !== undefined &&
|
|
this.iframe !== null
|
|
) {
|
|
if (deltaState.resize_to_fit_content) {
|
|
if (this.resizeObserver === null) {
|
|
this.resizeObserver = tryCreateIframeResizeObserver(
|
|
this.iframe
|
|
);
|
|
}
|
|
} else {
|
|
if (this.resizeObserver !== null) {
|
|
this.resizeObserver.disconnect();
|
|
this.resizeObserver = null;
|
|
|
|
this.iframe.style.removeProperty("min-width");
|
|
this.iframe.style.removeProperty("min-height");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
createIframe(): HTMLIFrameElement {
|
|
let iframe = document.createElement("iframe");
|
|
|
|
let self = this;
|
|
iframe.addEventListener("load", function () {
|
|
// Careful, this code runs with a delay! If this iframe has
|
|
// already been replaced by other content, do nothing.
|
|
if (
|
|
self.iframe !== iframe ||
|
|
self.resizeObserver !== null ||
|
|
!self.state.resize_to_fit_content
|
|
) {
|
|
return;
|
|
}
|
|
|
|
self.resizeObserver = tryCreateIframeResizeObserver(iframe);
|
|
});
|
|
|
|
return iframe;
|
|
}
|
|
|
|
runScriptsInElement(): void {
|
|
for (let oldScriptElement of this.element.querySelectorAll("script")) {
|
|
// Create a new script element
|
|
const newScriptElement = document.createElement("script");
|
|
|
|
// Copy over all attributes
|
|
for (let i = 0; i < oldScriptElement.attributes.length; i++) {
|
|
const attr = oldScriptElement.attributes[i];
|
|
newScriptElement.setAttribute(attr.name, attr.value);
|
|
}
|
|
|
|
// And the source itself
|
|
newScriptElement.appendChild(
|
|
document.createTextNode(oldScriptElement.innerHTML)
|
|
);
|
|
|
|
// Finally replace the old script element with the new one so
|
|
// the browser executes it
|
|
oldScriptElement.parentNode!.replaceChild(
|
|
newScriptElement,
|
|
oldScriptElement
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isUrl(urlOrHtml: string): boolean {
|
|
try {
|
|
new URL(urlOrHtml);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function requiresIframe(html: string): boolean {
|
|
return html.match(/^\s*(<!doctype |<html[ >])/i) !== null;
|
|
}
|
|
|
|
function tryCreateIframeResizeObserver(
|
|
iframe: HTMLIFrameElement
|
|
): ResizeObserver | null {
|
|
let contentDoc = iframe.contentDocument;
|
|
if (contentDoc === null) {
|
|
return null;
|
|
}
|
|
|
|
let docElement = contentDoc.documentElement;
|
|
|
|
let resizeObserver = new ResizeObserver(function () {
|
|
iframe.style.minWidth = `${docElement.scrollWidth}px`;
|
|
iframe.style.minHeight = `${docElement.scrollHeight}px`;
|
|
});
|
|
resizeObserver.observe(docElement);
|
|
|
|
return resizeObserver;
|
|
}
|