mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-09-12 20:48:22 +00:00
New Menu & Workflow Management (#3112)
* menu * wip * wip * wip * wip * wip * workflow saving/loading * Support inserting workflows Move buttosn to top of lists * fix session storage implement renaming * temp * refactor, better workflow instance management * wip * progress on progress * added send to workflow various fixes * Support multiple image loaders * Support dynamic size breakpoints based on content * various fixes add close unsaved warning * Add filtering tree * prevent renaming unsaved * fix zindex on hover * fix top offset * use filename as workflow name * resize on setting change * hide element until it is drawn * remove glow * Fix export name * Fix test, revert accidental changes to groupNode * Fix colors on all themes * show hover items on smaller screen (mobile) * remove debugging code * dialog fix * Dont reorder open workflows Allow elements around canvas * Toggle body display on setting change * Fix menu disappearing on chrome * Increase delay when typing, remove margin on Safari, fix dialog location * Fix overflow issue on iOS * Add reset view button Prevent view changes causing history entries * Bottom menu wip * Various fixes * Fix merge * Fix breaking old menu position * Fix merge adding restore view to loadGraphData
This commit is contained in:
64
web/scripts/ui/components/asyncDialog.js
Normal file
64
web/scripts/ui/components/asyncDialog.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ComfyDialog } from "../dialog.js";
|
||||
import { $el } from "../../ui.js";
|
||||
|
||||
export class ComfyAsyncDialog extends ComfyDialog {
|
||||
#resolve;
|
||||
|
||||
constructor(actions) {
|
||||
super(
|
||||
"dialog.comfy-dialog.comfyui-dialog",
|
||||
actions?.map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
}
|
||||
return $el("button.comfyui-button", {
|
||||
type: "button",
|
||||
textContent: opt.text,
|
||||
onclick: () => this.close(opt.value ?? opt.text),
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
show(html) {
|
||||
this.element.addEventListener("close", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
super.show(html);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
showModal(html) {
|
||||
this.element.addEventListener("close", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
super.show(html);
|
||||
this.element.showModal();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
close(result = null) {
|
||||
this.#resolve(result);
|
||||
this.element.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
static async prompt({ title = null, message, actions }) {
|
||||
const dialog = new ComfyAsyncDialog(actions);
|
||||
const content = [$el("span", message)];
|
||||
if (title) {
|
||||
content.unshift($el("h3", title));
|
||||
}
|
||||
const res = await dialog.showModal(content);
|
||||
dialog.element.remove();
|
||||
return res;
|
||||
}
|
||||
}
|
163
web/scripts/ui/components/button.js
Normal file
163
web/scripts/ui/components/button.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// @ts-check
|
||||
|
||||
import { $el } from "../../ui.js";
|
||||
import { applyClasses, toggleElement } from "../utils.js";
|
||||
import { prop } from "../../utils.js";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* icon?: string;
|
||||
* overIcon?: string;
|
||||
* iconSize?: number;
|
||||
* content?: string | HTMLElement;
|
||||
* tooltip?: string;
|
||||
* enabled?: boolean;
|
||||
* action?: (e: Event, btn: ComfyButton) => void,
|
||||
* classList?: import("../utils.js").ClassList,
|
||||
* visibilitySetting?: { id: string, showValue: any },
|
||||
* app?: import("../../app.js").ComfyApp
|
||||
* }} ComfyButtonProps
|
||||
*/
|
||||
export class ComfyButton {
|
||||
#over = 0;
|
||||
#popupOpen = false;
|
||||
isOver = false;
|
||||
iconElement = $el("i.mdi");
|
||||
contentElement = $el("span");
|
||||
/**
|
||||
* @type {import("./popup.js").ComfyPopup}
|
||||
*/
|
||||
popup;
|
||||
|
||||
/**
|
||||
* @param {ComfyButtonProps} opts
|
||||
*/
|
||||
constructor({
|
||||
icon,
|
||||
overIcon,
|
||||
iconSize,
|
||||
content,
|
||||
tooltip,
|
||||
action,
|
||||
classList = "comfyui-button",
|
||||
visibilitySetting,
|
||||
app,
|
||||
enabled = true,
|
||||
}) {
|
||||
this.element = $el("button", {
|
||||
onmouseenter: () => {
|
||||
this.isOver = true;
|
||||
if(this.overIcon) {
|
||||
this.updateIcon();
|
||||
}
|
||||
},
|
||||
onmouseleave: () => {
|
||||
this.isOver = false;
|
||||
if(this.overIcon) {
|
||||
this.updateIcon();
|
||||
}
|
||||
}
|
||||
|
||||
}, [this.iconElement, this.contentElement]);
|
||||
|
||||
this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon }));
|
||||
this.overIcon = prop(this, "overIcon", overIcon, () => {
|
||||
if(this.isOver) {
|
||||
this.updateIcon();
|
||||
}
|
||||
});
|
||||
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
|
||||
this.content = prop(
|
||||
this,
|
||||
"content",
|
||||
content,
|
||||
toggleElement(this.contentElement, {
|
||||
onShow: (el, v) => {
|
||||
if (typeof v === "string") {
|
||||
el.textContent = v;
|
||||
} else {
|
||||
el.replaceChildren(v);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
|
||||
if (v) {
|
||||
this.element.title = v;
|
||||
} else {
|
||||
this.element.removeAttribute("title");
|
||||
}
|
||||
});
|
||||
this.classList = prop(this, "classList", classList, this.updateClasses);
|
||||
this.hidden = prop(this, "hidden", false, this.updateClasses);
|
||||
this.enabled = prop(this, "enabled", enabled, () => {
|
||||
this.updateClasses();
|
||||
this.element.disabled = !this.enabled;
|
||||
});
|
||||
this.action = prop(this, "action", action);
|
||||
this.element.addEventListener("click", (e) => {
|
||||
if (this.popup) {
|
||||
// we are either a touch device or triggered by click not hover
|
||||
if (!this.#over) {
|
||||
this.popup.toggle();
|
||||
}
|
||||
}
|
||||
this.action?.(e, this);
|
||||
});
|
||||
|
||||
if (visibilitySetting?.id) {
|
||||
const settingUpdated = () => {
|
||||
this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue;
|
||||
};
|
||||
app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated);
|
||||
settingUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
|
||||
updateClasses = () => {
|
||||
const internalClasses = [];
|
||||
if (this.hidden) {
|
||||
internalClasses.push("hidden");
|
||||
}
|
||||
if (!this.enabled) {
|
||||
internalClasses.push("disabled");
|
||||
}
|
||||
if (this.popup) {
|
||||
if (this.#popupOpen) {
|
||||
internalClasses.push("popup-open");
|
||||
} else {
|
||||
internalClasses.push("popup-closed");
|
||||
}
|
||||
}
|
||||
applyClasses(this.element, this.classList, ...internalClasses);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param { import("./popup.js").ComfyPopup } popup
|
||||
* @param { "click" | "hover" } mode
|
||||
*/
|
||||
withPopup(popup, mode = "click") {
|
||||
this.popup = popup;
|
||||
|
||||
if (mode === "hover") {
|
||||
for (const el of [this.element, this.popup.element]) {
|
||||
el.addEventListener("mouseenter", () => {
|
||||
this.popup.open = !!++this.#over;
|
||||
});
|
||||
el.addEventListener("mouseleave", () => {
|
||||
this.popup.open = !!--this.#over;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
popup.addEventListener("change", () => {
|
||||
this.#popupOpen = popup.open;
|
||||
this.updateClasses();
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
45
web/scripts/ui/components/buttonGroup.js
Normal file
45
web/scripts/ui/components/buttonGroup.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @ts-check
|
||||
|
||||
import { $el } from "../../ui.js";
|
||||
import { ComfyButton } from "./button.js";
|
||||
import { prop } from "../../utils.js";
|
||||
|
||||
export class ComfyButtonGroup {
|
||||
element = $el("div.comfyui-button-group");
|
||||
|
||||
/** @param {Array<ComfyButton | HTMLElement>} buttons */
|
||||
constructor(...buttons) {
|
||||
this.buttons = prop(this, "buttons", buttons, () => this.update());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ComfyButton} button
|
||||
* @param {number} index
|
||||
*/
|
||||
insert(button, index) {
|
||||
this.buttons.splice(index, 0, button);
|
||||
this.update();
|
||||
}
|
||||
|
||||
/** @param {ComfyButton} button */
|
||||
append(button) {
|
||||
this.buttons.push(button);
|
||||
this.update();
|
||||
}
|
||||
|
||||
/** @param {ComfyButton|number} indexOrButton */
|
||||
remove(indexOrButton) {
|
||||
if (typeof indexOrButton !== "number") {
|
||||
indexOrButton = this.buttons.indexOf(indexOrButton);
|
||||
}
|
||||
if (indexOrButton > -1) {
|
||||
const r = this.buttons.splice(indexOrButton, 1);
|
||||
this.update();
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
|
||||
}
|
||||
}
|
128
web/scripts/ui/components/popup.js
Normal file
128
web/scripts/ui/components/popup.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// @ts-check
|
||||
|
||||
import { prop } from "../../utils.js";
|
||||
import { $el } from "../../ui.js";
|
||||
import { applyClasses } from "../utils.js";
|
||||
|
||||
export class ComfyPopup extends EventTarget {
|
||||
element = $el("div.comfyui-popup");
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* target: HTMLElement,
|
||||
* container?: HTMLElement,
|
||||
* classList?: import("../utils.js").ClassList,
|
||||
* ignoreTarget?: boolean,
|
||||
* closeOnEscape?: boolean,
|
||||
* position?: "absolute" | "relative",
|
||||
* horizontal?: "left" | "right"
|
||||
* }} param0
|
||||
* @param {...HTMLElement} children
|
||||
*/
|
||||
constructor(
|
||||
{
|
||||
target,
|
||||
container = document.body,
|
||||
classList = "",
|
||||
ignoreTarget = true,
|
||||
closeOnEscape = true,
|
||||
position = "absolute",
|
||||
horizontal = "left",
|
||||
},
|
||||
...children
|
||||
) {
|
||||
super();
|
||||
this.target = target;
|
||||
this.ignoreTarget = ignoreTarget;
|
||||
this.container = container;
|
||||
this.position = position;
|
||||
this.closeOnEscape = closeOnEscape;
|
||||
this.horizontal = horizontal;
|
||||
|
||||
container.append(this.element);
|
||||
|
||||
this.children = prop(this, "children", children, () => {
|
||||
this.element.replaceChildren(...this.children);
|
||||
this.update();
|
||||
});
|
||||
this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal));
|
||||
this.open = prop(this, "open", false, (v, o) => {
|
||||
if (v === o) return;
|
||||
if (v) {
|
||||
this.#show();
|
||||
} else {
|
||||
this.#hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
}
|
||||
|
||||
#hide() {
|
||||
this.element.classList.remove("open");
|
||||
window.removeEventListener("resize", this.update);
|
||||
window.removeEventListener("click", this.#clickHandler, { capture: true });
|
||||
window.removeEventListener("keydown", this.#escHandler, { capture: true });
|
||||
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
|
||||
#show() {
|
||||
this.element.classList.add("open");
|
||||
this.update();
|
||||
|
||||
window.addEventListener("resize", this.update);
|
||||
window.addEventListener("click", this.#clickHandler, { capture: true });
|
||||
if (this.closeOnEscape) {
|
||||
window.addEventListener("keydown", this.#escHandler, { capture: true });
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("open"));
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
|
||||
#escHandler = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
this.open = false;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
#clickHandler = (e) => {
|
||||
/** @type {any} */
|
||||
const target = e.target;
|
||||
if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) {
|
||||
this.open = false;
|
||||
}
|
||||
};
|
||||
|
||||
update = () => {
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
this.element.style.setProperty("--bottom", "unset");
|
||||
if (this.position === "absolute") {
|
||||
if (this.horizontal === "left") {
|
||||
this.element.style.setProperty("--left", rect.left + "px");
|
||||
} else {
|
||||
this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px");
|
||||
}
|
||||
this.element.style.setProperty("--top", rect.bottom + "px");
|
||||
this.element.style.setProperty("--limit", rect.bottom + "px");
|
||||
} else {
|
||||
this.element.style.setProperty("--left", 0 + "px");
|
||||
this.element.style.setProperty("--top", rect.height + "px");
|
||||
this.element.style.setProperty("--limit", rect.height + "px");
|
||||
}
|
||||
|
||||
const thisRect = this.element.getBoundingClientRect();
|
||||
if (thisRect.height < 30) {
|
||||
// Move up instead
|
||||
this.element.style.setProperty("--top", "unset");
|
||||
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
|
||||
this.element.style.setProperty("--limit", rect.height + 5 + "px");
|
||||
}
|
||||
};
|
||||
}
|
43
web/scripts/ui/components/splitButton.js
Normal file
43
web/scripts/ui/components/splitButton.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @ts-check
|
||||
|
||||
import { $el } from "../../ui.js";
|
||||
import { ComfyButton } from "./button.js";
|
||||
import { prop } from "../../utils.js";
|
||||
import { ComfyPopup } from "./popup.js";
|
||||
|
||||
export class ComfySplitButton {
|
||||
/**
|
||||
* @param {{
|
||||
* primary: ComfyButton,
|
||||
* mode?: "hover" | "click",
|
||||
* horizontal?: "left" | "right",
|
||||
* position?: "relative" | "absolute"
|
||||
* }} param0
|
||||
* @param {Array<ComfyButton> | Array<HTMLElement>} items
|
||||
*/
|
||||
constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) {
|
||||
this.arrow = new ComfyButton({
|
||||
icon: "chevron-down",
|
||||
});
|
||||
this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [
|
||||
$el("div.comfyui-split-primary", primary.element),
|
||||
$el("div.comfyui-split-arrow", this.arrow.element),
|
||||
]);
|
||||
this.popup = new ComfyPopup({
|
||||
target: this.element,
|
||||
container: position === "relative" ? this.element : document.body,
|
||||
classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
|
||||
closeOnEscape: mode === "click",
|
||||
position,
|
||||
horizontal,
|
||||
});
|
||||
|
||||
this.arrow.withPopup(this.popup, mode);
|
||||
|
||||
this.items = prop(this, "items", items, () => this.update());
|
||||
}
|
||||
|
||||
update() {
|
||||
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user