diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 58012fe6c..de0647eb5 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -4,49 +4,49 @@ function $el(tag, propsOrChildren, children) { const split = tag.split("."); const element = document.createElement(split.shift()); element.classList.add(...split); - if (propsOrChildren) { - if (Array.isArray(propsOrChildren)) { - element.append(...propsOrChildren); - } else { - const parent = propsOrChildren.parent; - delete propsOrChildren.parent; - const cb = propsOrChildren.$; - delete propsOrChildren.$; - - if (propsOrChildren.style) { - Object.assign(element.style, propsOrChildren.style); - delete propsOrChildren.style; - } - - Object.assign(element, propsOrChildren); - if (children) { - element.append(...children); - } - - if (parent) { - parent.append(element); - } - - if (cb) { - cb(element); - } - } + if (!propsOrChildren) return element; + if (Array.isArray(propsOrChildren)) { + element.append(...propsOrChildren); + return element; } + + const { parent, style } = propsOrChildren; + const cb = propsOrChildren.$; + delete propsOrChildren.parent; + delete propsOrChildren.$; + + if (style) { + Object.assign(element.style, style); + delete propsOrChildren.style; + } + + Object.assign(element, propsOrChildren); + if (children) { + element.append(...children); + } + + parent?.append(element); + cb?.(element); + return element; } class ComfyDialog { constructor() { - this.element = $el("div.comfy-modal", { parent: document.body }, [ - $el("div.comfy-modal-content", [ - $el("p", { $: (p) => (this.textElement = p) }), - $el("button", { - type: "button", - textContent: "CLOSE", - onclick: () => this.close(), - }), - ]), - ]); + const p = $el("p", { + $: (p) => (this.textElement = p) + }); + + const button = $el("button", { + type: "button", + textContent: "CLOSE", + onclick: this.close.bind(this) + }); + + const modalContent = $el("div.comfy-modal-content", [p, button]); + this.element = $el("div.comfy-modal", + { parent: document.body }, [modalContent] + ); } close() { @@ -76,55 +76,52 @@ class ComfySettingsDialog extends ComfyDialog { const settingId = "Comfy.Settings." + id; const v = localStorage[settingId]; - let value = v == null ? defaultValue : JSON.parse(v); + + // JSON.parse(null) -> null + // If v can be undefined -> JSON.parse(v ?? null) + let value = JSON.parse(v) ?? defaultValue; // Trigger initial setting of value - if (onChange) { - onChange(value, undefined); - } + onChange?.(value, undefined); - this.settings.push({ - render: () => { - const setter = (v) => { - if (onChange) { - onChange(v, value); - } - localStorage[settingId] = JSON.stringify(v); - value = v; - }; + const setter = (v) => { + onChange?.(v, value); + localStorage[settingId] = JSON.stringify(v); + value = v; + }; - if (typeof type === "function") { - return type(name, setter); - } + const createLable = (input) => ( + $el("div", [ + $el("label", { textContent: name || id }, [input]) + ]) + ); - switch (type) { - case "boolean": - return $el("div", [ - $el("label", { textContent: name || id }, [ - $el("input", { - type: "checkbox", - checked: !!value, - oninput: (e) => { - setter(e.target.checked); - }, - }), - ]), - ]); - default: - console.warn("Unsupported setting type, defaulting to text"); - return $el("div", [ - $el("label", { textContent: name || id }, [ - $el("input", { - value, - oninput: (e) => { - setter(e.target.value); - }, - }), - ]), - ]); - } - }, - }); + const render = () => { + if (typeof type === "function") { + return type(name, setter); + } + + if (type == 'boolean') { + return createLable( + $el("input", { + type: "checkbox", + checked: !!value, + oninput: (e) => setter(e.target.checked), + }) + ); + } + + console.warn("Unsupported setting type, defaulting to text"); + return createLable( + $el("input", { + value, + oninput: (e) => setter(e.target.value), + }) + ); + + }; + + this.settings.push({ render }); } show() { @@ -136,6 +133,7 @@ class ComfySettingsDialog extends ComfyDialog { class ComfyList { #type; #text; + element; constructor(text, type) { this.#text = text; @@ -150,39 +148,40 @@ class ComfyList { async load() { const items = await api.getItems(this.#type); + + const processItem = (item) => { + // Allow items to specify a custom remove action (e.g. for interrupt current prompt) + const removeAction = item.remove || { + name: "Delete", + cb: () => api.deleteItem(this.#type, item.prompt[1]), + }; + + const loadButton = $el("button", { + textContent: "Load", + onclick: () => { + if (item.outputs) { + this.app.nodeOutputs = item.outputs; + } + this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); + } + }); + const removeButton = $el("button", { + textContent: removeAction.name, + onclick: async () => { + await removeAction.cb(); + await this.update(); + } + }); + + return $el("div", { textContent: item.prompt[0] + ": " }, [loadButton, removeButton]); + }; + const processSection = (section) => [ + $el("h4", { textContent: section }), + $el("div.comfy-list-items", [...items[section].map(processItem)]), + ]; + this.element.replaceChildren( - ...Object.keys(items).flatMap((section) => [ - $el("h4", { - textContent: section, - }), - $el("div.comfy-list-items", [ - ...items[section].map((item) => { - // Allow items to specify a custom remove action (e.g. for interrupt current prompt) - const removeAction = item.remove || { - name: "Delete", - cb: () => api.deleteItem(this.#type, item.prompt[1]), - }; - return $el("div", { textContent: item.prompt[0] + ": " }, [ - $el("button", { - textContent: "Load", - onclick: () => { - if (item.outputs) { - app.nodeOutputs = item.outputs; - } - app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); - }, - }), - $el("button", { - textContent: removeAction.name, - onclick: async () => { - await removeAction.cb(); - await this.update(); - }, - }), - ]); - }), - ]), - ]), + ...Object.keys(items).flatMap(processSection), $el("div.comfy-list-actions", [ $el("button", { textContent: "Clear " + this.#text, @@ -191,15 +190,14 @@ class ComfyList { await this.load(); }, }), - $el("button", { textContent: "Refresh", onclick: () => this.load() }), + $el("button", { textContent: "Refresh", onclick: this.load.bind(this) }), ]) ); } async update() { - if (this.visible) { - await this.load(); - } + if (!this.visible) return; + await this.load(); } async show() { @@ -215,13 +213,8 @@ class ComfyList { } toggle() { - if (this.visible) { - this.hide(); - return false; - } else { - this.show(); - return true; - } + (this.visible ? this.hide : this.show).call(this); + return !this.visible; } } @@ -241,26 +234,30 @@ export class ComfyUI { this.history.update(); }); + this.createMenuContainer(); + this.setStatus({ exec_info: { queue_remaining: "X" } }); + } + + createMenuContainer() { const fileInput = $el("input", { type: "file", accept: ".json,image/png", style: { display: "none" }, parent: document.body, - onchange: () => { - app.handleFile(fileInput.files[0]); - }, + onchange: () => this.app.handleFile(fileInput.files[0]), }); this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ $el("div", { style: { overflow: "hidden", position: "relative", width: "100%" } }, [ $el("span", { $: (q) => (this.queueSize = q) }), - $el("button.comfy-settings-btn", { textContent: "⚙️", onclick: () => this.settings.show() }), + $el("button.comfy-settings-btn", { textContent: "⚙️", onclick: this.settings.show.bind(this.settings) }), ]), - $el("button.comfy-queue-btn", { textContent: "Queue Prompt", onclick: () => app.queuePrompt(0, this.batchCount) }), + $el("button.comfy-queue-btn", { textContent: "Queue Prompt", onclick: () => this.app.queuePrompt(0, this.batchCount) }), $el("div", {}, [ - $el("label", { innerHTML: "Extra options"}, [ - $el("input", { type: "checkbox", - onchange: (i) => { + $el("label", { innerHTML: "Extra options" }, [ + $el("input", { + type: "checkbox", + onchange: (i) => { document.getElementById('extraOptions').style.display = i.srcElement.checked ? "block" : "none"; this.batchCount = i.srcElement.checked ? document.getElementById('batchCountInputRange').value : 1; document.getElementById('autoQueueCheckbox').checked = false; @@ -268,26 +265,29 @@ export class ComfyUI { }) ]) ]), - $el("div", { id: "extraOptions", style: { width: "100%", display: "none" }}, [ + $el("div", { id: "extraOptions", style: { width: "100%", display: "none" } }, [ $el("label", { innerHTML: "Batch count" }, [ - $el("input", { id: "batchCountInputNumber", type: "number", value: this.batchCount, min: "1", style: { width: "35%", "margin-left": "0.4em" }, - oninput: (i) => { + $el("input", { + id: "batchCountInputNumber", type: "number", value: this.batchCount, min: "1", style: { width: "35%", "margin-left": "0.4em" }, + oninput: (i) => { this.batchCount = i.target.value; document.getElementById('batchCountInputRange').value = this.batchCount; } }), - $el("input", { id: "batchCountInputRange", type: "range", min: "1", max: "100", value: this.batchCount, + $el("input", { + id: "batchCountInputRange", type: "range", min: "1", max: "100", value: this.batchCount, oninput: (i) => { this.batchCount = i.srcElement.value; document.getElementById('batchCountInputNumber').value = i.srcElement.value; } }), - $el("input", { id: "autoQueueCheckbox", type: "checkbox", checked: false, title: "automatically queue prompt when the queue size hits 0", + $el("input", { + id: "autoQueueCheckbox", type: "checkbox", checked: false, title: "automatically queue prompt when the queue size hits 0", }) ]), ]), $el("div.comfy-menu-btns", [ - $el("button", { textContent: "Queue Front", onclick: () => app.queuePrompt(-1, this.batchCount) }), + $el("button", { textContent: "Queue Front", onclick: () => this.app.queuePrompt(-1, this.batchCount) }), $el("button", { $: (b) => (this.queue.button = b), textContent: "View Queue", @@ -310,7 +310,7 @@ export class ComfyUI { $el("button", { textContent: "Save", onclick: () => { - const json = JSON.stringify(app.graph.serialize(), null, 2); // convert the data to a JSON string + const json = JSON.stringify(this.app.graph.serialize(), null, 2); // convert the data to a JSON string const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = $el("a", { @@ -326,21 +326,18 @@ export class ComfyUI { }, 0); }, }), - $el("button", { textContent: "Load", onclick: () => fileInput.click() }), - $el("button", { textContent: "Clear", onclick: () => app.graph.clear() }), - $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), + $el("button", { textContent: "Load", onclick: fileInput.click.bind(fileInput) }), + $el("button", { textContent: "Clear", onclick: () => this.app.graph.clear() }), + $el("button", { textContent: "Load Default", onclick: () => this.app.loadGraphData() }) ]); - - this.setStatus({ exec_info: { queue_remaining: "X" } }); } setStatus(status) { - this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR"); - if (status) { - if (this.lastQueueSize != 0 && status.exec_info.queue_remaining == 0 && document.getElementById('autoQueueCheckbox').checked) { - app.queuePrompt(0, this.batchCount); - } - this.lastQueueSize = status.exec_info.queue_remaining + this.queueSize.textContent = "Queue size: " + status?.exec_info?.queue_remaining ?? "ERR"; + if (!status) return; + if (this.lastQueueSize != 0 && status.exec_info.queue_remaining == 0 && document.getElementById('autoQueueCheckbox').checked) { + this.app.queuePrompt(0, this.batchCount); } + this.lastQueueSize = status.exec_info.queue_remaining } }