gradio/frontend
1442k
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 