Apply new code format standard (#217)

This commit is contained in:
Chenlei Hu
2024-07-25 10:10:18 -04:00
committed by GitHub
parent 19c70d95d3
commit e179f75387
121 changed files with 11898 additions and 11983 deletions

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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))
}
}

View File

@@ -1,3 +1,3 @@
export interface ComfyComponent<T extends HTMLElement = HTMLElement> {
element: T;
element: T
}

View File

@@ -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')
}
};
}
}

View File

@@ -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))
)
}
}

View File

@@ -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'
}
}

View File

@@ -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())
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}
});
})
}
}

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
})
}
}

View File

@@ -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)
}
};
}
}