mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 09:30:06 +00:00
Apply new code format standard (#217)
This commit is contained in:
@@ -1,64 +1,64 @@
|
||||
import { ComfyDialog } from "../dialog";
|
||||
import { $el } from "../../ui";
|
||||
import { ComfyDialog } from '../dialog'
|
||||
import { $el } from '../../ui'
|
||||
|
||||
export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
#resolve: (value: any) => void;
|
||||
#resolve: (value: any) => void
|
||||
|
||||
constructor(actions?: Array<string | { value?: any; text: string }>) {
|
||||
super(
|
||||
"dialog.comfy-dialog.comfyui-dialog",
|
||||
'dialog.comfy-dialog.comfyui-dialog',
|
||||
actions?.map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
if (typeof opt === 'string') {
|
||||
opt = { text: opt }
|
||||
}
|
||||
return $el("button.comfyui-button", {
|
||||
type: "button",
|
||||
return $el('button.comfyui-button', {
|
||||
type: 'button',
|
||||
textContent: opt.text,
|
||||
onclick: () => this.close(opt.value ?? opt.text),
|
||||
});
|
||||
onclick: () => this.close(opt.value ?? opt.text)
|
||||
})
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
show(html: string | HTMLElement | HTMLElement[]) {
|
||||
this.element.addEventListener("close", () => {
|
||||
this.close();
|
||||
});
|
||||
this.element.addEventListener('close', () => {
|
||||
this.close()
|
||||
})
|
||||
|
||||
super.show(html);
|
||||
super.show(html)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
this.#resolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
showModal(html: string | HTMLElement | HTMLElement[]) {
|
||||
this.element.addEventListener("close", () => {
|
||||
this.close();
|
||||
});
|
||||
this.element.addEventListener('close', () => {
|
||||
this.close()
|
||||
})
|
||||
|
||||
super.show(html);
|
||||
this.element.showModal();
|
||||
super.show(html)
|
||||
this.element.showModal()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
this.#resolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
close(result = null) {
|
||||
this.#resolve(result);
|
||||
this.element.close();
|
||||
super.close();
|
||||
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)];
|
||||
const dialog = new ComfyAsyncDialog(actions)
|
||||
const content = [$el('span', message)]
|
||||
if (title) {
|
||||
content.unshift($el("h3", title));
|
||||
content.unshift($el('h3', title))
|
||||
}
|
||||
const res = await dialog.showModal(content);
|
||||
dialog.element.remove();
|
||||
return res;
|
||||
const res = await dialog.showModal(content)
|
||||
dialog.element.remove()
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { $el } from "../../ui";
|
||||
import { applyClasses, ClassList, toggleElement } from "../utils";
|
||||
import { prop } from "../../utils";
|
||||
import type { ComfyPopup } from "./popup";
|
||||
import type { ComfyComponent } from ".";
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList, toggleElement } from '../utils'
|
||||
import { prop } from '../../utils'
|
||||
import type { ComfyPopup } from './popup'
|
||||
import type { ComfyComponent } from '.'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
type ComfyButtonProps = {
|
||||
icon?: string;
|
||||
overIcon?: string;
|
||||
iconSize?: number;
|
||||
content?: string | HTMLElement;
|
||||
tooltip?: string;
|
||||
enabled?: boolean;
|
||||
action?: (e: Event, btn: ComfyButton) => void;
|
||||
classList?: ClassList;
|
||||
visibilitySetting?: { id: string; showValue: any };
|
||||
app?: ComfyApp;
|
||||
};
|
||||
icon?: string
|
||||
overIcon?: string
|
||||
iconSize?: number
|
||||
content?: string | HTMLElement
|
||||
tooltip?: string
|
||||
enabled?: boolean
|
||||
action?: (e: Event, btn: ComfyButton) => void
|
||||
classList?: ClassList
|
||||
visibilitySetting?: { id: string; showValue: any }
|
||||
app?: ComfyApp
|
||||
}
|
||||
|
||||
export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
#over = 0;
|
||||
#popupOpen = false;
|
||||
isOver = false;
|
||||
iconElement = $el("i.mdi");
|
||||
contentElement = $el("span");
|
||||
popup: ComfyPopup;
|
||||
element: HTMLElement;
|
||||
overIcon: string;
|
||||
iconSize: number;
|
||||
content: string | HTMLElement;
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
classList: ClassList;
|
||||
hidden: boolean;
|
||||
enabled: boolean;
|
||||
action: (e: Event, btn: ComfyButton) => void;
|
||||
#over = 0
|
||||
#popupOpen = false
|
||||
isOver = false
|
||||
iconElement = $el('i.mdi')
|
||||
contentElement = $el('span')
|
||||
popup: ComfyPopup
|
||||
element: HTMLElement
|
||||
overIcon: string
|
||||
iconSize: number
|
||||
content: string | HTMLElement
|
||||
icon: string
|
||||
tooltip: string
|
||||
classList: ClassList
|
||||
hidden: boolean
|
||||
enabled: boolean
|
||||
action: (e: Event, btn: ComfyButton) => void
|
||||
|
||||
constructor({
|
||||
icon,
|
||||
@@ -43,134 +43,134 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
content,
|
||||
tooltip,
|
||||
action,
|
||||
classList = "comfyui-button",
|
||||
classList = 'comfyui-button',
|
||||
visibilitySetting,
|
||||
app,
|
||||
enabled = true,
|
||||
enabled = true
|
||||
}: ComfyButtonProps) {
|
||||
this.element = $el(
|
||||
"button",
|
||||
'button',
|
||||
{
|
||||
onmouseenter: () => {
|
||||
this.isOver = true;
|
||||
this.isOver = true
|
||||
if (this.overIcon) {
|
||||
this.updateIcon();
|
||||
this.updateIcon()
|
||||
}
|
||||
},
|
||||
onmouseleave: () => {
|
||||
this.isOver = false;
|
||||
this.isOver = false
|
||||
if (this.overIcon) {
|
||||
this.updateIcon();
|
||||
this.updateIcon()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
[this.iconElement, this.contentElement]
|
||||
);
|
||||
)
|
||||
|
||||
this.icon = prop(
|
||||
this,
|
||||
"icon",
|
||||
'icon',
|
||||
icon,
|
||||
toggleElement(this.iconElement, { onShow: this.updateIcon })
|
||||
);
|
||||
this.overIcon = prop(this, "overIcon", overIcon, () => {
|
||||
)
|
||||
this.overIcon = prop(this, 'overIcon', overIcon, () => {
|
||||
if (this.isOver) {
|
||||
this.updateIcon();
|
||||
this.updateIcon()
|
||||
}
|
||||
});
|
||||
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
|
||||
})
|
||||
this.iconSize = prop(this, 'iconSize', iconSize, this.updateIcon)
|
||||
this.content = prop(
|
||||
this,
|
||||
"content",
|
||||
'content',
|
||||
content,
|
||||
toggleElement(this.contentElement, {
|
||||
onShow: (el, v) => {
|
||||
if (typeof v === "string") {
|
||||
el.textContent = v;
|
||||
if (typeof v === 'string') {
|
||||
el.textContent = v
|
||||
} else {
|
||||
el.replaceChildren(v);
|
||||
el.replaceChildren(v)
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
|
||||
this.tooltip = prop(this, 'tooltip', tooltip, (v) => {
|
||||
if (v) {
|
||||
this.element.title = v;
|
||||
this.element.title = v
|
||||
} else {
|
||||
this.element.removeAttribute("title");
|
||||
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 as HTMLButtonElement).disabled = !this.enabled;
|
||||
});
|
||||
this.action = prop(this, "action", action);
|
||||
this.element.addEventListener("click", (e) => {
|
||||
})
|
||||
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 as HTMLButtonElement).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.popup.toggle()
|
||||
}
|
||||
}
|
||||
this.action?.(e, this);
|
||||
});
|
||||
this.action?.(e, this)
|
||||
})
|
||||
|
||||
if (visibilitySetting?.id) {
|
||||
const settingUpdated = () => {
|
||||
this.hidden =
|
||||
app.ui.settings.getSettingValue(visibilitySetting.id) !==
|
||||
visibilitySetting.showValue;
|
||||
};
|
||||
visibilitySetting.showValue
|
||||
}
|
||||
app.ui.settings.addEventListener(
|
||||
visibilitySetting.id + ".change",
|
||||
visibilitySetting.id + '.change',
|
||||
settingUpdated
|
||||
);
|
||||
settingUpdated();
|
||||
)
|
||||
settingUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
updateIcon = () =>
|
||||
(this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
|
||||
(this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? ' mdi-' + this.iconSize + 'px' : ''}`)
|
||||
updateClasses = () => {
|
||||
const internalClasses = [];
|
||||
const internalClasses = []
|
||||
if (this.hidden) {
|
||||
internalClasses.push("hidden");
|
||||
internalClasses.push('hidden')
|
||||
}
|
||||
if (!this.enabled) {
|
||||
internalClasses.push("disabled");
|
||||
internalClasses.push('disabled')
|
||||
}
|
||||
if (this.popup) {
|
||||
if (this.#popupOpen) {
|
||||
internalClasses.push("popup-open");
|
||||
internalClasses.push('popup-open')
|
||||
} else {
|
||||
internalClasses.push("popup-closed");
|
||||
internalClasses.push('popup-closed')
|
||||
}
|
||||
}
|
||||
applyClasses(this.element, this.classList, ...internalClasses);
|
||||
};
|
||||
applyClasses(this.element, this.classList, ...internalClasses)
|
||||
}
|
||||
|
||||
withPopup(popup: ComfyPopup, mode: "click" | "hover" = "click") {
|
||||
this.popup = popup;
|
||||
withPopup(popup: ComfyPopup, mode: 'click' | 'hover' = 'click') {
|
||||
this.popup = popup
|
||||
|
||||
if (mode === "hover") {
|
||||
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;
|
||||
});
|
||||
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();
|
||||
});
|
||||
popup.addEventListener('change', () => {
|
||||
this.#popupOpen = popup.open
|
||||
this.updateClasses()
|
||||
})
|
||||
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { $el } from "../../ui";
|
||||
import { ComfyButton } from "./button";
|
||||
import { prop } from "../../utils";
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyButton } from './button'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
export class ComfyButtonGroup {
|
||||
element = $el("div.comfyui-button-group");
|
||||
buttons: (HTMLElement | ComfyButton)[];
|
||||
element = $el('div.comfyui-button-group')
|
||||
buttons: (HTMLElement | ComfyButton)[]
|
||||
|
||||
constructor(...buttons: (HTMLElement | ComfyButton)[]) {
|
||||
this.buttons = prop(this, "buttons", buttons, () => this.update());
|
||||
this.buttons = prop(this, 'buttons', buttons, () => this.update())
|
||||
}
|
||||
|
||||
insert(button: ComfyButton, index: number) {
|
||||
this.buttons.splice(index, 0, button);
|
||||
this.update();
|
||||
this.buttons.splice(index, 0, button)
|
||||
this.update()
|
||||
}
|
||||
|
||||
append(button: ComfyButton) {
|
||||
this.buttons.push(button);
|
||||
this.update();
|
||||
this.buttons.push(button)
|
||||
this.update()
|
||||
}
|
||||
|
||||
remove(indexOrButton: ComfyButton | number) {
|
||||
if (typeof indexOrButton !== "number") {
|
||||
indexOrButton = this.buttons.indexOf(indexOrButton);
|
||||
if (typeof indexOrButton !== 'number') {
|
||||
indexOrButton = this.buttons.indexOf(indexOrButton)
|
||||
}
|
||||
if (indexOrButton > -1) {
|
||||
const r = this.buttons.splice(indexOrButton, 1);
|
||||
this.update();
|
||||
return r;
|
||||
const r = this.buttons.splice(indexOrButton, 1)
|
||||
this.update()
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
|
||||
this.element.replaceChildren(...this.buttons.map((b) => b['element'] ?? b))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface ComfyComponent<T extends HTMLElement = HTMLElement> {
|
||||
element: T;
|
||||
element: T
|
||||
}
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
import { prop } from "../../utils";
|
||||
import { $el } from "../../ui";
|
||||
import { applyClasses, ClassList } from "../utils";
|
||||
import { prop } from '../../utils'
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList } from '../utils'
|
||||
|
||||
export class ComfyPopup extends EventTarget {
|
||||
element = $el("div.comfyui-popup");
|
||||
open: boolean;
|
||||
children: HTMLElement[];
|
||||
target: HTMLElement;
|
||||
ignoreTarget: boolean;
|
||||
container: HTMLElement;
|
||||
position: string;
|
||||
closeOnEscape: boolean;
|
||||
horizontal: string;
|
||||
classList: ClassList;
|
||||
element = $el('div.comfyui-popup')
|
||||
open: boolean
|
||||
children: HTMLElement[]
|
||||
target: HTMLElement
|
||||
ignoreTarget: boolean
|
||||
container: HTMLElement
|
||||
position: string
|
||||
closeOnEscape: boolean
|
||||
horizontal: string
|
||||
classList: ClassList
|
||||
|
||||
constructor(
|
||||
{
|
||||
target,
|
||||
container = document.body,
|
||||
classList = "",
|
||||
classList = '',
|
||||
ignoreTarget = true,
|
||||
closeOnEscape = true,
|
||||
position = "absolute",
|
||||
horizontal = "left",
|
||||
position = 'absolute',
|
||||
horizontal = 'left'
|
||||
}: {
|
||||
target: HTMLElement;
|
||||
container?: HTMLElement;
|
||||
classList?: ClassList;
|
||||
ignoreTarget?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
position?: "absolute" | "relative";
|
||||
horizontal?: "left" | "right";
|
||||
target: HTMLElement
|
||||
container?: HTMLElement
|
||||
classList?: ClassList
|
||||
ignoreTarget?: boolean
|
||||
closeOnEscape?: boolean
|
||||
position?: 'absolute' | 'relative'
|
||||
horizontal?: 'left' | 'right'
|
||||
},
|
||||
...children: HTMLElement[]
|
||||
) {
|
||||
super();
|
||||
this.target = target;
|
||||
this.ignoreTarget = ignoreTarget;
|
||||
this.container = container;
|
||||
this.position = position;
|
||||
this.closeOnEscape = closeOnEscape;
|
||||
this.horizontal = horizontal;
|
||||
super()
|
||||
this.target = target
|
||||
this.ignoreTarget = ignoreTarget
|
||||
this.container = container
|
||||
this.position = position
|
||||
this.closeOnEscape = closeOnEscape
|
||||
this.horizontal = horizontal
|
||||
|
||||
container.append(this.element);
|
||||
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;
|
||||
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();
|
||||
this.#show()
|
||||
} else {
|
||||
this.#hide();
|
||||
this.#hide()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
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.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"));
|
||||
this.dispatchEvent(new CustomEvent('close'))
|
||||
this.dispatchEvent(new CustomEvent('change'))
|
||||
}
|
||||
|
||||
#show() {
|
||||
this.element.classList.add("open");
|
||||
this.update();
|
||||
this.element.classList.add('open')
|
||||
this.update()
|
||||
|
||||
window.addEventListener("resize", this.update);
|
||||
window.addEventListener("click", this.#clickHandler, { capture: true });
|
||||
window.addEventListener('resize', this.update)
|
||||
window.addEventListener('click', this.#clickHandler, { capture: true })
|
||||
if (this.closeOnEscape) {
|
||||
window.addEventListener("keydown", this.#escHandler, { capture: true });
|
||||
window.addEventListener('keydown', this.#escHandler, { capture: true })
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("open"));
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
this.dispatchEvent(new CustomEvent('open'))
|
||||
this.dispatchEvent(new CustomEvent('change'))
|
||||
}
|
||||
|
||||
#escHandler = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
this.open = false;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (e.key === 'Escape') {
|
||||
this.open = false
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#clickHandler = (e) => {
|
||||
/** @type {any} */
|
||||
const target = e.target;
|
||||
const target = e.target
|
||||
if (
|
||||
!this.element.contains(target) &&
|
||||
this.ignoreTarget &&
|
||||
!this.target.contains(target)
|
||||
) {
|
||||
this.open = false;
|
||||
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");
|
||||
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"
|
||||
);
|
||||
'--left',
|
||||
rect.right - this.element.clientWidth + 'px'
|
||||
)
|
||||
}
|
||||
this.element.style.setProperty("--top", rect.bottom + "px");
|
||||
this.element.style.setProperty("--limit", rect.bottom + "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");
|
||||
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();
|
||||
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");
|
||||
this.element.style.setProperty('--top', 'unset')
|
||||
this.element.style.setProperty('--bottom', rect.height + 5 + 'px')
|
||||
this.element.style.setProperty('--limit', rect.height + 5 + 'px')
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { $el } from "../../ui";
|
||||
import { ComfyButton } from "./button";
|
||||
import { prop } from "../../utils";
|
||||
import { ComfyPopup } from "./popup";
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyButton } from './button'
|
||||
import { prop } from '../../utils'
|
||||
import { ComfyPopup } from './popup'
|
||||
|
||||
export class ComfySplitButton {
|
||||
arrow: ComfyButton;
|
||||
element: HTMLElement;
|
||||
popup: ComfyPopup;
|
||||
items: Array<HTMLElement | ComfyButton>;
|
||||
arrow: ComfyButton
|
||||
element: HTMLElement
|
||||
popup: ComfyPopup
|
||||
items: Array<HTMLElement | ComfyButton>
|
||||
|
||||
constructor(
|
||||
{
|
||||
primary,
|
||||
mode,
|
||||
horizontal = "left",
|
||||
position = "relative",
|
||||
horizontal = 'left',
|
||||
position = 'relative'
|
||||
}: {
|
||||
primary: ComfyButton;
|
||||
mode?: "hover" | "click";
|
||||
horizontal?: "left" | "right";
|
||||
position?: "relative" | "absolute";
|
||||
primary: ComfyButton
|
||||
mode?: 'hover' | 'click'
|
||||
horizontal?: 'left' | 'right'
|
||||
position?: 'relative' | 'absolute'
|
||||
},
|
||||
...items: Array<HTMLElement | ComfyButton>
|
||||
) {
|
||||
this.arrow = new ComfyButton({
|
||||
icon: "chevron-down",
|
||||
});
|
||||
icon: 'chevron-down'
|
||||
})
|
||||
this.element = $el(
|
||||
"div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""),
|
||||
'div.comfyui-split-button' + (mode === 'hover' ? '.hover' : ''),
|
||||
[
|
||||
$el("div.comfyui-split-primary", primary.element),
|
||||
$el("div.comfyui-split-arrow", this.arrow.element),
|
||||
$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,
|
||||
container: position === 'relative' ? this.element : document.body,
|
||||
classList:
|
||||
"comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
|
||||
closeOnEscape: mode === "click",
|
||||
'comfyui-split-button-popup' + (mode === 'hover' ? ' hover' : ''),
|
||||
closeOnEscape: mode === 'click',
|
||||
position,
|
||||
horizontal,
|
||||
});
|
||||
horizontal
|
||||
})
|
||||
|
||||
this.arrow.withPopup(this.popup, mode);
|
||||
this.arrow.withPopup(this.popup, mode)
|
||||
|
||||
this.items = prop(this, "items", items, () => this.update());
|
||||
this.items = prop(this, 'items', items, () => this.update())
|
||||
}
|
||||
|
||||
update() {
|
||||
this.popup.element.replaceChildren(
|
||||
...this.items.map((b) => ("element" in b ? b.element : b))
|
||||
);
|
||||
...this.items.map((b) => ('element' in b ? b.element : b))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import { $el } from "../ui";
|
||||
import { $el } from '../ui'
|
||||
|
||||
export class ComfyDialog<
|
||||
T extends HTMLElement = HTMLElement,
|
||||
T extends HTMLElement = HTMLElement
|
||||
> extends EventTarget {
|
||||
element: T;
|
||||
textElement: HTMLElement;
|
||||
#buttons: HTMLButtonElement[] | null;
|
||||
element: T
|
||||
textElement: HTMLElement
|
||||
#buttons: HTMLButtonElement[] | null
|
||||
|
||||
constructor(type = "div", buttons = null) {
|
||||
super();
|
||||
this.#buttons = buttons;
|
||||
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
||||
$el("div.comfy-modal-content", [
|
||||
$el("p", { $: (p) => (this.textElement = p) }),
|
||||
...this.createButtons(),
|
||||
]),
|
||||
]) as T;
|
||||
constructor(type = 'div', buttons = null) {
|
||||
super()
|
||||
this.#buttons = buttons
|
||||
this.element = $el(type + '.comfy-modal', { parent: document.body }, [
|
||||
$el('div.comfy-modal-content', [
|
||||
$el('p', { $: (p) => (this.textElement = p) }),
|
||||
...this.createButtons()
|
||||
])
|
||||
]) as T
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
return (
|
||||
this.#buttons ?? [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => this.close(),
|
||||
}),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Close',
|
||||
onclick: () => this.close()
|
||||
})
|
||||
]
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
this.element.style.display = 'none'
|
||||
}
|
||||
|
||||
show(html) {
|
||||
if (typeof html === "string") {
|
||||
this.textElement.innerHTML = html;
|
||||
if (typeof html === 'string') {
|
||||
this.textElement.innerHTML = html
|
||||
} else {
|
||||
this.textElement.replaceChildren(
|
||||
...(html instanceof Array ? html : [html])
|
||||
);
|
||||
)
|
||||
}
|
||||
this.element.style.display = "flex";
|
||||
this.element.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import { $el } from "../ui";
|
||||
import { $el } from '../ui'
|
||||
|
||||
$el("style", {
|
||||
$el('style', {
|
||||
parent: document.head,
|
||||
textContent: `
|
||||
.draggable-item {
|
||||
@@ -40,261 +40,261 @@ $el("style", {
|
||||
.draggable-item.is-draggable {
|
||||
z-index: 10;
|
||||
}
|
||||
`,
|
||||
});
|
||||
`
|
||||
})
|
||||
|
||||
export class DraggableList extends EventTarget {
|
||||
listContainer;
|
||||
draggableItem;
|
||||
pointerStartX;
|
||||
pointerStartY;
|
||||
scrollYMax;
|
||||
itemsGap = 0;
|
||||
items = [];
|
||||
itemSelector;
|
||||
handleClass = "drag-handle";
|
||||
off = [];
|
||||
offDrag = [];
|
||||
listContainer
|
||||
draggableItem
|
||||
pointerStartX
|
||||
pointerStartY
|
||||
scrollYMax
|
||||
itemsGap = 0
|
||||
items = []
|
||||
itemSelector
|
||||
handleClass = 'drag-handle'
|
||||
off = []
|
||||
offDrag = []
|
||||
|
||||
constructor(element, itemSelector) {
|
||||
super();
|
||||
this.listContainer = element;
|
||||
this.itemSelector = itemSelector;
|
||||
super()
|
||||
this.listContainer = element
|
||||
this.itemSelector = itemSelector
|
||||
|
||||
if (!this.listContainer) return;
|
||||
if (!this.listContainer) return
|
||||
|
||||
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
|
||||
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
|
||||
this.off.push(this.on(document, "mouseup", this.dragEnd));
|
||||
this.off.push(this.on(document, "touchend", this.dragEnd));
|
||||
this.off.push(this.on(this.listContainer, 'mousedown', this.dragStart))
|
||||
this.off.push(this.on(this.listContainer, 'touchstart', this.dragStart))
|
||||
this.off.push(this.on(document, 'mouseup', this.dragEnd))
|
||||
this.off.push(this.on(document, 'touchend', this.dragEnd))
|
||||
}
|
||||
|
||||
getAllItems() {
|
||||
if (!this.items?.length) {
|
||||
this.items = Array.from(
|
||||
this.listContainer.querySelectorAll(this.itemSelector)
|
||||
);
|
||||
)
|
||||
this.items.forEach((element) => {
|
||||
element.classList.add("is-idle");
|
||||
});
|
||||
element.classList.add('is-idle')
|
||||
})
|
||||
}
|
||||
return this.items;
|
||||
return this.items
|
||||
}
|
||||
|
||||
getIdleItems() {
|
||||
return this.getAllItems().filter((item) =>
|
||||
item.classList.contains("is-idle")
|
||||
);
|
||||
item.classList.contains('is-idle')
|
||||
)
|
||||
}
|
||||
|
||||
isItemAbove(item) {
|
||||
return item.hasAttribute("data-is-above");
|
||||
return item.hasAttribute('data-is-above')
|
||||
}
|
||||
|
||||
isItemToggled(item) {
|
||||
return item.hasAttribute("data-is-toggled");
|
||||
return item.hasAttribute('data-is-toggled')
|
||||
}
|
||||
|
||||
on(source, event, listener, options?) {
|
||||
listener = listener.bind(this);
|
||||
source.addEventListener(event, listener, options);
|
||||
return () => source.removeEventListener(event, listener);
|
||||
listener = listener.bind(this)
|
||||
source.addEventListener(event, listener, options)
|
||||
return () => source.removeEventListener(event, listener)
|
||||
}
|
||||
|
||||
dragStart(e) {
|
||||
if (e.target.classList.contains(this.handleClass)) {
|
||||
this.draggableItem = e.target.closest(this.itemSelector);
|
||||
this.draggableItem = e.target.closest(this.itemSelector)
|
||||
}
|
||||
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return
|
||||
|
||||
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
||||
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
||||
this.pointerStartX = e.clientX || e.touches[0].clientX
|
||||
this.pointerStartY = e.clientY || e.touches[0].clientY
|
||||
this.scrollYMax =
|
||||
this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
||||
this.listContainer.scrollHeight - this.listContainer.clientHeight
|
||||
|
||||
this.setItemsGap();
|
||||
this.initDraggableItem();
|
||||
this.initItemsState();
|
||||
this.setItemsGap()
|
||||
this.initDraggableItem()
|
||||
this.initItemsState()
|
||||
|
||||
this.offDrag.push(this.on(document, "mousemove", this.drag));
|
||||
this.offDrag.push(this.on(document, 'mousemove', this.drag))
|
||||
this.offDrag.push(
|
||||
this.on(document, "touchmove", this.drag, { passive: false })
|
||||
);
|
||||
this.on(document, 'touchmove', this.drag, { passive: false })
|
||||
)
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragstart", {
|
||||
new CustomEvent('dragstart', {
|
||||
detail: {
|
||||
element: this.draggableItem,
|
||||
position: this.getAllItems().indexOf(this.draggableItem),
|
||||
},
|
||||
position: this.getAllItems().indexOf(this.draggableItem)
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
setItemsGap() {
|
||||
if (this.getIdleItems().length <= 1) {
|
||||
this.itemsGap = 0;
|
||||
return;
|
||||
this.itemsGap = 0
|
||||
return
|
||||
}
|
||||
|
||||
const item1 = this.getIdleItems()[0];
|
||||
const item2 = this.getIdleItems()[1];
|
||||
const item1 = this.getIdleItems()[0]
|
||||
const item2 = this.getIdleItems()[1]
|
||||
|
||||
const item1Rect = item1.getBoundingClientRect();
|
||||
const item2Rect = item2.getBoundingClientRect();
|
||||
const item1Rect = item1.getBoundingClientRect()
|
||||
const item2Rect = item2.getBoundingClientRect()
|
||||
|
||||
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
|
||||
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top)
|
||||
}
|
||||
|
||||
initItemsState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
||||
item.dataset.isAbove = "";
|
||||
item.dataset.isAbove = ''
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
initDraggableItem() {
|
||||
this.draggableItem.classList.remove("is-idle");
|
||||
this.draggableItem.classList.add("is-draggable");
|
||||
this.draggableItem.classList.remove('is-idle')
|
||||
this.draggableItem.classList.add('is-draggable')
|
||||
}
|
||||
|
||||
drag(e) {
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return
|
||||
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const clientX = e.clientX || e.touches[0].clientX;
|
||||
const clientY = e.clientY || e.touches[0].clientY;
|
||||
const clientX = e.clientX || e.touches[0].clientX
|
||||
const clientY = e.clientY || e.touches[0].clientY
|
||||
|
||||
const listRect = this.listContainer.getBoundingClientRect();
|
||||
const listRect = this.listContainer.getBoundingClientRect()
|
||||
|
||||
if (clientY > listRect.bottom) {
|
||||
if (this.listContainer.scrollTop < this.scrollYMax) {
|
||||
this.listContainer.scrollBy(0, 10);
|
||||
this.pointerStartY -= 10;
|
||||
this.listContainer.scrollBy(0, 10)
|
||||
this.pointerStartY -= 10
|
||||
}
|
||||
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
||||
this.pointerStartY += 10;
|
||||
this.listContainer.scrollBy(0, -10);
|
||||
this.pointerStartY += 10
|
||||
this.listContainer.scrollBy(0, -10)
|
||||
}
|
||||
|
||||
const pointerOffsetX = clientX - this.pointerStartX;
|
||||
const pointerOffsetY = clientY - this.pointerStartY;
|
||||
const pointerOffsetX = clientX - this.pointerStartX
|
||||
const pointerOffsetY = clientY - this.pointerStartY
|
||||
|
||||
this.updateIdleItemsStateAndPosition();
|
||||
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
|
||||
this.updateIdleItemsStateAndPosition()
|
||||
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`
|
||||
}
|
||||
|
||||
updateIdleItemsStateAndPosition() {
|
||||
const draggableItemRect = this.draggableItem.getBoundingClientRect();
|
||||
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
|
||||
const draggableItemRect = this.draggableItem.getBoundingClientRect()
|
||||
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2
|
||||
|
||||
// Update state
|
||||
this.getIdleItems().forEach((item) => {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const itemY = itemRect.top + itemRect.height / 2;
|
||||
const itemRect = item.getBoundingClientRect()
|
||||
const itemY = itemRect.top + itemRect.height / 2
|
||||
if (this.isItemAbove(item)) {
|
||||
if (draggableItemY <= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
item.dataset.isToggled = ''
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
delete item.dataset.isToggled
|
||||
}
|
||||
} else {
|
||||
if (draggableItemY >= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
item.dataset.isToggled = ''
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
delete item.dataset.isToggled
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Update position
|
||||
this.getIdleItems().forEach((item) => {
|
||||
if (this.isItemToggled(item)) {
|
||||
const direction = this.isItemAbove(item) ? 1 : -1;
|
||||
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
|
||||
const direction = this.isItemAbove(item) ? 1 : -1
|
||||
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`
|
||||
} else {
|
||||
item.style.transform = "";
|
||||
item.style.transform = ''
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
dragEnd() {
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return
|
||||
|
||||
this.applyNewItemsOrder();
|
||||
this.cleanup();
|
||||
this.applyNewItemsOrder()
|
||||
this.cleanup()
|
||||
}
|
||||
|
||||
applyNewItemsOrder() {
|
||||
const reorderedItems = [];
|
||||
const reorderedItems = []
|
||||
|
||||
let oldPosition = -1;
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index;
|
||||
return;
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item;
|
||||
return;
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
|
||||
reorderedItems[newIndex] = item;
|
||||
});
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index];
|
||||
if (typeof item === "undefined") {
|
||||
reorderedItems[index] = this.draggableItem;
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem
|
||||
}
|
||||
}
|
||||
|
||||
reorderedItems.forEach((item) => {
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
this.listContainer.appendChild(item)
|
||||
})
|
||||
|
||||
this.items = reorderedItems;
|
||||
this.items = reorderedItems
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragend", {
|
||||
new CustomEvent('dragend', {
|
||||
detail: {
|
||||
element: this.draggableItem,
|
||||
oldPosition,
|
||||
newPosition: reorderedItems.indexOf(this.draggableItem),
|
||||
},
|
||||
newPosition: reorderedItems.indexOf(this.draggableItem)
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.itemsGap = 0;
|
||||
this.items = [];
|
||||
this.unsetDraggableItem();
|
||||
this.unsetItemState();
|
||||
this.itemsGap = 0
|
||||
this.items = []
|
||||
this.unsetDraggableItem()
|
||||
this.unsetItemState()
|
||||
|
||||
this.offDrag.forEach((f) => f());
|
||||
this.offDrag = [];
|
||||
this.offDrag.forEach((f) => f())
|
||||
this.offDrag = []
|
||||
}
|
||||
|
||||
unsetDraggableItem() {
|
||||
this.draggableItem.style = null;
|
||||
this.draggableItem.classList.remove("is-draggable");
|
||||
this.draggableItem.classList.add("is-idle");
|
||||
this.draggableItem = null;
|
||||
this.draggableItem.style = null
|
||||
this.draggableItem.classList.remove('is-draggable')
|
||||
this.draggableItem.classList.add('is-idle')
|
||||
this.draggableItem = null
|
||||
}
|
||||
|
||||
unsetItemState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
delete item.dataset.isAbove;
|
||||
delete item.dataset.isToggled;
|
||||
item.style.transform = "";
|
||||
});
|
||||
delete item.dataset.isAbove
|
||||
delete item.dataset.isToggled
|
||||
item.style.transform = ''
|
||||
})
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.off.forEach((f) => f());
|
||||
this.off.forEach((f) => f())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
import { app } from "../app";
|
||||
import { $el } from "../ui";
|
||||
import { app } from '../app'
|
||||
import { $el } from '../ui'
|
||||
|
||||
export function calculateImageGrid(imgs, dw, dh) {
|
||||
let best = 0;
|
||||
let w = imgs[0].naturalWidth;
|
||||
let h = imgs[0].naturalHeight;
|
||||
const numImages = imgs.length;
|
||||
let best = 0
|
||||
let w = imgs[0].naturalWidth
|
||||
let h = imgs[0].naturalHeight
|
||||
const numImages = imgs.length
|
||||
|
||||
let cellWidth, cellHeight, cols, rows, shiftX;
|
||||
let cellWidth, cellHeight, cols, rows, shiftX
|
||||
// compact style
|
||||
for (let c = 1; c <= numImages; c++) {
|
||||
const r = Math.ceil(numImages / c);
|
||||
const cW = dw / c;
|
||||
const cH = dh / r;
|
||||
const scaleX = cW / w;
|
||||
const scaleY = cH / h;
|
||||
const r = Math.ceil(numImages / c)
|
||||
const cW = dw / c
|
||||
const cH = dh / r
|
||||
const scaleX = cW / w
|
||||
const scaleY = cH / h
|
||||
|
||||
const scale = Math.min(scaleX, scaleY, 1);
|
||||
const imageW = w * scale;
|
||||
const imageH = h * scale;
|
||||
const area = imageW * imageH * numImages;
|
||||
const scale = Math.min(scaleX, scaleY, 1)
|
||||
const imageW = w * scale
|
||||
const imageH = h * scale
|
||||
const area = imageW * imageH * numImages
|
||||
|
||||
if (area > best) {
|
||||
best = area;
|
||||
cellWidth = imageW;
|
||||
cellHeight = imageH;
|
||||
cols = c;
|
||||
rows = r;
|
||||
shiftX = c * ((cW - imageW) / 2);
|
||||
best = area
|
||||
cellWidth = imageW
|
||||
cellHeight = imageH
|
||||
cols = c
|
||||
rows = r
|
||||
shiftX = c * ((cW - imageW) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
return { cellWidth, cellHeight, cols, rows, shiftX };
|
||||
return { cellWidth, cellHeight, cols, rows, shiftX }
|
||||
}
|
||||
|
||||
export function createImageHost(node) {
|
||||
const el = $el("div.comfy-img-preview");
|
||||
let currentImgs;
|
||||
let first = true;
|
||||
const el = $el('div.comfy-img-preview')
|
||||
let currentImgs
|
||||
let first = true
|
||||
|
||||
function updateSize() {
|
||||
let w = null;
|
||||
let h = null;
|
||||
let w = null
|
||||
let h = null
|
||||
|
||||
if (currentImgs) {
|
||||
let elH = el.clientHeight;
|
||||
let elH = el.clientHeight
|
||||
if (first) {
|
||||
first = false;
|
||||
first = false
|
||||
// On first run, if we are small then grow a bit
|
||||
if (elH < 190) {
|
||||
elH = 190;
|
||||
elH = 190
|
||||
}
|
||||
el.style.setProperty("--comfy-widget-min-height", elH.toString());
|
||||
el.style.setProperty('--comfy-widget-min-height', elH.toString())
|
||||
} else {
|
||||
el.style.setProperty("--comfy-widget-min-height", null);
|
||||
el.style.setProperty('--comfy-widget-min-height', null)
|
||||
}
|
||||
|
||||
const nw = node.size[0];
|
||||
({ cellWidth: w, cellHeight: h } = calculateImageGrid(
|
||||
const nw = node.size[0]
|
||||
;({ cellWidth: w, cellHeight: h } = calculateImageGrid(
|
||||
currentImgs,
|
||||
nw - 20,
|
||||
elH
|
||||
));
|
||||
w += "px";
|
||||
h += "px";
|
||||
))
|
||||
w += 'px'
|
||||
h += 'px'
|
||||
|
||||
el.style.setProperty("--comfy-img-preview-width", w);
|
||||
el.style.setProperty("--comfy-img-preview-height", h);
|
||||
el.style.setProperty('--comfy-img-preview-width', w)
|
||||
el.style.setProperty('--comfy-img-preview-height', h)
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -75,31 +75,31 @@ export function createImageHost(node) {
|
||||
if (imgs !== currentImgs) {
|
||||
if (currentImgs == null) {
|
||||
requestAnimationFrame(() => {
|
||||
updateSize();
|
||||
});
|
||||
updateSize()
|
||||
})
|
||||
}
|
||||
el.replaceChildren(...imgs);
|
||||
currentImgs = imgs;
|
||||
node.onResize(node.size);
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
el.replaceChildren(...imgs)
|
||||
currentImgs = imgs
|
||||
node.onResize(node.size)
|
||||
node.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
},
|
||||
getHeight() {
|
||||
updateSize();
|
||||
updateSize()
|
||||
},
|
||||
onDraw() {
|
||||
// Element from point uses a hittest find elements so we need to toggle pointer events
|
||||
el.style.pointerEvents = "all";
|
||||
el.style.pointerEvents = 'all'
|
||||
const over = document.elementFromPoint(
|
||||
app.canvas.mouse[0],
|
||||
app.canvas.mouse[1]
|
||||
);
|
||||
el.style.pointerEvents = "none";
|
||||
)
|
||||
el.style.pointerEvents = 'none'
|
||||
|
||||
if (!over) return;
|
||||
if (!over) return
|
||||
// Set the overIndex so Open Image etc work
|
||||
const idx = currentImgs.indexOf(over);
|
||||
node.overIndex = idx;
|
||||
},
|
||||
};
|
||||
const idx = currentImgs.indexOf(over)
|
||||
node.overIndex = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,320 +1,308 @@
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { api } from "../../api";
|
||||
import { $el } from "../../ui";
|
||||
import { downloadBlob } from "../../utils";
|
||||
import { ComfyButton } from "../components/button";
|
||||
import { ComfyButtonGroup } from "../components/buttonGroup";
|
||||
import { ComfySplitButton } from "../components/splitButton";
|
||||
import { ComfyQueueButton } from "./queueButton";
|
||||
import { ComfyWorkflowsMenu } from "./workflows";
|
||||
import { getInteruptButton } from "./interruptButton";
|
||||
import "./menu.css";
|
||||
import type { ComfySettingsDialog } from "../settings";
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { api } from '../../api'
|
||||
import { $el } from '../../ui'
|
||||
import { downloadBlob } from '../../utils'
|
||||
import { ComfyButton } from '../components/button'
|
||||
import { ComfyButtonGroup } from '../components/buttonGroup'
|
||||
import { ComfySplitButton } from '../components/splitButton'
|
||||
import { ComfyQueueButton } from './queueButton'
|
||||
import { ComfyWorkflowsMenu } from './workflows'
|
||||
import { getInteruptButton } from './interruptButton'
|
||||
import './menu.css'
|
||||
import type { ComfySettingsDialog } from '../settings'
|
||||
|
||||
type MenuPosition = "Disabled" | "Top" | "Bottom";
|
||||
type MenuPosition = 'Disabled' | 'Top' | 'Bottom'
|
||||
|
||||
const collapseOnMobile = (t) => {
|
||||
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
|
||||
return t;
|
||||
};
|
||||
;(t.element ?? t).classList.add('comfyui-menu-mobile-collapse')
|
||||
return t
|
||||
}
|
||||
const showOnMobile = (t) => {
|
||||
(t.element ?? t).classList.add("lt-lg-show");
|
||||
return t;
|
||||
};
|
||||
;(t.element ?? t).classList.add('lt-lg-show')
|
||||
return t
|
||||
}
|
||||
|
||||
export class ComfyAppMenu {
|
||||
#sizeBreak = "lg";
|
||||
#sizeBreak = 'lg'
|
||||
#lastSizeBreaks = {
|
||||
lg: null,
|
||||
md: null,
|
||||
sm: null,
|
||||
xs: null,
|
||||
};
|
||||
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
|
||||
#cachedInnerSize = null;
|
||||
#cacheTimeout = null;
|
||||
app: ComfyApp;
|
||||
workflows: ComfyWorkflowsMenu;
|
||||
logo: HTMLElement;
|
||||
saveButton: ComfySplitButton;
|
||||
actionsGroup: ComfyButtonGroup;
|
||||
settingsGroup: ComfyButtonGroup;
|
||||
viewGroup: ComfyButtonGroup;
|
||||
mobileMenuButton: ComfyButton;
|
||||
element: HTMLElement;
|
||||
menuPositionSetting: ReturnType<ComfySettingsDialog["addSetting"]>;
|
||||
position: MenuPosition;
|
||||
xs: null
|
||||
}
|
||||
#sizeBreaks = Object.keys(this.#lastSizeBreaks)
|
||||
#cachedInnerSize = null
|
||||
#cacheTimeout = null
|
||||
app: ComfyApp
|
||||
workflows: ComfyWorkflowsMenu
|
||||
logo: HTMLElement
|
||||
saveButton: ComfySplitButton
|
||||
actionsGroup: ComfyButtonGroup
|
||||
settingsGroup: ComfyButtonGroup
|
||||
viewGroup: ComfyButtonGroup
|
||||
mobileMenuButton: ComfyButton
|
||||
element: HTMLElement
|
||||
menuPositionSetting: ReturnType<ComfySettingsDialog['addSetting']>
|
||||
position: MenuPosition
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
this.app = app;
|
||||
this.app = app
|
||||
|
||||
this.workflows = new ComfyWorkflowsMenu(app);
|
||||
this.workflows = new ComfyWorkflowsMenu(app)
|
||||
const getSaveButton = (t?: string) =>
|
||||
new ComfyButton({
|
||||
icon: "content-save",
|
||||
tooltip: "Save the current workflow",
|
||||
icon: 'content-save',
|
||||
tooltip: 'Save the current workflow',
|
||||
action: () => app.workflowManager.activeWorkflow.save(),
|
||||
content: t,
|
||||
});
|
||||
content: t
|
||||
})
|
||||
|
||||
this.logo = $el(
|
||||
"h1.comfyui-logo.nlg-hide",
|
||||
{ title: "ComfyUI" },
|
||||
"ComfyUI"
|
||||
);
|
||||
this.logo = $el('h1.comfyui-logo.nlg-hide', { title: 'ComfyUI' }, 'ComfyUI')
|
||||
this.saveButton = new ComfySplitButton(
|
||||
{
|
||||
primary: getSaveButton(),
|
||||
mode: "hover",
|
||||
position: "absolute",
|
||||
mode: 'hover',
|
||||
position: 'absolute'
|
||||
},
|
||||
getSaveButton("Save"),
|
||||
getSaveButton('Save'),
|
||||
new ComfyButton({
|
||||
icon: "content-save-edit",
|
||||
content: "Save As",
|
||||
tooltip: "Save the current graph as a new workflow",
|
||||
action: () => app.workflowManager.activeWorkflow.save(true),
|
||||
icon: 'content-save-edit',
|
||||
content: 'Save As',
|
||||
tooltip: 'Save the current graph as a new workflow',
|
||||
action: () => app.workflowManager.activeWorkflow.save(true)
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "download",
|
||||
content: "Export",
|
||||
tooltip: "Export the current workflow as JSON",
|
||||
action: () => this.exportWorkflow("workflow", "workflow"),
|
||||
icon: 'download',
|
||||
content: 'Export',
|
||||
tooltip: 'Export the current workflow as JSON',
|
||||
action: () => this.exportWorkflow('workflow', 'workflow')
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "api",
|
||||
content: "Export (API Format)",
|
||||
icon: 'api',
|
||||
content: 'Export (API Format)',
|
||||
tooltip:
|
||||
"Export the current workflow as JSON for use with the ComfyUI API",
|
||||
action: () => this.exportWorkflow("workflow_api", "output"),
|
||||
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
||||
app,
|
||||
'Export the current workflow as JSON for use with the ComfyUI API',
|
||||
action: () => this.exportWorkflow('workflow_api', 'output'),
|
||||
visibilitySetting: { id: 'Comfy.DevMode', showValue: true },
|
||||
app
|
||||
})
|
||||
);
|
||||
)
|
||||
this.actionsGroup = new ComfyButtonGroup(
|
||||
new ComfyButton({
|
||||
icon: "refresh",
|
||||
content: "Refresh",
|
||||
tooltip: "Refresh widgets in nodes to find new models or files",
|
||||
action: () => app.refreshComboInNodes(),
|
||||
icon: 'refresh',
|
||||
content: 'Refresh',
|
||||
tooltip: 'Refresh widgets in nodes to find new models or files',
|
||||
action: () => app.refreshComboInNodes()
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "clipboard-edit-outline",
|
||||
content: "Clipspace",
|
||||
tooltip: "Open Clipspace window",
|
||||
action: () => app["openClipspace"](),
|
||||
icon: 'clipboard-edit-outline',
|
||||
content: 'Clipspace',
|
||||
tooltip: 'Open Clipspace window',
|
||||
action: () => app['openClipspace']()
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "fit-to-page-outline",
|
||||
content: "Reset View",
|
||||
tooltip: "Reset the canvas view",
|
||||
action: () => app.resetView(),
|
||||
icon: 'fit-to-page-outline',
|
||||
content: 'Reset View',
|
||||
tooltip: 'Reset the canvas view',
|
||||
action: () => app.resetView()
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "cancel",
|
||||
content: "Clear",
|
||||
tooltip: "Clears current workflow",
|
||||
icon: 'cancel',
|
||||
content: 'Clear',
|
||||
tooltip: 'Clears current workflow',
|
||||
action: () => {
|
||||
if (
|
||||
!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) ||
|
||||
confirm("Clear workflow?")
|
||||
!app.ui.settings.getSettingValue('Comfy.ConfirmClear', true) ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean();
|
||||
app.graph.clear();
|
||||
api.dispatchEvent(new CustomEvent("graphCleared"));
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
api.dispatchEvent(new CustomEvent('graphCleared'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
// Keep the settings group as there are custom scripts attaching extra
|
||||
// elements to it.
|
||||
this.settingsGroup = new ComfyButtonGroup();
|
||||
this.viewGroup = new ComfyButtonGroup(
|
||||
getInteruptButton("nlg-hide").element
|
||||
);
|
||||
this.settingsGroup = new ComfyButtonGroup()
|
||||
this.viewGroup = new ComfyButtonGroup(getInteruptButton('nlg-hide').element)
|
||||
this.mobileMenuButton = new ComfyButton({
|
||||
icon: "menu",
|
||||
icon: 'menu',
|
||||
action: (_, btn) => {
|
||||
btn.icon = this.element.classList.toggle("expanded")
|
||||
? "menu-open"
|
||||
: "menu";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
btn.icon = this.element.classList.toggle('expanded')
|
||||
? 'menu-open'
|
||||
: 'menu'
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
},
|
||||
classList: "comfyui-button comfyui-menu-button",
|
||||
});
|
||||
classList: 'comfyui-button comfyui-menu-button'
|
||||
})
|
||||
|
||||
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
|
||||
this.element = $el('nav.comfyui-menu.lg', { style: { display: 'none' } }, [
|
||||
this.logo,
|
||||
this.workflows.element,
|
||||
this.saveButton.element,
|
||||
collapseOnMobile(this.actionsGroup).element,
|
||||
$el("section.comfyui-menu-push"),
|
||||
$el('section.comfyui-menu-push'),
|
||||
collapseOnMobile(this.settingsGroup).element,
|
||||
collapseOnMobile(this.viewGroup).element,
|
||||
|
||||
getInteruptButton("lt-lg-show").element,
|
||||
getInteruptButton('lt-lg-show').element,
|
||||
new ComfyQueueButton(app).element,
|
||||
showOnMobile(this.mobileMenuButton).element,
|
||||
]);
|
||||
showOnMobile(this.mobileMenuButton).element
|
||||
])
|
||||
|
||||
let resizeHandler: () => void;
|
||||
let resizeHandler: () => void
|
||||
this.menuPositionSetting = app.ui.settings.addSetting({
|
||||
id: "Comfy.UseNewMenu",
|
||||
defaultValue: "Disabled",
|
||||
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
|
||||
type: "combo",
|
||||
options: ["Disabled", "Top", "Bottom"],
|
||||
id: 'Comfy.UseNewMenu',
|
||||
defaultValue: 'Disabled',
|
||||
name: '[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.',
|
||||
type: 'combo',
|
||||
options: ['Disabled', 'Top', 'Bottom'],
|
||||
onChange: async (v: MenuPosition) => {
|
||||
if (v && v !== "Disabled") {
|
||||
if (v && v !== 'Disabled') {
|
||||
if (!resizeHandler) {
|
||||
resizeHandler = () => {
|
||||
this.calculateSizeBreak();
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
this.calculateSizeBreak()
|
||||
}
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
}
|
||||
this.updatePosition(v);
|
||||
this.updatePosition(v)
|
||||
} else {
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
resizeHandler = null;
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
resizeHandler = null
|
||||
}
|
||||
document.body.style.removeProperty("display");
|
||||
app.ui.menuContainer.style.removeProperty("display");
|
||||
this.element.style.display = "none";
|
||||
app.ui.restoreMenuPosition();
|
||||
document.body.style.removeProperty('display')
|
||||
app.ui.menuContainer.style.removeProperty('display')
|
||||
this.element.style.display = 'none'
|
||||
app.ui.restoreMenuPosition()
|
||||
}
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updatePosition(v: MenuPosition) {
|
||||
document.body.style.display = "grid";
|
||||
this.app.ui.menuContainer.style.display = "none";
|
||||
this.element.style.removeProperty("display");
|
||||
this.position = v;
|
||||
if (v === "Bottom") {
|
||||
this.app.bodyBottom.append(this.element);
|
||||
document.body.style.display = 'grid'
|
||||
this.app.ui.menuContainer.style.display = 'none'
|
||||
this.element.style.removeProperty('display')
|
||||
this.position = v
|
||||
if (v === 'Bottom') {
|
||||
this.app.bodyBottom.append(this.element)
|
||||
} else {
|
||||
this.app.bodyTop.prepend(this.element);
|
||||
this.app.bodyTop.prepend(this.element)
|
||||
}
|
||||
this.calculateSizeBreak();
|
||||
this.calculateSizeBreak()
|
||||
}
|
||||
|
||||
updateSizeBreak(idx: number, prevIdx: number, direction: number) {
|
||||
const newSize = this.#sizeBreaks[idx];
|
||||
if (newSize === this.#sizeBreak) return;
|
||||
this.#cachedInnerSize = null;
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
const newSize = this.#sizeBreaks[idx]
|
||||
if (newSize === this.#sizeBreak) return
|
||||
this.#cachedInnerSize = null
|
||||
clearTimeout(this.#cacheTimeout)
|
||||
|
||||
this.#sizeBreak = this.#sizeBreaks[idx];
|
||||
this.#sizeBreak = this.#sizeBreaks[idx]
|
||||
for (let i = 0; i < this.#sizeBreaks.length; i++) {
|
||||
const sz = this.#sizeBreaks[i];
|
||||
const sz = this.#sizeBreaks[i]
|
||||
if (sz === this.#sizeBreak) {
|
||||
this.element.classList.add(sz);
|
||||
this.element.classList.add(sz)
|
||||
} else {
|
||||
this.element.classList.remove(sz);
|
||||
this.element.classList.remove(sz)
|
||||
}
|
||||
if (i < idx) {
|
||||
this.element.classList.add("lt-" + sz);
|
||||
this.element.classList.add('lt-' + sz)
|
||||
} else {
|
||||
this.element.classList.remove("lt-" + sz);
|
||||
this.element.classList.remove('lt-' + sz)
|
||||
}
|
||||
}
|
||||
|
||||
if (idx) {
|
||||
// We're on a small screen, force the menu at the top
|
||||
if (this.position !== "Top") {
|
||||
this.updatePosition("Top");
|
||||
if (this.position !== 'Top') {
|
||||
this.updatePosition('Top')
|
||||
}
|
||||
} else if (this.position != this.menuPositionSetting.value) {
|
||||
// Restore user position
|
||||
this.updatePosition(this.menuPositionSetting.value);
|
||||
this.updatePosition(this.menuPositionSetting.value)
|
||||
}
|
||||
|
||||
// Allow multiple updates, but prevent bouncing
|
||||
if (!direction) {
|
||||
direction = prevIdx - idx;
|
||||
direction = prevIdx - idx
|
||||
} else if (direction != prevIdx - idx) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.calculateSizeBreak(direction);
|
||||
this.calculateSizeBreak(direction)
|
||||
}
|
||||
|
||||
calculateSizeBreak(direction = 0) {
|
||||
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
|
||||
const currIdx = idx;
|
||||
const innerSize = this.calculateInnerSize(idx);
|
||||
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak)
|
||||
const currIdx = idx
|
||||
const innerSize = this.calculateInnerSize(idx)
|
||||
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
|
||||
if (idx > 0) {
|
||||
idx--;
|
||||
idx--
|
||||
}
|
||||
} else if (innerSize > this.element.clientWidth) {
|
||||
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(
|
||||
window.innerWidth,
|
||||
innerSize
|
||||
);
|
||||
)
|
||||
// We need to shrink
|
||||
if (idx < this.#sizeBreaks.length - 1) {
|
||||
idx++;
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSizeBreak(idx, currIdx, direction);
|
||||
this.updateSizeBreak(idx, currIdx, direction)
|
||||
}
|
||||
|
||||
calculateInnerSize(idx: number) {
|
||||
// Cache the inner size to prevent too much calculation when resizing the window
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
clearTimeout(this.#cacheTimeout)
|
||||
if (this.#cachedInnerSize) {
|
||||
// Extend cache time
|
||||
this.#cacheTimeout = setTimeout(
|
||||
() => (this.#cachedInnerSize = null),
|
||||
100
|
||||
);
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100)
|
||||
} else {
|
||||
let innerSize = 0;
|
||||
let count = 1;
|
||||
let innerSize = 0
|
||||
let count = 1
|
||||
for (const c of this.element.children) {
|
||||
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
||||
if (idx && c.classList.contains("comfyui-menu-mobile-collapse"))
|
||||
continue; // ignore collapse items
|
||||
innerSize += c.clientWidth;
|
||||
count++;
|
||||
if (c.classList.contains('comfyui-menu-push')) continue // ignore right push
|
||||
if (idx && c.classList.contains('comfyui-menu-mobile-collapse'))
|
||||
continue // ignore collapse items
|
||||
innerSize += c.clientWidth
|
||||
count++
|
||||
}
|
||||
innerSize += 8 * count;
|
||||
this.#cachedInnerSize = innerSize;
|
||||
this.#cacheTimeout = setTimeout(
|
||||
() => (this.#cachedInnerSize = null),
|
||||
100
|
||||
);
|
||||
innerSize += 8 * count
|
||||
this.#cachedInnerSize = innerSize
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100)
|
||||
}
|
||||
return this.#cachedInnerSize;
|
||||
return this.#cachedInnerSize
|
||||
}
|
||||
|
||||
getFilename(defaultName: string) {
|
||||
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
|
||||
defaultName = prompt("Save workflow as:", defaultName);
|
||||
if (!defaultName) return;
|
||||
if (!defaultName.toLowerCase().endsWith(".json")) {
|
||||
defaultName += ".json";
|
||||
if (this.app.ui.settings.getSettingValue('Comfy.PromptFilename', true)) {
|
||||
defaultName = prompt('Save workflow as:', defaultName)
|
||||
if (!defaultName) return
|
||||
if (!defaultName.toLowerCase().endsWith('.json')) {
|
||||
defaultName += '.json'
|
||||
}
|
||||
}
|
||||
return defaultName;
|
||||
return defaultName
|
||||
}
|
||||
|
||||
async exportWorkflow(
|
||||
filename: string,
|
||||
promptProperty: "workflow" | "output"
|
||||
promptProperty: 'workflow' | 'output'
|
||||
) {
|
||||
if (this.app.workflowManager.activeWorkflow?.path) {
|
||||
filename = this.app.workflowManager.activeWorkflow.name;
|
||||
filename = this.app.workflowManager.activeWorkflow.name
|
||||
}
|
||||
const p = await this.app.graphToPrompt();
|
||||
const json = JSON.stringify(p[promptProperty], null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const file = this.getFilename(filename);
|
||||
if (!file) return;
|
||||
downloadBlob(file, blob);
|
||||
const p = await this.app.graphToPrompt()
|
||||
const json = JSON.stringify(p[promptProperty], null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const file = this.getFilename(filename)
|
||||
if (!file) return
|
||||
downloadBlob(file, blob)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { api } from "../../api";
|
||||
import { ComfyButton } from "../components/button";
|
||||
import { api } from '../../api'
|
||||
import { ComfyButton } from '../components/button'
|
||||
|
||||
export function getInteruptButton(visibility: string) {
|
||||
const btn = new ComfyButton({
|
||||
icon: "close",
|
||||
tooltip: "Cancel current generation",
|
||||
icon: 'close',
|
||||
tooltip: 'Cancel current generation',
|
||||
enabled: false,
|
||||
action: () => {
|
||||
api.interrupt();
|
||||
api.interrupt()
|
||||
},
|
||||
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
|
||||
});
|
||||
classList: ['comfyui-button', 'comfyui-interrupt-button', visibility]
|
||||
})
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
const sz = detail?.exec_info?.queue_remaining;
|
||||
btn.enabled = sz > 0;
|
||||
});
|
||||
api.addEventListener('status', ({ detail }) => {
|
||||
const sz = detail?.exec_info?.queue_remaining
|
||||
btn.enabled = sz > 0
|
||||
})
|
||||
|
||||
return btn;
|
||||
return btn
|
||||
}
|
||||
|
||||
@@ -1,107 +1,107 @@
|
||||
import { ComfyButton } from "../components/button";
|
||||
import { $el } from "../../ui";
|
||||
import { api } from "../../api";
|
||||
import { ComfySplitButton } from "../components/splitButton";
|
||||
import { ComfyQueueOptions } from "./queueOptions";
|
||||
import { prop } from "../../utils";
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { ComfyButton } from '../components/button'
|
||||
import { $el } from '../../ui'
|
||||
import { api } from '../../api'
|
||||
import { ComfySplitButton } from '../components/splitButton'
|
||||
import { ComfyQueueOptions } from './queueOptions'
|
||||
import { prop } from '../../utils'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
export class ComfyQueueButton {
|
||||
element = $el("div.comfyui-queue-button");
|
||||
#internalQueueSize = 0;
|
||||
element = $el('div.comfyui-queue-button')
|
||||
#internalQueueSize = 0
|
||||
|
||||
queuePrompt = async (e?: MouseEvent) => {
|
||||
this.#internalQueueSize += this.queueOptions.batchCount;
|
||||
this.#internalQueueSize += this.queueOptions.batchCount
|
||||
// Hold shift to queue front, event is undefined when auto-queue is enabled
|
||||
await this.app.queuePrompt(
|
||||
e?.shiftKey ? -1 : 0,
|
||||
this.queueOptions.batchCount
|
||||
);
|
||||
};
|
||||
queueOptions: ComfyQueueOptions;
|
||||
app: ComfyApp;
|
||||
queueSizeElement: HTMLElement;
|
||||
autoQueueMode: string;
|
||||
graphHasChanged: boolean;
|
||||
)
|
||||
}
|
||||
queueOptions: ComfyQueueOptions
|
||||
app: ComfyApp
|
||||
queueSizeElement: HTMLElement
|
||||
autoQueueMode: string
|
||||
graphHasChanged: boolean
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
this.app = app;
|
||||
this.queueSizeElement = $el("span.comfyui-queue-count", {
|
||||
textContent: "?",
|
||||
});
|
||||
this.app = app
|
||||
this.queueSizeElement = $el('span.comfyui-queue-count', {
|
||||
textContent: '?'
|
||||
})
|
||||
|
||||
const queue = new ComfyButton({
|
||||
content: $el("div", [
|
||||
$el("span", {
|
||||
textContent: "Queue",
|
||||
content: $el('div', [
|
||||
$el('span', {
|
||||
textContent: 'Queue'
|
||||
}),
|
||||
this.queueSizeElement,
|
||||
this.queueSizeElement
|
||||
]),
|
||||
icon: "play",
|
||||
classList: "comfyui-button",
|
||||
action: this.queuePrompt,
|
||||
});
|
||||
icon: 'play',
|
||||
classList: 'comfyui-button',
|
||||
action: this.queuePrompt
|
||||
})
|
||||
|
||||
this.queueOptions = new ComfyQueueOptions(app);
|
||||
this.queueOptions = new ComfyQueueOptions(app)
|
||||
|
||||
const btn = new ComfySplitButton(
|
||||
{
|
||||
primary: queue,
|
||||
mode: "click",
|
||||
position: "absolute",
|
||||
horizontal: "right",
|
||||
mode: 'click',
|
||||
position: 'absolute',
|
||||
horizontal: 'right'
|
||||
},
|
||||
this.queueOptions.element
|
||||
);
|
||||
btn.element.classList.add("primary");
|
||||
this.element.append(btn.element);
|
||||
)
|
||||
btn.element.classList.add('primary')
|
||||
this.element.append(btn.element)
|
||||
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "", () => {
|
||||
this.autoQueueMode = prop(this, 'autoQueueMode', '', () => {
|
||||
switch (this.autoQueueMode) {
|
||||
case "instant":
|
||||
queue.icon = "infinity";
|
||||
break;
|
||||
case "change":
|
||||
queue.icon = "auto-mode";
|
||||
break;
|
||||
case 'instant':
|
||||
queue.icon = 'infinity'
|
||||
break
|
||||
case 'change':
|
||||
queue.icon = 'auto-mode'
|
||||
break
|
||||
default:
|
||||
queue.icon = "play";
|
||||
break;
|
||||
queue.icon = 'play'
|
||||
break
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this.queueOptions.addEventListener(
|
||||
"autoQueueMode",
|
||||
(e) => (this.autoQueueMode = e["detail"])
|
||||
);
|
||||
'autoQueueMode',
|
||||
(e) => (this.autoQueueMode = e['detail'])
|
||||
)
|
||||
|
||||
api.addEventListener("graphChanged", () => {
|
||||
if (this.autoQueueMode === "change") {
|
||||
api.addEventListener('graphChanged', () => {
|
||||
if (this.autoQueueMode === 'change') {
|
||||
if (this.#internalQueueSize) {
|
||||
this.graphHasChanged = true;
|
||||
this.graphHasChanged = true
|
||||
} else {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
this.graphHasChanged = false
|
||||
this.queuePrompt()
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
|
||||
api.addEventListener('status', ({ detail }) => {
|
||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining
|
||||
if (this.#internalQueueSize != null) {
|
||||
this.queueSizeElement.textContent =
|
||||
this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
|
||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
|
||||
this.#internalQueueSize > 99 ? '99+' : this.#internalQueueSize + ''
|
||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`
|
||||
if (!this.#internalQueueSize && !app.lastExecutionError) {
|
||||
if (
|
||||
this.autoQueueMode === "instant" ||
|
||||
(this.autoQueueMode === "change" && this.graphHasChanged)
|
||||
this.autoQueueMode === 'instant' ||
|
||||
(this.autoQueueMode === 'change' && this.graphHasChanged)
|
||||
) {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
this.graphHasChanged = false
|
||||
this.queuePrompt()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { $el } from "../../ui";
|
||||
import { prop } from "../../utils";
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { $el } from '../../ui'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
export class ComfyQueueOptions extends EventTarget {
|
||||
element = $el("div.comfyui-queue-options");
|
||||
app: ComfyApp;
|
||||
batchCountInput: HTMLInputElement;
|
||||
batchCount: number;
|
||||
batchCountRange: HTMLInputElement;
|
||||
autoQueueMode: string;
|
||||
autoQueueEl: HTMLElement;
|
||||
element = $el('div.comfyui-queue-options')
|
||||
app: ComfyApp
|
||||
batchCountInput: HTMLInputElement
|
||||
batchCount: number
|
||||
batchCountRange: HTMLInputElement
|
||||
autoQueueMode: string
|
||||
autoQueueEl: HTMLElement
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
super();
|
||||
this.app = app;
|
||||
super()
|
||||
this.app = app
|
||||
|
||||
this.batchCountInput = $el("input", {
|
||||
className: "comfyui-queue-batch-value",
|
||||
type: "number",
|
||||
min: "1",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountInput.value),
|
||||
});
|
||||
this.batchCountInput = $el('input', {
|
||||
className: 'comfyui-queue-batch-value',
|
||||
type: 'number',
|
||||
min: '1',
|
||||
value: '1',
|
||||
oninput: () => (this.batchCount = +this.batchCountInput.value)
|
||||
})
|
||||
|
||||
this.batchCountRange = $el("input", {
|
||||
type: "range",
|
||||
min: "1",
|
||||
max: "100",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountRange.value),
|
||||
});
|
||||
this.batchCountRange = $el('input', {
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: '1',
|
||||
oninput: () => (this.batchCount = +this.batchCountRange.value)
|
||||
})
|
||||
|
||||
this.element.append(
|
||||
$el("div.comfyui-queue-batch", [
|
||||
$el('div.comfyui-queue-batch', [
|
||||
$el(
|
||||
"label",
|
||||
'label',
|
||||
{
|
||||
textContent: "Batch count: ",
|
||||
textContent: 'Batch count: '
|
||||
},
|
||||
this.batchCountInput
|
||||
),
|
||||
this.batchCountRange,
|
||||
this.batchCountRange
|
||||
])
|
||||
);
|
||||
)
|
||||
|
||||
const createOption = (text, value, checked = false) =>
|
||||
$el(
|
||||
"label",
|
||||
'label',
|
||||
{ textContent: text },
|
||||
$el("input", {
|
||||
type: "radio",
|
||||
name: "AutoQueueMode",
|
||||
$el('input', {
|
||||
type: 'radio',
|
||||
name: 'AutoQueueMode',
|
||||
checked,
|
||||
value,
|
||||
oninput: (e) => (this.autoQueueMode = e.target["value"]),
|
||||
oninput: (e) => (this.autoQueueMode = e.target['value'])
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
this.autoQueueEl = $el("div.comfyui-queue-mode", [
|
||||
$el("span", "Auto Queue:"),
|
||||
createOption("Disabled", "", true),
|
||||
createOption("Instant", "instant"),
|
||||
createOption("On Change", "change"),
|
||||
]);
|
||||
this.autoQueueEl = $el('div.comfyui-queue-mode', [
|
||||
$el('span', 'Auto Queue:'),
|
||||
createOption('Disabled', '', true),
|
||||
createOption('Instant', 'instant'),
|
||||
createOption('On Change', 'change')
|
||||
])
|
||||
|
||||
this.element.append(this.autoQueueEl);
|
||||
this.element.append(this.autoQueueEl)
|
||||
|
||||
this.batchCount = prop(this, "batchCount", 1, () => {
|
||||
this.batchCountInput.value = this.batchCount + "";
|
||||
this.batchCountRange.value = this.batchCount + "";
|
||||
});
|
||||
this.batchCount = prop(this, 'batchCount', 1, () => {
|
||||
this.batchCountInput.value = this.batchCount + ''
|
||||
this.batchCountRange.value = this.batchCount + ''
|
||||
})
|
||||
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => {
|
||||
this.autoQueueMode = prop(this, 'autoQueueMode', 'Disabled', () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("autoQueueMode", {
|
||||
detail: this.autoQueueMode,
|
||||
new CustomEvent('autoQueueMode', {
|
||||
detail: this.autoQueueMode
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,60 @@
|
||||
import { $el } from "../ui";
|
||||
import { api } from "../api";
|
||||
import { ComfyDialog } from "./dialog";
|
||||
import type { ComfyApp } from "../app";
|
||||
import type { Setting, SettingParams } from "@/types/settingTypes";
|
||||
import { useSettingStore } from "@/stores/settingStore";
|
||||
import { $el } from '../ui'
|
||||
import { api } from '../api'
|
||||
import { ComfyDialog } from './dialog'
|
||||
import type { ComfyApp } from '../app'
|
||||
import type { Setting, SettingParams } from '@/types/settingTypes'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
app: ComfyApp;
|
||||
settingsValues: any;
|
||||
settingsLookup: Record<string, Setting>;
|
||||
settingsParamLookup: Record<string, SettingParams>;
|
||||
app: ComfyApp
|
||||
settingsValues: any
|
||||
settingsLookup: Record<string, Setting>
|
||||
settingsParamLookup: Record<string, SettingParams>
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
super();
|
||||
const frontendVersion = window["__COMFYUI_FRONTEND_VERSION__"];
|
||||
this.app = app;
|
||||
this.settingsValues = {};
|
||||
this.settingsLookup = {};
|
||||
this.settingsParamLookup = {};
|
||||
super()
|
||||
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__']
|
||||
this.app = app
|
||||
this.settingsValues = {}
|
||||
this.settingsLookup = {}
|
||||
this.settingsParamLookup = {}
|
||||
this.element = $el(
|
||||
"dialog",
|
||||
'dialog',
|
||||
{
|
||||
id: "comfy-settings-dialog",
|
||||
parent: document.body,
|
||||
id: 'comfy-settings-dialog',
|
||||
parent: document.body
|
||||
},
|
||||
[
|
||||
$el("table.comfy-modal-content.comfy-table", [
|
||||
$el('table.comfy-modal-content.comfy-table', [
|
||||
$el(
|
||||
"caption",
|
||||
'caption',
|
||||
{ textContent: `Settings (v${frontendVersion})` },
|
||||
$el("button.comfy-btn", {
|
||||
type: "button",
|
||||
textContent: "\u00d7",
|
||||
$el('button.comfy-btn', {
|
||||
type: 'button',
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
this.element.close()
|
||||
}
|
||||
})
|
||||
),
|
||||
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
$el('tbody', { $: (tbody) => (this.textElement = tbody) }),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Close',
|
||||
style: {
|
||||
cursor: "pointer",
|
||||
cursor: 'pointer'
|
||||
},
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
}),
|
||||
]),
|
||||
this.element.close()
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
) as HTMLDialogElement;
|
||||
) as HTMLDialogElement
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return Object.values(this.settingsLookup);
|
||||
return Object.values(this.settingsLookup)
|
||||
}
|
||||
|
||||
#dispatchChange<T>(id: string, value: T, oldValue?: T) {
|
||||
@@ -63,84 +63,84 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
// `load` re-dispatch the change for any settings added before load so
|
||||
// settingStore is always up to date.
|
||||
if (this.app.vueAppReady) {
|
||||
useSettingStore().settingValues[id] = value;
|
||||
useSettingStore().settingValues[id] = value
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(id + ".change", {
|
||||
new CustomEvent(id + '.change', {
|
||||
detail: {
|
||||
value,
|
||||
oldValue,
|
||||
},
|
||||
oldValue
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
this.settingsValues = localStorage;
|
||||
if (this.app.storageLocation === 'browser') {
|
||||
this.settingsValues = localStorage
|
||||
} else {
|
||||
this.settingsValues = await api.getSettings();
|
||||
this.settingsValues = await api.getSettings()
|
||||
}
|
||||
|
||||
// Trigger onChange for any settings added before load
|
||||
for (const id in this.settingsLookup) {
|
||||
const value = this.settingsValues[this.getId(id)];
|
||||
this.settingsLookup[id].onChange?.(value);
|
||||
this.#dispatchChange(id, value);
|
||||
const value = this.settingsValues[this.getId(id)]
|
||||
this.settingsLookup[id].onChange?.(value)
|
||||
this.#dispatchChange(id, value)
|
||||
}
|
||||
}
|
||||
|
||||
getId(id: string) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
id = "Comfy.Settings." + id;
|
||||
if (this.app.storageLocation === 'browser') {
|
||||
id = 'Comfy.Settings.' + id
|
||||
}
|
||||
return id;
|
||||
return id
|
||||
}
|
||||
|
||||
getSettingValue<T>(id: string, defaultValue?: T): T {
|
||||
let value = this.settingsValues[this.getId(id)];
|
||||
let value = this.settingsValues[this.getId(id)]
|
||||
if (value != null) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
if (this.app.storageLocation === 'browser') {
|
||||
try {
|
||||
value = JSON.parse(value) as T;
|
||||
value = JSON.parse(value) as T
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
return value ?? defaultValue;
|
||||
return value ?? defaultValue
|
||||
}
|
||||
|
||||
getSettingDefaultValue(id: string) {
|
||||
const param = this.settingsParamLookup[id];
|
||||
return param?.defaultValue;
|
||||
const param = this.settingsParamLookup[id]
|
||||
return param?.defaultValue
|
||||
}
|
||||
|
||||
async setSettingValueAsync(id: string, value: any) {
|
||||
const json = JSON.stringify(value);
|
||||
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
|
||||
const json = JSON.stringify(value)
|
||||
localStorage['Comfy.Settings.' + id] = json // backwards compatibility for extensions keep setting in storage
|
||||
|
||||
let oldValue = this.getSettingValue(id, undefined);
|
||||
this.settingsValues[this.getId(id)] = value;
|
||||
let oldValue = this.getSettingValue(id, undefined)
|
||||
this.settingsValues[this.getId(id)] = value
|
||||
|
||||
if (id in this.settingsLookup) {
|
||||
this.settingsLookup[id].onChange?.(value, oldValue);
|
||||
this.settingsLookup[id].onChange?.(value, oldValue)
|
||||
}
|
||||
this.#dispatchChange(id, value, oldValue);
|
||||
this.#dispatchChange(id, value, oldValue)
|
||||
|
||||
await api.storeSetting(id, value);
|
||||
await api.storeSetting(id, value)
|
||||
}
|
||||
|
||||
setSettingValue(id: string, value: any) {
|
||||
this.setSettingValueAsync(id, value).catch((err) => {
|
||||
alert(`Error saving setting '${id}'`);
|
||||
console.error(err);
|
||||
});
|
||||
alert(`Error saving setting '${id}'`)
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
refreshSetting(id: string) {
|
||||
const value = this.getSettingValue(id);
|
||||
this.settingsLookup[id].onChange?.(value);
|
||||
this.#dispatchChange(id, value);
|
||||
const value = this.getSettingValue(id)
|
||||
this.settingsLookup[id].onChange?.(value)
|
||||
this.#dispatchChange(id, value)
|
||||
}
|
||||
|
||||
addSetting(params: SettingParams) {
|
||||
@@ -151,232 +151,231 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
defaultValue,
|
||||
onChange,
|
||||
attrs = {},
|
||||
tooltip = "",
|
||||
options = undefined,
|
||||
} = params;
|
||||
tooltip = '',
|
||||
options = undefined
|
||||
} = params
|
||||
if (!id) {
|
||||
throw new Error("Settings must have an ID");
|
||||
throw new Error('Settings must have an ID')
|
||||
}
|
||||
|
||||
if (id in this.settingsLookup) {
|
||||
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
|
||||
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`)
|
||||
}
|
||||
|
||||
let skipOnChange = false;
|
||||
let value = this.getSettingValue(id);
|
||||
let skipOnChange = false
|
||||
let value = this.getSettingValue(id)
|
||||
if (value == null) {
|
||||
if (this.app.isNewUserSession) {
|
||||
// Check if we have a localStorage value but not a setting value and we are a new user
|
||||
const localValue = localStorage["Comfy.Settings." + id];
|
||||
const localValue = localStorage['Comfy.Settings.' + id]
|
||||
if (localValue) {
|
||||
value = JSON.parse(localValue);
|
||||
this.setSettingValue(id, value); // Store on the server
|
||||
value = JSON.parse(localValue)
|
||||
this.setSettingValue(id, value) // Store on the server
|
||||
}
|
||||
}
|
||||
if (value == null) {
|
||||
value = defaultValue;
|
||||
value = defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger initial setting of value
|
||||
if (!skipOnChange) {
|
||||
onChange?.(value, undefined);
|
||||
this.#dispatchChange(id, value);
|
||||
onChange?.(value, undefined)
|
||||
this.#dispatchChange(id, value)
|
||||
}
|
||||
|
||||
this.settingsParamLookup[id] = params;
|
||||
this.settingsParamLookup[id] = params
|
||||
this.settingsLookup[id] = {
|
||||
id,
|
||||
onChange,
|
||||
name,
|
||||
render: () => {
|
||||
if (type === "hidden") return;
|
||||
if (type === 'hidden') return
|
||||
|
||||
const setter = (v) => {
|
||||
if (onChange) {
|
||||
onChange(v, value);
|
||||
onChange(v, value)
|
||||
}
|
||||
|
||||
this.setSettingValue(id, v);
|
||||
value = v;
|
||||
};
|
||||
value = this.getSettingValue(id, defaultValue);
|
||||
this.setSettingValue(id, v)
|
||||
value = v
|
||||
}
|
||||
value = this.getSettingValue(id, defaultValue)
|
||||
|
||||
let element;
|
||||
const htmlID = id.replaceAll(".", "-");
|
||||
let element
|
||||
const htmlID = id.replaceAll('.', '-')
|
||||
|
||||
const labelCell = $el("td", [
|
||||
$el("label", {
|
||||
const labelCell = $el('td', [
|
||||
$el('label', {
|
||||
for: htmlID,
|
||||
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
|
||||
textContent: name,
|
||||
}),
|
||||
]);
|
||||
classList: [tooltip !== '' ? 'comfy-tooltip-indicator' : ''],
|
||||
textContent: name
|
||||
})
|
||||
])
|
||||
|
||||
if (typeof type === "function") {
|
||||
element = type(name, setter, value, attrs);
|
||||
if (typeof type === 'function') {
|
||||
element = type(name, setter, value, attrs)
|
||||
} else {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
element = $el("tr", [
|
||||
case 'boolean':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
id: htmlID,
|
||||
type: "checkbox",
|
||||
type: 'checkbox',
|
||||
checked: value,
|
||||
onchange: (event) => {
|
||||
const isChecked = event.target.checked;
|
||||
const isChecked = event.target.checked
|
||||
if (onChange !== undefined) {
|
||||
onChange(isChecked);
|
||||
onChange(isChecked)
|
||||
}
|
||||
this.setSettingValue(id, isChecked);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "number":
|
||||
element = $el("tr", [
|
||||
this.setSettingValue(id, isChecked)
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'number':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
type,
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
setter(e.target.value)
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "slider":
|
||||
element = $el("tr", [
|
||||
...attrs
|
||||
})
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'slider':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el('td', [
|
||||
$el(
|
||||
"div",
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: "grid",
|
||||
gridAutoFlow: "column",
|
||||
},
|
||||
display: 'grid',
|
||||
gridAutoFlow: 'column'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
...attrs,
|
||||
value,
|
||||
type: "range",
|
||||
type: 'range',
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.nextElementSibling.value = e.target.value;
|
||||
},
|
||||
setter(e.target.value)
|
||||
e.target.nextElementSibling.value = e.target.value
|
||||
}
|
||||
}),
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
...attrs,
|
||||
value,
|
||||
id: htmlID,
|
||||
type: "number",
|
||||
style: { maxWidth: "4rem" },
|
||||
type: 'number',
|
||||
style: { maxWidth: '4rem' },
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.previousElementSibling.value =
|
||||
e.target.value;
|
||||
},
|
||||
}),
|
||||
setter(e.target.value)
|
||||
e.target.previousElementSibling.value = e.target.value
|
||||
}
|
||||
})
|
||||
]
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "combo":
|
||||
element = $el("tr", [
|
||||
)
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'combo':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el('td', [
|
||||
$el(
|
||||
"select",
|
||||
'select',
|
||||
{
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
setter(e.target.value)
|
||||
}
|
||||
},
|
||||
(typeof options === "function"
|
||||
(typeof options === 'function'
|
||||
? options(value)
|
||||
: options || []
|
||||
).map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
if (typeof opt === 'string') {
|
||||
opt = { text: opt }
|
||||
}
|
||||
const v = opt.value ?? opt.text;
|
||||
return $el("option", {
|
||||
const v = opt.value ?? opt.text
|
||||
return $el('option', {
|
||||
value: v,
|
||||
textContent: opt.text,
|
||||
selected: value + "" === v + "",
|
||||
});
|
||||
selected: value + '' === v + ''
|
||||
})
|
||||
})
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "text":
|
||||
)
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'text':
|
||||
default:
|
||||
if (type !== "text") {
|
||||
if (type !== 'text') {
|
||||
console.warn(
|
||||
`Unsupported setting type '${type}, defaulting to text`
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
element = $el("tr", [
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
setter(e.target.value)
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
...attrs
|
||||
})
|
||||
])
|
||||
])
|
||||
break
|
||||
}
|
||||
}
|
||||
if (tooltip) {
|
||||
element.title = tooltip;
|
||||
element.title = tooltip
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
} as Setting;
|
||||
return element
|
||||
}
|
||||
} as Setting
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
return {
|
||||
get value() {
|
||||
return self.getSettingValue(id, defaultValue);
|
||||
return self.getSettingValue(id, defaultValue)
|
||||
},
|
||||
set value(v) {
|
||||
self.setSettingValue(id, v);
|
||||
},
|
||||
};
|
||||
self.setSettingValue(id, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.textElement.replaceChildren(
|
||||
$el(
|
||||
"tr",
|
||||
'tr',
|
||||
{
|
||||
style: { display: "none" },
|
||||
style: { display: 'none' }
|
||||
},
|
||||
[$el("th"), $el("th", { style: { width: "33%" } })]
|
||||
[$el('th'), $el('th', { style: { width: '33%' } })]
|
||||
),
|
||||
...this.settings
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((s) => s.render())
|
||||
.filter(Boolean)
|
||||
);
|
||||
this.element.showModal();
|
||||
)
|
||||
this.element.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./spinner.css";
|
||||
import './spinner.css'
|
||||
|
||||
export function createSpinner() {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
||||
return div.firstElementChild;
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`
|
||||
return div.firstElementChild
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $el } from "../ui";
|
||||
import { $el } from '../ui'
|
||||
|
||||
/**
|
||||
* @typedef { { text: string, value?: string, tooltip?: string } } ToggleSwitchItem
|
||||
@@ -11,55 +11,55 @@ import { $el } from "../ui";
|
||||
* @param { (e: { item: ToggleSwitchItem, prev?: ToggleSwitchItem }) => void } [opts.onChange]
|
||||
*/
|
||||
export function toggleSwitch(name, items, e?) {
|
||||
const onChange = e?.onChange;
|
||||
const onChange = e?.onChange
|
||||
|
||||
let selectedIndex;
|
||||
let elements;
|
||||
let selectedIndex
|
||||
let elements
|
||||
|
||||
function updateSelected(index) {
|
||||
if (selectedIndex != null) {
|
||||
elements[selectedIndex].classList.remove("comfy-toggle-selected");
|
||||
elements[selectedIndex].classList.remove('comfy-toggle-selected')
|
||||
}
|
||||
onChange?.({
|
||||
item: items[index],
|
||||
prev: selectedIndex == null ? undefined : items[selectedIndex],
|
||||
});
|
||||
selectedIndex = index;
|
||||
elements[selectedIndex].classList.add("comfy-toggle-selected");
|
||||
prev: selectedIndex == null ? undefined : items[selectedIndex]
|
||||
})
|
||||
selectedIndex = index
|
||||
elements[selectedIndex].classList.add('comfy-toggle-selected')
|
||||
}
|
||||
|
||||
elements = items.map((item, i) => {
|
||||
if (typeof item === "string") item = { text: item };
|
||||
if (!item.value) item.value = item.text;
|
||||
if (typeof item === 'string') item = { text: item }
|
||||
if (!item.value) item.value = item.text
|
||||
|
||||
const toggle = $el(
|
||||
"label",
|
||||
'label',
|
||||
{
|
||||
textContent: item.text,
|
||||
title: item.tooltip ?? "",
|
||||
title: item.tooltip ?? ''
|
||||
},
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
name,
|
||||
type: "radio",
|
||||
type: 'radio',
|
||||
value: item.value ?? item.text,
|
||||
checked: item.selected,
|
||||
onchange: () => {
|
||||
updateSelected(i);
|
||||
},
|
||||
updateSelected(i)
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
if (item.selected) {
|
||||
updateSelected(i);
|
||||
updateSelected(i)
|
||||
}
|
||||
return toggle;
|
||||
});
|
||||
return toggle
|
||||
})
|
||||
|
||||
const container = $el("div.comfy-toggle-switch", elements);
|
||||
const container = $el('div.comfy-toggle-switch', elements)
|
||||
|
||||
if (selectedIndex == null) {
|
||||
elements[0].children[0].checked = true;
|
||||
updateSelected(0);
|
||||
elements[0].children[0].checked = true
|
||||
updateSelected(0)
|
||||
}
|
||||
|
||||
return container;
|
||||
return container
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
import { api } from "../api";
|
||||
import { $el } from "../ui";
|
||||
import { createSpinner } from "./spinner";
|
||||
import "./userSelection.css";
|
||||
import { api } from '../api'
|
||||
import { $el } from '../ui'
|
||||
import { createSpinner } from './spinner'
|
||||
import './userSelection.css'
|
||||
|
||||
interface SelectedUser {
|
||||
username: string;
|
||||
userId: string;
|
||||
created: boolean;
|
||||
username: string
|
||||
userId: string
|
||||
created: boolean
|
||||
}
|
||||
|
||||
export class UserSelectionScreen {
|
||||
async show(users, user): Promise<SelectedUser> {
|
||||
const userSelection = document.getElementById("comfy-user-selection");
|
||||
userSelection.style.display = "";
|
||||
const userSelection = document.getElementById('comfy-user-selection')
|
||||
userSelection.style.display = ''
|
||||
return new Promise((resolve) => {
|
||||
const input = userSelection.getElementsByTagName("input")[0];
|
||||
const select = userSelection.getElementsByTagName("select")[0];
|
||||
const inputSection = input.closest("section");
|
||||
const selectSection = select.closest("section");
|
||||
const form = userSelection.getElementsByTagName("form")[0];
|
||||
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
|
||||
const input = userSelection.getElementsByTagName('input')[0]
|
||||
const select = userSelection.getElementsByTagName('select')[0]
|
||||
const inputSection = input.closest('section')
|
||||
const selectSection = select.closest('section')
|
||||
const form = userSelection.getElementsByTagName('form')[0]
|
||||
const error = userSelection.getElementsByClassName('comfy-user-error')[0]
|
||||
const button = userSelection.getElementsByClassName(
|
||||
"comfy-user-button-next"
|
||||
)[0];
|
||||
'comfy-user-button-next'
|
||||
)[0]
|
||||
|
||||
let inputActive = null;
|
||||
input.addEventListener("focus", () => {
|
||||
inputSection.classList.add("selected");
|
||||
selectSection.classList.remove("selected");
|
||||
inputActive = true;
|
||||
});
|
||||
select.addEventListener("focus", () => {
|
||||
inputSection.classList.remove("selected");
|
||||
selectSection.classList.add("selected");
|
||||
inputActive = false;
|
||||
select.style.color = "";
|
||||
});
|
||||
select.addEventListener("blur", () => {
|
||||
let inputActive = null
|
||||
input.addEventListener('focus', () => {
|
||||
inputSection.classList.add('selected')
|
||||
selectSection.classList.remove('selected')
|
||||
inputActive = true
|
||||
})
|
||||
select.addEventListener('focus', () => {
|
||||
inputSection.classList.remove('selected')
|
||||
selectSection.classList.add('selected')
|
||||
inputActive = false
|
||||
select.style.color = ''
|
||||
})
|
||||
select.addEventListener('blur', () => {
|
||||
if (!select.value) {
|
||||
select.style.color = "var(--descrip-text)";
|
||||
select.style.color = 'var(--descrip-text)'
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
if (inputActive == null) {
|
||||
error.textContent =
|
||||
"Please enter a username or select an existing user.";
|
||||
'Please enter a username or select an existing user.'
|
||||
} else if (inputActive) {
|
||||
const username = input.value.trim();
|
||||
const username = input.value.trim()
|
||||
if (!username) {
|
||||
error.textContent = "Please enter a username.";
|
||||
return;
|
||||
error.textContent = 'Please enter a username.'
|
||||
return
|
||||
}
|
||||
|
||||
// Create new user
|
||||
@@ -63,31 +63,31 @@ export class UserSelectionScreen {
|
||||
input.readonly =
|
||||
// @ts-ignore
|
||||
select.readonly =
|
||||
true;
|
||||
const spinner = createSpinner();
|
||||
button.prepend(spinner);
|
||||
true
|
||||
const spinner = createSpinner()
|
||||
button.prepend(spinner)
|
||||
try {
|
||||
const resp = await api.createUser(username);
|
||||
const resp = await api.createUser(username)
|
||||
if (resp.status >= 300) {
|
||||
let message =
|
||||
"Error creating user: " + resp.status + " " + resp.statusText;
|
||||
'Error creating user: ' + resp.status + ' ' + resp.statusText
|
||||
try {
|
||||
const res = await resp.json();
|
||||
const res = await resp.json()
|
||||
if (res.error) {
|
||||
message = res.error;
|
||||
message = res.error
|
||||
}
|
||||
} catch (error) {}
|
||||
throw new Error(message);
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
resolve({ username, userId: await resp.json(), created: true });
|
||||
resolve({ username, userId: await resp.json(), created: true })
|
||||
} catch (err) {
|
||||
spinner.remove();
|
||||
spinner.remove()
|
||||
error.textContent =
|
||||
err.message ??
|
||||
err.statusText ??
|
||||
err ??
|
||||
"An unknown error occurred.";
|
||||
'An unknown error occurred.'
|
||||
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
||||
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
||||
input.disabled =
|
||||
@@ -96,50 +96,50 @@ export class UserSelectionScreen {
|
||||
input.readonly =
|
||||
// @ts-ignore
|
||||
select.readonly =
|
||||
false;
|
||||
return;
|
||||
false
|
||||
return
|
||||
}
|
||||
} else if (!select.value) {
|
||||
error.textContent = "Please select an existing user.";
|
||||
return;
|
||||
error.textContent = 'Please select an existing user.'
|
||||
return
|
||||
} else {
|
||||
resolve({
|
||||
username: users[select.value],
|
||||
userId: select.value,
|
||||
created: false,
|
||||
});
|
||||
created: false
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (user) {
|
||||
const name = localStorage["Comfy.userName"];
|
||||
const name = localStorage['Comfy.userName']
|
||||
if (name) {
|
||||
input.value = name;
|
||||
input.value = name
|
||||
}
|
||||
}
|
||||
if (input.value) {
|
||||
// Focus the input, do this separately as sometimes browsers like to fill in the value
|
||||
input.focus();
|
||||
input.focus()
|
||||
}
|
||||
|
||||
const userIds = Object.keys(users ?? {});
|
||||
const userIds = Object.keys(users ?? {})
|
||||
if (userIds.length) {
|
||||
for (const u of userIds) {
|
||||
$el("option", { textContent: users[u], value: u, parent: select });
|
||||
$el('option', { textContent: users[u], value: u, parent: select })
|
||||
}
|
||||
select.style.color = "var(--descrip-text)";
|
||||
select.style.color = 'var(--descrip-text)'
|
||||
|
||||
if (select.value) {
|
||||
// Focus the select, do this separately as sometimes browsers like to fill in the value
|
||||
select.focus();
|
||||
select.focus()
|
||||
}
|
||||
} else {
|
||||
userSelection.classList.add("no-users");
|
||||
input.focus();
|
||||
userSelection.classList.add('no-users')
|
||||
input.focus()
|
||||
}
|
||||
}).then((r: SelectedUser) => {
|
||||
userSelection.remove();
|
||||
return r;
|
||||
});
|
||||
userSelection.remove()
|
||||
return r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
export type ClassList = string | string[] | Record<string, boolean>;
|
||||
export type ClassList = string | string[] | Record<string, boolean>
|
||||
|
||||
export function applyClasses(
|
||||
element: HTMLElement,
|
||||
classList: ClassList,
|
||||
...requiredClasses: string[]
|
||||
) {
|
||||
classList ??= "";
|
||||
classList ??= ''
|
||||
|
||||
let str: string;
|
||||
if (typeof classList === "string") {
|
||||
str = classList;
|
||||
let str: string
|
||||
if (typeof classList === 'string') {
|
||||
str = classList
|
||||
} else if (classList instanceof Array) {
|
||||
str = classList.join(" ");
|
||||
str = classList.join(' ')
|
||||
} else {
|
||||
str = Object.entries(classList).reduce((p, c) => {
|
||||
if (c[1]) {
|
||||
p += (p.length ? " " : "") + c[0];
|
||||
p += (p.length ? ' ' : '') + c[0]
|
||||
}
|
||||
return p;
|
||||
}, "");
|
||||
return p
|
||||
}, '')
|
||||
}
|
||||
element.className = str;
|
||||
element.className = str
|
||||
if (requiredClasses) {
|
||||
element.classList.add(...requiredClasses);
|
||||
element.classList.add(...requiredClasses)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,28 +30,28 @@ export function toggleElement(
|
||||
element: HTMLElement,
|
||||
{
|
||||
onHide,
|
||||
onShow,
|
||||
onShow
|
||||
}: {
|
||||
onHide?: (el: HTMLElement) => void;
|
||||
onShow?: (el: HTMLElement, value) => void;
|
||||
onHide?: (el: HTMLElement) => void
|
||||
onShow?: (el: HTMLElement, value) => void
|
||||
} = {}
|
||||
) {
|
||||
let placeholder: HTMLElement | Comment;
|
||||
let hidden: boolean;
|
||||
let placeholder: HTMLElement | Comment
|
||||
let hidden: boolean
|
||||
return (value) => {
|
||||
if (value) {
|
||||
if (hidden) {
|
||||
hidden = false;
|
||||
placeholder.replaceWith(element);
|
||||
hidden = false
|
||||
placeholder.replaceWith(element)
|
||||
}
|
||||
onShow?.(element, value);
|
||||
onShow?.(element, value)
|
||||
} else {
|
||||
if (!placeholder) {
|
||||
placeholder = document.createComment("");
|
||||
placeholder = document.createComment('')
|
||||
}
|
||||
hidden = true;
|
||||
element.replaceWith(placeholder);
|
||||
onHide?.(element);
|
||||
hidden = true
|
||||
element.replaceWith(placeholder)
|
||||
onHide?.(element)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user