CoolFace
Datasetpublic

gradio/frontend

sourceHugging Faceupdated 1d agoView on Hugging Face
1likes442kdownloads
HTML.svelte378 linesDownload Raw Back to shared
1<script lang="ts">2	import { createEventDispatcher, tick } from "svelte";3	import Handlebars from "handlebars";4 5	let {6		elem_classes = [],7		props = {},8		html_template = "${value}",9		css_template = "",10		js_on_load = null,11		visible = true,12		autoscroll = false,13		apply_default_css = true,14		component_class_name = "HTML"15	} = $props();16 17	let old_props = $state(JSON.parse(JSON.stringify(props)));18 19	const dispatch = createEventDispatcher<{20		event: { type: "click" | "submit"; data: any };21		update_value: { data: any; property: "value" | "label" | "visible" };22	}>();23 24	const trigger = (25		event_type: "click" | "submit",26		event_data: any = {}27	): void => {28		dispatch("event", { type: event_type, data: event_data });29	};30 31	let element: HTMLDivElement;32	let scrollable_parent: HTMLElement | null = null;33	let random_id = `html-${Math.random().toString(36).substring(2, 11)}`;34	let style_element: HTMLStyleElement | null = null;35	let reactiveProps: Record<string, any> = {};36	let currentHtml = $state("");37	let currentCss = $state("");38	let renderScheduled = $state(false);39	let mounted = $state(false);40	let html_error_message: string | null = $state(null);41	let css_error_message: string | null = $state(null);42 43	function get_scrollable_parent(element: HTMLElement): HTMLElement | null {44		let parent = element.parentElement;45		while (parent) {46			const style = window.getComputedStyle(parent);47			if (48				style.overflow === "auto" ||49				style.overflow === "scroll" ||50				style.overflowY === "auto" ||51				style.overflowY === "scroll"52			) {53				return parent;54			}55			parent = parent.parentElement;56		}57		return null;58	}59 60	function is_at_bottom(): boolean {61		if (!element) return true;62		if (!scrollable_parent) {63			return (64				window.innerHeight + window.scrollY >=65				document.documentElement.scrollHeight - 10066			);67		}68		return (69			scrollable_parent.offsetHeight + scrollable_parent.scrollTop >=70			scrollable_parent.scrollHeight - 10071		);72	}73 74	function scroll_to_bottom(): void {75		if (!element) return;76		if (scrollable_parent) {77			scrollable_parent.scrollTo(0, scrollable_parent.scrollHeight);78		} else {79			window.scrollTo(0, document.documentElement.scrollHeight);80		}81	}82 83	async function scroll_on_html_update(): Promise<void> {84		if (!autoscroll || !element) return;85		if (!scrollable_parent) {86			scrollable_parent = get_scrollable_parent(element);87		}88		if (is_at_bottom()) {89			await new Promise((resolve) => setTimeout(resolve, 300));90			scroll_to_bottom();91		}92	}93 94	function render_template(95		template: string,96		props: Record<string, any>,97		language: "html" | "css" = "html"98	): string {99		try {100			const handlebarsTemplate = Handlebars.compile(template);101			const handlebarsRendered = handlebarsTemplate(props);102 103			const propKeys = Object.keys(props);104			const propValues = Object.values(props);105			const templateFunc = new Function(106				...propKeys,107				`return \`${handlebarsRendered}\`;`108			);109			if (language === "html") {110				html_error_message = null;111			} else if (language === "css") {112				css_error_message = null;113			}114			return templateFunc(...propValues);115		} catch (e) {116			console.error("Error evaluating template:", e);117			if (language === "html") {118				html_error_message = e instanceof Error ? e.message : String(e);119			} else if (language === "css") {120				css_error_message = e instanceof Error ? e.message : String(e);121			}122			return "";123		}124	}125 126	function update_css(): void {127		if (typeof document === "undefined") return;128		if (!style_element) {129			style_element = document.createElement("style");130			document.head.appendChild(style_element);131		}132		currentCss = render_template(css_template, reactiveProps, "css");133		if (currentCss) {134			style_element.textContent = `#${random_id} { ${currentCss} }`;135		} else {136			style_element.textContent = "";137		}138	}139 140	function updateDOM(oldHtml: string, newHtml: string): void {141		if (!element || oldHtml === newHtml) return;142 143		const tempContainer = document.createElement("div");144		tempContainer.innerHTML = newHtml;145 146		const oldNodes = Array.from(element.childNodes);147		const newNodes = Array.from(tempContainer.childNodes);148 149		const maxLength = Math.max(oldNodes.length, newNodes.length);150 151		for (let i = 0; i < maxLength; i++) {152			const oldNode = oldNodes[i];153			const newNode = newNodes[i];154 155			if (!oldNode && newNode) {156				element.appendChild(newNode.cloneNode(true));157			} else if (oldNode && !newNode) {158				element.removeChild(oldNode);159			} else if (oldNode && newNode) {160				updateNode(oldNode, newNode);161			}162		}163	}164 165	// eslint-disable-next-line complexity166	function updateNode(oldNode: Node, newNode: Node): void {167		if (168			oldNode.nodeType === Node.TEXT_NODE &&169			newNode.nodeType === Node.TEXT_NODE170		) {171			if (oldNode.textContent !== newNode.textContent) {172				oldNode.textContent = newNode.textContent;173			}174			return;175		}176 177		if (178			oldNode.nodeType === Node.ELEMENT_NODE &&179			newNode.nodeType === Node.ELEMENT_NODE180		) {181			const oldElement = oldNode as Element;182			const newElement = newNode as Element;183 184			if (oldElement.tagName !== newElement.tagName) {185				oldNode.parentNode?.replaceChild(newNode.cloneNode(true), oldNode);186				return;187			}188 189			const oldAttrs = Array.from(oldElement.attributes);190			const newAttrs = Array.from(newElement.attributes);191 192			for (const attr of oldAttrs) {193				if (!newElement.hasAttribute(attr.name)) {194					oldElement.removeAttribute(attr.name);195				}196			}197 198			for (const attr of newAttrs) {199				if (oldElement.getAttribute(attr.name) !== attr.value) {200					oldElement.setAttribute(attr.name, attr.value);201 202					if (203						attr.name === "value" &&204						(oldElement.tagName === "INPUT" ||205							oldElement.tagName === "TEXTAREA" ||206							oldElement.tagName === "SELECT")207					) {208						(209							oldElement as210								| HTMLInputElement211								| HTMLTextAreaElement212								| HTMLSelectElement213						).value = attr.value;214					}215				}216			}217 218			const oldChildren = Array.from(oldElement.childNodes);219			const newChildren = Array.from(newElement.childNodes);220			const maxChildren = Math.max(oldChildren.length, newChildren.length);221 222			for (let i = 0; i < maxChildren; i++) {223				const oldChild = oldChildren[i];224				const newChild = newChildren[i];225 226				if (!oldChild && newChild) {227					oldElement.appendChild(newChild.cloneNode(true));228				} else if (oldChild && !newChild) {229					oldElement.removeChild(oldChild);230				} else if (oldChild && newChild) {231					updateNode(oldChild, newChild);232				}233			}234		} else {235			oldNode.parentNode?.replaceChild(newNode.cloneNode(true), oldNode);236		}237	}238 239	function renderHTML(): void {240		const newHtml = render_template(html_template, reactiveProps, "html");241		if (element) {242			updateDOM(currentHtml, newHtml);243		}244		currentHtml = newHtml;245		if (autoscroll) {246			scroll_on_html_update();247		}248	}249 250	function scheduleRender(): void {251		if (!renderScheduled) {252			renderScheduled = true;253			queueMicrotask(() => {254				renderScheduled = false;255				renderHTML();256				update_css();257			});258		}259	}260 261	// Mount effect262	$effect(() => {263		if (!element || mounted) return;264		mounted = true;265 266		reactiveProps = new Proxy(267			{ ...props },268			{269				set(target, property, value) {270					const oldValue = target[property as string];271					target[property as string] = value;272 273					if (oldValue !== value) {274						scheduleRender();275 276						if (277							property === "value" ||278							property === "label" ||279							property === "visible"280						) {281							props[property] = value;282							old_props[property] = value;283							dispatch("update_value", { data: value, property });284						}285					}286					return true;287				}288			}289		);290 291		currentHtml = render_template(html_template, reactiveProps, "html");292		element.innerHTML = currentHtml;293		update_css();294 295		if (autoscroll) {296			scroll_to_bottom();297		}298		scroll_on_html_update();299 300		if (js_on_load && element) {301			try {302				const func = new Function("element", "trigger", "props", js_on_load);303				func(element, trigger, reactiveProps);304			} catch (error) {305				console.error("Error executing js_on_load:", error);306			}307		}308	});309 310	// Props update effect311	$effect(() => {312		if (313			reactiveProps &&314			props &&315			JSON.stringify(old_props) !== JSON.stringify(props)316		) {317			for (const key in props) {318				if (reactiveProps[key] !== props[key]) {319					reactiveProps[key] = props[key];320				}321			}322			old_props = props;323		}324	});325</script>326 327{#if html_error_message || css_error_message}328	<div class="error-container">329		<strong class="error-title"330			>Error rendering <code class="error-component-name"331				>{component_class_name}</code332			>:</strong333		>334		<code class="error-message">{html_error_message || css_error_message}</code>335	</div>336{:else}337	<div338		bind:this={element}339		id={random_id}340		class="{apply_default_css ? 'prose gradio-style' : ''} {elem_classes.join(341			' '342		)}"343		class:hide={!visible}344	></div>345{/if}346 347<style>348	.hide {349		display: none;350	}351 352	.error-container {353		padding: 12px;354		background-color: #fee;355		border: 1px solid #fcc;356		border-radius: 4px;357		color: #c33;358		font-family: monospace;359		font-size: 13px;360	}361 362	.error-title {363		display: block;364		margin-bottom: 8px;365	}366 367	.error-component-name {368		background-color: #fdd;369		padding: 2px 4px;370		border-radius: 2px;371	}372 373	.error-message {374		white-space: pre-wrap;375		word-break: break-word;376	}377</style>378 
gradio/frontend · CoolFace