Vue component multi-select widget (#2987)

This commit is contained in:
Chenlei Hu
2025-03-12 12:03:21 -04:00
committed by GitHub
parent aad7ded636
commit bcd76ba49b
8 changed files with 299 additions and 133 deletions

View File

@@ -5,47 +5,64 @@ import type {
IWidgetOptions
} from '@comfyorg/litegraph/dist/types/widgets'
import _ from 'lodash'
import { type Component, toRaw } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { generateUUID } from '@/utils/formatUtil'
export interface DOMWidget<T extends HTMLElement, V extends object | string>
export interface BaseDOMWidget<V extends object | string>
extends ICustomWidget {
// ICustomWidget properties
type: 'custom'
element: T
options: DOMWidgetOptions<T, V>
options: DOMWidgetOptions<V>
value: V
/**
* @deprecated Legacy property used by some extensions for customtext
* (textarea) widgets. Use `element` instead as it provides the same
* functionality and works for all DOMWidget types.
*/
inputEl?: T
callback?: (value: V) => void
// DOMWidget properties
// BaseDOMWidget properties
/** The unique ID of the widget. */
id: string
readonly id: string
/** The node that the widget belongs to. */
node: LGraphNode
readonly node: LGraphNode
/** Whether the widget is visible. */
isVisible(): boolean
}
export interface DOMWidgetOptions<
T extends HTMLElement,
V extends object | string
> extends IWidgetOptions {
/**
* A DOM widget that wraps a custom HTML element as a litegraph widget.
*/
export interface DOMWidget<T extends HTMLElement, V extends object | string>
extends BaseDOMWidget<V> {
element: T
/**
* @deprecated Legacy property used by some extensions for customtext
* (textarea) widgets. Use {@link element} instead as it provides the same
* functionality and works for all DOMWidget types.
*/
inputEl?: T
}
/**
* A DOM widget that wraps a Vue component as a litegraph widget.
*/
export interface ComponentWidget<V extends object | string>
extends BaseDOMWidget<V> {
readonly component: Component
readonly inputSpec: InputSpec
}
export interface DOMWidgetOptions<V extends object | string>
extends IWidgetOptions {
hideOnZoom?: boolean
selectOn?: string[]
onHide?: (widget: DOMWidget<T, V>) => void
onHide?: (widget: BaseDOMWidget<V>) => void
getValue?: () => V
setValue?: (value: V) => void
getMinHeight?: () => number
getMaxHeight?: () => number
getHeight?: () => string | number
onDraw?: (widget: DOMWidget<T, V>) => void
onDraw?: (widget: BaseDOMWidget<V>) => void
/**
* @deprecated Use `afterResize` instead. This callback is a legacy API
* that fires before resize happens, but it is no longer supported. Now it
@@ -53,21 +70,24 @@ export interface DOMWidgetOptions<
* The resize logic has been upstreamed to litegraph in
* https://github.com/Comfy-Org/ComfyUI_frontend/pull/2557
*/
beforeResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
beforeResize?: (this: BaseDOMWidget<V>, node: LGraphNode) => void
afterResize?: (this: BaseDOMWidget<V>, node: LGraphNode) => void
}
export const isDOMWidget = <T extends HTMLElement, V extends object | string>(
widget: IWidget
): widget is DOMWidget<T, V> => 'element' in widget && !!widget.element
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
implements DOMWidget<T, V>
export const isComponentWidget = <V extends object | string>(
widget: IWidget
): widget is ComponentWidget<V> => 'component' in widget && !!widget.component
abstract class BaseDOMWidgetImpl<V extends object | string>
implements BaseDOMWidget<V>
{
readonly type: 'custom'
readonly name: string
readonly element: T
readonly options: DOMWidgetOptions<T, V>
readonly options: DOMWidgetOptions<V>
computedHeight?: number
y: number = 0
callback?: (value: V) => void
@@ -80,13 +100,11 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
node: LGraphNode
name: string
type: string
element: T
options: DOMWidgetOptions<T, V>
options: DOMWidgetOptions<V>
}) {
// @ts-expect-error custom widget type
this.type = obj.type
this.name = obj.name
this.element = obj.element
this.options = obj.options
this.id = obj.id
@@ -102,6 +120,42 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
this.callback?.(this.value)
}
isVisible(): boolean {
return (
!_.isNil(this.computedHeight) &&
this.computedHeight > 0 &&
!['converted-widget', 'hidden'].includes(this.type) &&
!this.node.collapsed
)
}
draw(): void {
this.options.onDraw?.(this)
}
onRemove(): void {
useDomWidgetStore().unregisterWidget(this.id)
}
}
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
extends BaseDOMWidgetImpl<V>
implements DOMWidget<T, V>
{
readonly element: T
constructor(obj: {
id: string
node: LGraphNode
name: string
type: string
element: T
options: DOMWidgetOptions<V>
}) {
super(obj)
this.element = obj.element
}
/** Extract DOM widget size info */
computeLayoutSize(node: LGraphNode) {
// @ts-expect-error custom widget type
@@ -144,25 +198,63 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
minWidth: 0
}
}
}
isVisible(): boolean {
return (
!_.isNil(this.computedHeight) &&
this.computedHeight > 0 &&
!['converted-widget', 'hidden'].includes(this.type) &&
!this.node.collapsed
)
export class ComponentWidgetImpl<V extends object | string>
extends BaseDOMWidgetImpl<V>
implements ComponentWidget<V>
{
readonly component: Component
readonly inputSpec: InputSpec
constructor(obj: {
id: string
node: LGraphNode
name: string
component: Component
inputSpec: InputSpec
options: DOMWidgetOptions<V>
}) {
super({
...obj,
type: 'custom'
})
this.component = obj.component
this.inputSpec = obj.inputSpec
}
draw(): void {
this.options.onDraw?.(this)
computeLayoutSize() {
const minHeight = this.options.getMinHeight?.() ?? 50
const maxHeight = this.options.getMaxHeight?.()
return {
minHeight,
maxHeight,
minWidth: 0
}
}
onRemove(): void {
useDomWidgetStore().unregisterWidget(this.id)
serializeValue(): V {
return toRaw(this.value)
}
}
export const addWidget = <W extends BaseDOMWidget<object | string>>(
node: LGraphNode,
widget: W
) => {
node.addCustomWidget(widget)
node.onRemoved = useChainCallback(node.onRemoved, () => {
widget.onRemove?.()
})
node.onResize = useChainCallback(node.onResize, () => {
widget.options.beforeResize?.call(widget, node)
widget.options.afterResize?.call(widget, node)
})
useDomWidgetStore().registerWidget(widget)
}
LGraphNode.prototype.addDOMWidget = function <
T extends HTMLElement,
V extends object | string
@@ -171,22 +263,18 @@ LGraphNode.prototype.addDOMWidget = function <
name: string,
type: string,
element: T,
options: DOMWidgetOptions<T, V> = {}
options: DOMWidgetOptions<V> = {}
): DOMWidget<T, V> {
const widget = new DOMWidgetImpl({
id: generateUUID(),
node: this,
name,
type,
element,
options: { hideOnZoom: true, ...options }
})
// Note: Before `LGraphNode.configure` is called, `this.id` is always `-1`.
const widget = this.addCustomWidget(
new DOMWidgetImpl({
id: generateUUID(),
node: this,
name,
type,
element,
options: {
hideOnZoom: true,
...options
}
})
)
addWidget(this, widget as unknown as BaseDOMWidget<object | string>)
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
// Some custom nodes are explicitly expecting getter and setter of `value`
@@ -201,16 +289,5 @@ LGraphNode.prototype.addDOMWidget = function <
}
})
this.onRemoved = useChainCallback(this.onRemoved, () => {
widget.onRemove()
})
this.onResize = useChainCallback(this.onResize, () => {
options.beforeResize?.call(widget, this)
options.afterResize?.call(widget, this)
})
useDomWidgetStore().registerWidget(widget)
return widget
}