Spaces:
Sleeping
Sleeping
| // @ts-check | |
| import { ComfyButton } from "../components/button.js"; | |
| import { prop, getStorageValue, setStorageValue } from "../../utils.js"; | |
| import { $el } from "../../ui.js"; | |
| import { api } from "../../api.js"; | |
| import { ComfyPopup } from "../components/popup.js"; | |
| import { createSpinner } from "../spinner.js"; | |
| import { ComfyWorkflow, trimJsonExt } from "../../workflows.js"; | |
| import { ComfyAsyncDialog } from "../components/asyncDialog.js"; | |
| export class ComfyWorkflowsMenu { | |
| #first = true; | |
| element = $el("div.comfyui-workflows"); | |
| get open() { | |
| return this.popup.open; | |
| } | |
| set open(open) { | |
| this.popup.open = open; | |
| } | |
| /** | |
| * @param {import("../../app.js").ComfyApp} app | |
| */ | |
| constructor(app) { | |
| this.app = app; | |
| this.#bindEvents(); | |
| const classList = { | |
| "comfyui-workflows-button": true, | |
| "comfyui-button": true, | |
| unsaved: getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true", | |
| running: false, | |
| }; | |
| this.buttonProgress = $el("div.comfyui-workflows-button-progress"); | |
| this.workflowLabel = $el("span.comfyui-workflows-label", ""); | |
| this.button = new ComfyButton({ | |
| content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]), | |
| icon: "chevron-down", | |
| classList, | |
| }); | |
| this.element.append(this.button.element); | |
| this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" }); | |
| this.content = new ComfyWorkflowsContent(app, this.popup); | |
| this.popup.children = [this.content.element]; | |
| this.popup.addEventListener("change", () => { | |
| this.button.icon = "chevron-" + (this.popup.open ? "up" : "down"); | |
| }); | |
| this.button.withPopup(this.popup); | |
| this.unsaved = prop(this, "unsaved", classList.unsaved, (v) => { | |
| classList.unsaved = v; | |
| this.button.classList = classList; | |
| setStorageValue("Comfy.PreviousWorkflowUnsaved", v); | |
| }); | |
| } | |
| #updateProgress = () => { | |
| const prompt = this.app.workflowManager.activePrompt; | |
| let percent = 0; | |
| if (this.app.workflowManager.activeWorkflow === prompt?.workflow) { | |
| const total = Object.values(prompt.nodes); | |
| const done = total.filter(Boolean); | |
| percent = (done.length / total.length) * 100; | |
| } | |
| this.buttonProgress.style.width = percent + "%"; | |
| }; | |
| #updateActive = () => { | |
| const active = this.app.workflowManager.activeWorkflow; | |
| this.button.tooltip = active.path; | |
| this.workflowLabel.textContent = active.name; | |
| this.unsaved = active.unsaved; | |
| if (this.#first) { | |
| this.#first = false; | |
| this.content.load(); | |
| } | |
| this.#updateProgress(); | |
| }; | |
| #bindEvents() { | |
| this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive); | |
| this.app.workflowManager.addEventListener("rename", this.#updateActive); | |
| this.app.workflowManager.addEventListener("delete", this.#updateActive); | |
| this.app.workflowManager.addEventListener("save", () => { | |
| this.unsaved = this.app.workflowManager.activeWorkflow.unsaved; | |
| }); | |
| this.app.workflowManager.addEventListener("execute", (e) => { | |
| this.#updateProgress(); | |
| }); | |
| api.addEventListener("graphChanged", () => { | |
| this.unsaved = true; | |
| }); | |
| } | |
| #getMenuOptions(callback) { | |
| const menu = []; | |
| const directories = new Map(); | |
| for (const workflow of this.app.workflowManager.workflows || []) { | |
| const path = workflow.pathParts; | |
| if (!path) continue; | |
| let parent = menu; | |
| let currentPath = ""; | |
| for (let i = 0; i < path.length - 1; i++) { | |
| currentPath += "/" + path[i]; | |
| let newParent = directories.get(currentPath); | |
| if (!newParent) { | |
| newParent = { | |
| title: path[i], | |
| has_submenu: true, | |
| submenu: { | |
| options: [], | |
| }, | |
| }; | |
| parent.push(newParent); | |
| newParent = newParent.submenu.options; | |
| directories.set(currentPath, newParent); | |
| } | |
| parent = newParent; | |
| } | |
| parent.push({ | |
| title: trimJsonExt(path[path.length - 1]), | |
| callback: () => callback(workflow), | |
| }); | |
| } | |
| return menu; | |
| } | |
| #getFavoriteMenuOptions(callback) { | |
| const menu = []; | |
| for (const workflow of this.app.workflowManager.workflows || []) { | |
| if (workflow.isFavorite) { | |
| menu.push({ | |
| title: "⭐ " + workflow.name, | |
| callback: () => callback(workflow), | |
| }); | |
| } | |
| } | |
| return menu; | |
| } | |
| /** | |
| * @param {import("../../app.js").ComfyApp} app | |
| */ | |
| registerExtension(app) { | |
| const self = this; | |
| app.registerExtension({ | |
| name: "Comfy.Workflows", | |
| async beforeRegisterNodeDef(nodeType) { | |
| function getImageWidget(node) { | |
| const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional }; | |
| for (const input in inputs) { | |
| if (inputs[input][0] === "IMAGEUPLOAD") { | |
| const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image")); | |
| if (imageWidget) return imageWidget; | |
| } | |
| } | |
| } | |
| function setWidgetImage(node, widget, img) { | |
| const url = new URL(img.src); | |
| const filename = url.searchParams.get("filename"); | |
| const subfolder = url.searchParams.get("subfolder"); | |
| const type = url.searchParams.get("type"); | |
| const imageId = `${subfolder ? subfolder + "/" : ""}${filename} [${type}]`; | |
| widget.value = imageId; | |
| node.imgs = [img]; | |
| app.graph.setDirtyCanvas(true, true); | |
| } | |
| /** | |
| * @param {HTMLImageElement} img | |
| * @param {ComfyWorkflow} workflow | |
| */ | |
| async function sendToWorkflow(img, workflow) { | |
| await workflow.load(); | |
| let options = []; | |
| const nodes = app.graph.computeExecutionOrder(false); | |
| for (const node of nodes) { | |
| const widget = getImageWidget(node); | |
| if (widget == null) continue; | |
| if (node.title?.toLowerCase().includes("input")) { | |
| options = [{ widget, node }]; | |
| break; | |
| } else { | |
| options.push({ widget, node }); | |
| } | |
| } | |
| if (!options.length) { | |
| alert("No image nodes have been found in this workflow!"); | |
| return; | |
| } else if (options.length > 1) { | |
| const dialog = new WidgetSelectionDialog(options); | |
| const res = await dialog.show(app); | |
| if (!res) return; | |
| options = [res]; | |
| } | |
| setWidgetImage(options[0].node, options[0].widget, img); | |
| } | |
| const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"]; | |
| nodeType.prototype["getExtraMenuOptions"] = function (_, options) { | |
| const r = getExtraMenuOptions?.apply?.(this, arguments); | |
| if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) { | |
| const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this); | |
| let img; | |
| if (t.imageIndex != null) { | |
| // An image is selected so select that | |
| img = t.imgs?.[t.imageIndex]; | |
| } else if (t.overIndex != null) { | |
| // No image is selected but one is hovered | |
| img = t.img?.s[t.overIndex]; | |
| } | |
| if (img) { | |
| let pos = options.findIndex((o) => o.content === "Save Image"); | |
| if (pos === -1) { | |
| pos = 0; | |
| } else { | |
| pos++; | |
| } | |
| options.splice(pos, 0, { | |
| content: "Send to workflow", | |
| has_submenu: true, | |
| submenu: { | |
| options: [ | |
| { | |
| callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow), | |
| title: "[Current workflow]", | |
| }, | |
| ...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)), | |
| null, | |
| ...self.#getMenuOptions(sendToWorkflow.bind(null, img)), | |
| ], | |
| }, | |
| }); | |
| } | |
| } | |
| return r; | |
| }; | |
| }, | |
| }); | |
| } | |
| } | |
| export class ComfyWorkflowsContent { | |
| element = $el("div.comfyui-workflows-panel"); | |
| treeState = {}; | |
| treeFiles = {}; | |
| /** @type { Map<ComfyWorkflow, WorkflowElement> } */ | |
| openFiles = new Map(); | |
| /** @type {WorkflowElement} */ | |
| activeElement = null; | |
| /** | |
| * @param {import("../../app.js").ComfyApp} app | |
| * @param {ComfyPopup} popup | |
| */ | |
| constructor(app, popup) { | |
| this.app = app; | |
| this.popup = popup; | |
| this.actions = $el("div.comfyui-workflows-actions", [ | |
| new ComfyButton({ | |
| content: "Default", | |
| icon: "file-code", | |
| iconSize: 18, | |
| classList: "comfyui-button primary", | |
| tooltip: "Load default workflow", | |
| action: () => { | |
| popup.open = false; | |
| app.loadGraphData(); | |
| app.resetView(); | |
| }, | |
| }).element, | |
| new ComfyButton({ | |
| content: "Browse", | |
| icon: "folder", | |
| iconSize: 18, | |
| tooltip: "Browse for an image or exported workflow", | |
| action: () => { | |
| popup.open = false; | |
| app.ui.loadFile(); | |
| }, | |
| }).element, | |
| new ComfyButton({ | |
| content: "Blank", | |
| icon: "plus-thick", | |
| iconSize: 18, | |
| tooltip: "Create a new blank workflow", | |
| action: () => { | |
| app.workflowManager.setWorkflow(null); | |
| app.clean(); | |
| app.graph.clear(); | |
| app.workflowManager.activeWorkflow.track(); | |
| popup.open = false; | |
| }, | |
| }).element, | |
| ]); | |
| this.spinner = createSpinner(); | |
| this.element.replaceChildren(this.actions, this.spinner); | |
| this.popup.addEventListener("open", () => this.load()); | |
| this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner)); | |
| this.app.workflowManager.addEventListener("favorite", (e) => { | |
| const workflow = e["detail"]; | |
| const button = this.treeFiles[workflow.path]?.primary; | |
| if (!button) return; // Can happen when a workflow is renamed | |
| button.icon = this.#getFavoriteIcon(workflow); | |
| button.overIcon = this.#getFavoriteOverIcon(workflow); | |
| this.updateFavorites(); | |
| }); | |
| for (const e of ["save", "open", "close", "changeWorkflow"]) { | |
| // TODO: dont be lazy and just update the specific element | |
| app.workflowManager.addEventListener(e, () => this.updateOpen()); | |
| } | |
| this.app.workflowManager.addEventListener("rename", () => this.load()); | |
| this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive()); | |
| } | |
| async load() { | |
| await this.app.workflowManager.loadWorkflows(); | |
| this.updateTree(); | |
| this.updateFavorites(); | |
| this.updateOpen(); | |
| this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement); | |
| } | |
| updateOpen() { | |
| const current = this.openElement; | |
| this.openFiles.clear(); | |
| this.openElement = $el("div.comfyui-workflows-open", [ | |
| $el("h3", "Open"), | |
| ...this.app.workflowManager.openWorkflows.map((w) => { | |
| const wrapper = new WorkflowElement(this, w, { | |
| primary: { element: $el("i.mdi.mdi-18px.mdi-progress-pencil") }, | |
| buttons: [ | |
| this.#getRenameButton(w), | |
| new ComfyButton({ | |
| icon: "close", | |
| iconSize: 18, | |
| classList: "comfyui-button comfyui-workflows-file-action", | |
| tooltip: "Close workflow", | |
| action: (e) => { | |
| e.stopImmediatePropagation(); | |
| this.app.workflowManager.closeWorkflow(w); | |
| }, | |
| }), | |
| ], | |
| }); | |
| if (w.unsaved) { | |
| wrapper.element.classList.add("unsaved"); | |
| } | |
| if(w === this.app.workflowManager.activeWorkflow) { | |
| wrapper.element.classList.add("active"); | |
| } | |
| this.openFiles.set(w, wrapper); | |
| return wrapper.element; | |
| }), | |
| ]); | |
| this.#updateActive(); | |
| current?.replaceWith(this.openElement); | |
| } | |
| updateFavorites() { | |
| const current = this.favoritesElement; | |
| const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)]; | |
| this.favoritesElement = $el("div.comfyui-workflows-favorites", [ | |
| $el("h3", "Favorites"), | |
| ...favorites | |
| .map((w) => { | |
| return this.#getWorkflowElement(w).element; | |
| }) | |
| .filter(Boolean), | |
| ]); | |
| current?.replaceWith(this.favoritesElement); | |
| } | |
| filterTree() { | |
| if (!this.filterText) { | |
| this.treeRoot.classList.remove("filtered"); | |
| // Unfilter whole tree | |
| for (const item of Object.values(this.treeFiles)) { | |
| item.element.parentElement.style.removeProperty("display"); | |
| this.showTreeParents(item.element.parentElement); | |
| } | |
| return; | |
| } | |
| this.treeRoot.classList.add("filtered"); | |
| const searchTerms = this.filterText.toLocaleLowerCase().split(" "); | |
| for (const item of Object.values(this.treeFiles)) { | |
| const parts = item.workflow.pathParts; | |
| let termIndex = 0; | |
| let valid = false; | |
| for (const part of parts) { | |
| let currentIndex = 0; | |
| do { | |
| currentIndex = part.indexOf(searchTerms[termIndex], currentIndex); | |
| if (currentIndex > -1) currentIndex += searchTerms[termIndex].length; | |
| } while (currentIndex !== -1 && ++termIndex < searchTerms.length); | |
| if (termIndex >= searchTerms.length) { | |
| valid = true; | |
| break; | |
| } | |
| } | |
| if (valid) { | |
| item.element.parentElement.style.removeProperty("display"); | |
| this.showTreeParents(item.element.parentElement); | |
| } else { | |
| item.element.parentElement.style.display = "none"; | |
| this.hideTreeParents(item.element.parentElement); | |
| } | |
| } | |
| } | |
| hideTreeParents(element) { | |
| // Hide all parents if no children are visible | |
| if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { | |
| for (let i = 1; i < element.parentElement.children.length; i++) { | |
| const c = element.parentElement.children[i]; | |
| if (c.style.display !== "none") { | |
| return; | |
| } | |
| } | |
| element.parentElement.style.display = "none"; | |
| this.hideTreeParents(element.parentElement); | |
| } | |
| } | |
| showTreeParents(element) { | |
| if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { | |
| element.parentElement.style.removeProperty("display"); | |
| this.showTreeParents(element.parentElement); | |
| } | |
| } | |
| updateTree() { | |
| const current = this.treeElement; | |
| const nodes = {}; | |
| let typingTimeout; | |
| this.treeFiles = {}; | |
| this.treeRoot = $el("ul.comfyui-workflows-tree"); | |
| this.treeElement = $el("section", [ | |
| $el("header", [ | |
| $el("h3", "Browse"), | |
| $el("div.comfy-ui-workflows-search", [ | |
| $el("i.mdi.mdi-18px.mdi-magnify"), | |
| $el("input", { | |
| placeholder: "Search", | |
| value: this.filterText ?? "", | |
| oninput: (e) => { | |
| this.filterText = e.target["value"]?.trim(); | |
| clearTimeout(typingTimeout); | |
| typingTimeout = setTimeout(() => this.filterTree(), 250); | |
| }, | |
| }), | |
| ]), | |
| ]), | |
| this.treeRoot, | |
| ]); | |
| for (const workflow of this.app.workflowManager.workflows) { | |
| if (!workflow.pathParts) continue; | |
| let currentPath = ""; | |
| let currentRoot = this.treeRoot; | |
| for (let i = 0; i < workflow.pathParts.length; i++) { | |
| currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i]; | |
| const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot); | |
| nodes[currentPath] = parentNode; | |
| currentRoot = parentNode; | |
| } | |
| } | |
| current?.replaceWith(this.treeElement); | |
| this.filterTree(); | |
| } | |
| #expandNode(el, workflow, thisPath, i) { | |
| const expanded = !el.classList.toggle("closed"); | |
| if (expanded) { | |
| let c = ""; | |
| for (let j = 0; j <= i; j++) { | |
| c += (c ? "\\" : "") + workflow.pathParts[j]; | |
| this.treeState[c] = true; | |
| } | |
| } else { | |
| let c = thisPath; | |
| for (let j = i + 1; j < workflow.pathParts.length; j++) { | |
| c += (c ? "\\" : "") + workflow.pathParts[j]; | |
| delete this.treeState[c]; | |
| } | |
| delete this.treeState[thisPath]; | |
| } | |
| } | |
| #updateActive() { | |
| this.#removeActive(); | |
| const active = this.app.workflowManager.activePrompt; | |
| if (!active?.workflow) return; | |
| const open = this.openFiles.get(active.workflow); | |
| if (!open) return; | |
| this.activeElement = open; | |
| const total = Object.values(active.nodes); | |
| const done = total.filter(Boolean); | |
| const percent = done.length / total.length; | |
| open.element.classList.add("running"); | |
| open.element.style.setProperty("--progress", percent * 100 + "%"); | |
| open.primary.element.classList.remove("mdi-progress-pencil"); | |
| open.primary.element.classList.add("mdi-play"); | |
| } | |
| #removeActive() { | |
| if (!this.activeElement) return; | |
| this.activeElement.element.classList.remove("running"); | |
| this.activeElement.element.style.removeProperty("--progress"); | |
| this.activeElement.primary.element.classList.add("mdi-progress-pencil"); | |
| this.activeElement.primary.element.classList.remove("mdi-play"); | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getFavoriteIcon(workflow) { | |
| return workflow.isFavorite ? "star" : "file-outline"; | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getFavoriteOverIcon(workflow) { | |
| return workflow.isFavorite ? "star-off" : "star-outline"; | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getFavoriteTooltip(workflow) { | |
| return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites"; | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getFavoriteButton(workflow, primary) { | |
| return new ComfyButton({ | |
| icon: this.#getFavoriteIcon(workflow), | |
| overIcon: this.#getFavoriteOverIcon(workflow), | |
| iconSize: 18, | |
| classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""), | |
| tooltip: this.#getFavoriteTooltip(workflow), | |
| action: (e) => { | |
| e.stopImmediatePropagation(); | |
| workflow.favorite(!workflow.isFavorite); | |
| }, | |
| }); | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getDeleteButton(workflow) { | |
| const deleteButton = new ComfyButton({ | |
| icon: "delete", | |
| tooltip: "Delete this workflow", | |
| classList: "comfyui-button comfyui-workflows-file-action", | |
| iconSize: 18, | |
| action: async (e, btn) => { | |
| e.stopImmediatePropagation(); | |
| if (btn.icon === "delete-empty") { | |
| btn.enabled = false; | |
| await workflow.delete(); | |
| await this.load(); | |
| } else { | |
| btn.icon = "delete-empty"; | |
| btn.element.style.background = "red"; | |
| } | |
| }, | |
| }); | |
| deleteButton.element.addEventListener("mouseleave", () => { | |
| deleteButton.icon = "delete"; | |
| deleteButton.element.style.removeProperty("background"); | |
| }); | |
| return deleteButton; | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getInsertButton(workflow) { | |
| return new ComfyButton({ | |
| icon: "file-move-outline", | |
| iconSize: 18, | |
| tooltip: "Insert this workflow into the current workflow", | |
| classList: "comfyui-button comfyui-workflows-file-action", | |
| action: (e) => { | |
| if (!this.app.shiftDown) { | |
| this.popup.open = false; | |
| } | |
| e.stopImmediatePropagation(); | |
| if (!this.app.shiftDown) { | |
| this.popup.open = false; | |
| } | |
| workflow.insert(); | |
| }, | |
| }); | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getRenameButton(workflow) { | |
| return new ComfyButton({ | |
| icon: "pencil", | |
| tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.", | |
| classList: "comfyui-button comfyui-workflows-file-action", | |
| iconSize: 18, | |
| enabled: !!workflow.path, | |
| action: async (e) => { | |
| e.stopImmediatePropagation(); | |
| const newName = prompt("Enter new name", workflow.path); | |
| if (newName) { | |
| await workflow.rename(newName); | |
| } | |
| }, | |
| }); | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #getWorkflowElement(workflow) { | |
| return new WorkflowElement(this, workflow, { | |
| primary: this.#getFavoriteButton(workflow, true), | |
| buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)], | |
| }); | |
| } | |
| /** @param {ComfyWorkflow} workflow */ | |
| #createLeafNode(workflow) { | |
| const fileNode = this.#getWorkflowElement(workflow); | |
| this.treeFiles[workflow.path] = fileNode; | |
| return fileNode; | |
| } | |
| #createNode(currentPath, workflow, i, currentRoot) { | |
| const part = workflow.pathParts[i]; | |
| const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), { | |
| $: (el) => { | |
| el.onclick = (e) => { | |
| this.#expandNode(el, workflow, currentPath, i); | |
| e.stopImmediatePropagation(); | |
| }; | |
| }, | |
| }); | |
| currentRoot.append(parentNode); | |
| // Create a node for the current part and an inner UL for its children if it isnt a leaf node | |
| const leaf = i === workflow.pathParts.length - 1; | |
| let nodeElement; | |
| if (leaf) { | |
| nodeElement = this.#createLeafNode(workflow).element; | |
| } else { | |
| nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]); | |
| } | |
| parentNode.append(nodeElement); | |
| return parentNode; | |
| } | |
| } | |
| class WorkflowElement { | |
| /** | |
| * @param { ComfyWorkflowsContent } parent | |
| * @param { ComfyWorkflow } workflow | |
| */ | |
| constructor(parent, workflow, { tagName = "li", primary, buttons }) { | |
| this.parent = parent; | |
| this.workflow = workflow; | |
| this.primary = primary; | |
| this.buttons = buttons; | |
| this.element = $el( | |
| tagName + ".comfyui-workflows-tree-file", | |
| { | |
| onclick: () => { | |
| workflow.load(); | |
| this.parent.popup.open = false; | |
| }, | |
| title: this.workflow.path, | |
| }, | |
| [this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)] | |
| ); | |
| } | |
| } | |
| class WidgetSelectionDialog extends ComfyAsyncDialog { | |
| #options; | |
| /** | |
| * @param {Array<{widget: {name: string}, node: {pos: [number, number], title: string, id: string, type: string}}>} options | |
| */ | |
| constructor(options) { | |
| super(); | |
| this.#options = options; | |
| } | |
| show(app) { | |
| this.element.classList.add("comfy-widget-selection-dialog"); | |
| return super.show( | |
| $el("div", [ | |
| $el("h2", "Select image target"), | |
| $el( | |
| "p", | |
| "This workflow has multiple image loader nodes, you can rename a node to include 'input' in the title for it to be automatically selected, or select one below." | |
| ), | |
| $el( | |
| "section", | |
| this.#options.map((opt) => { | |
| return $el("div.comfy-widget-selection-item", [ | |
| $el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`), | |
| $el( | |
| "button.comfyui-button", | |
| { | |
| onclick: () => { | |
| app.canvas.ds.offset[0] = -opt.node.pos[0] + 50; | |
| app.canvas.ds.offset[1] = -opt.node.pos[1] + 50; | |
| app.canvas.selectNode(opt.node); | |
| app.graph.setDirtyCanvas(true, true); | |
| }, | |
| }, | |
| "Show" | |
| ), | |
| $el( | |
| "button.comfyui-button.primary", | |
| { | |
| onclick: () => { | |
| this.close(opt); | |
| }, | |
| }, | |
| "Select" | |
| ), | |
| ]); | |
| }) | |
| ), | |
| ]) | |
| ); | |
| } | |
| } |