mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
Vue component multi-select widget (#2987)
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
:key="widget.id"
|
:key="widget.id"
|
||||||
:widget="widget"
|
:widget="widget"
|
||||||
:widget-state="domWidgetStore.widgetStates.get(widget.id)"
|
:widget-state="domWidgetStore.widgetStates.get(widget.id)"
|
||||||
|
@update:widget-value="widget.value = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -16,7 +17,7 @@ import { computed, watch } from 'vue'
|
|||||||
|
|
||||||
import DomWidget from '@/components/graph/widgets/DomWidget.vue'
|
import DomWidget from '@/components/graph/widgets/DomWidget.vue'
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
import { DOMWidget } from '@/scripts/domWidget'
|
import { BaseDOMWidget } from '@/scripts/domWidget'
|
||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ const domWidgetStore = useDomWidgetStore()
|
|||||||
const widgets = computed(() =>
|
const widgets = computed(() =>
|
||||||
Array.from(
|
Array.from(
|
||||||
domWidgetStore.widgetInstances.values() as Iterable<
|
domWidgetStore.widgetInstances.values() as Iterable<
|
||||||
DOMWidget<HTMLElement, object | string>
|
BaseDOMWidget<string | object>
|
||||||
>
|
>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,15 @@
|
|||||||
ref="widgetElement"
|
ref="widgetElement"
|
||||||
:style="style"
|
:style="style"
|
||||||
v-show="widgetState.visible"
|
v-show="widgetState.visible"
|
||||||
/>
|
>
|
||||||
|
<component
|
||||||
|
v-if="isComponentWidget(widget)"
|
||||||
|
:is="widget.component"
|
||||||
|
:modelValue="widget.value"
|
||||||
|
@update:modelValue="emit('update:widgetValue', $event)"
|
||||||
|
:widget="widget"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -15,16 +23,24 @@ import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
|
|||||||
|
|
||||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||||
import { useDomClipping } from '@/composables/element/useDomClipping'
|
import { useDomClipping } from '@/composables/element/useDomClipping'
|
||||||
import type { DOMWidget } from '@/scripts/domWidget'
|
import {
|
||||||
|
type BaseDOMWidget,
|
||||||
|
isComponentWidget,
|
||||||
|
isDOMWidget
|
||||||
|
} from '@/scripts/domWidget'
|
||||||
import { DomWidgetState } from '@/stores/domWidgetStore'
|
import { DomWidgetState } from '@/stores/domWidgetStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
const { widget, widgetState } = defineProps<{
|
const { widget, widgetState } = defineProps<{
|
||||||
widget: DOMWidget<HTMLElement, any>
|
widget: BaseDOMWidget<string | object>
|
||||||
widgetState: DomWidgetState
|
widgetState: DomWidgetState
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:widgetValue', value: string | object): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const widgetElement = ref<HTMLElement>()
|
const widgetElement = ref<HTMLElement>()
|
||||||
|
|
||||||
const { style: positionStyle, updatePositionWithTransform } =
|
const { style: positionStyle, updatePositionWithTransform } =
|
||||||
@@ -92,27 +108,31 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (widget.element.blur) {
|
if (isDOMWidget(widget)) {
|
||||||
useEventListener(document, 'mousedown', (event) => {
|
if (widget.element.blur) {
|
||||||
if (!widget.element.contains(event.target as HTMLElement)) {
|
useEventListener(document, 'mousedown', (event) => {
|
||||||
widget.element.blur()
|
if (!widget.element.contains(event.target as HTMLElement)) {
|
||||||
}
|
widget.element.blur()
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
|
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
|
||||||
useEventListener(widget.element, evt, () => {
|
useEventListener(widget.element, evt, () => {
|
||||||
const lgCanvas = canvasStore.canvas
|
const lgCanvas = canvasStore.canvas
|
||||||
lgCanvas?.selectNode(widget.node)
|
lgCanvas?.selectNode(widget.node)
|
||||||
lgCanvas?.bringToFront(widget.node)
|
lgCanvas?.bringToFront(widget.node)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputSpec = widget.node.constructor.nodeData
|
const inputSpec = widget.node.constructor.nodeData
|
||||||
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
widgetElement.value.appendChild(widget.element)
|
if (isDOMWidget(widget)) {
|
||||||
|
widgetElement.value.appendChild(widget.element)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
30
src/components/graph/widgets/MultiSelectWidget.vue
Normal file
30
src/components/graph/widgets/MultiSelectWidget.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MultiSelect
|
||||||
|
v-model="selectedItems"
|
||||||
|
:options="options"
|
||||||
|
filter
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxSelectedLabels="3"
|
||||||
|
:display="display"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MultiSelect from 'primevue/multiselect'
|
||||||
|
|
||||||
|
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||||
|
|
||||||
|
const selectedItems = defineModel<string[]>({ required: true })
|
||||||
|
const { widget } = defineProps<{
|
||||||
|
widget: ComponentWidget<string[]>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const inputSpec = widget.inputSpec as ComboInputSpec
|
||||||
|
const options = inputSpec.options ?? []
|
||||||
|
const placeholder = inputSpec.multi_select?.placeholder ?? 'Select items'
|
||||||
|
const display = inputSpec.multi_select?.chip ? 'chip' : 'comma'
|
||||||
|
</script>
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||||
import {
|
import {
|
||||||
ComboInputSpec,
|
ComboInputSpec,
|
||||||
type InputSpec,
|
type InputSpec,
|
||||||
isComboInputSpec
|
isComboInputSpec
|
||||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
import {
|
||||||
|
type BaseDOMWidget,
|
||||||
|
ComponentWidgetImpl,
|
||||||
|
addWidget
|
||||||
|
} from '@/scripts/domWidget'
|
||||||
import {
|
import {
|
||||||
type ComfyWidgetConstructorV2,
|
type ComfyWidgetConstructorV2,
|
||||||
addValueControlWidgets
|
addValueControlWidgets
|
||||||
} from '@/scripts/widgets'
|
} from '@/scripts/widgets'
|
||||||
|
import { generateUUID } from '@/utils/formatUtil'
|
||||||
|
|
||||||
import { useRemoteWidget } from './useRemoteWidget'
|
import { useRemoteWidget } from './useRemoteWidget'
|
||||||
|
|
||||||
@@ -21,6 +29,71 @@ const getDefaultValue = (inputSpec: ComboInputSpec) => {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
|
||||||
|
const widgetValue = ref<string[]>([])
|
||||||
|
const widget = new ComponentWidgetImpl({
|
||||||
|
id: generateUUID(),
|
||||||
|
node,
|
||||||
|
name: inputSpec.name,
|
||||||
|
component: MultiSelectWidget,
|
||||||
|
inputSpec,
|
||||||
|
options: {
|
||||||
|
getValue: () => widgetValue.value,
|
||||||
|
setValue: (value: string[]) => {
|
||||||
|
widgetValue.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
addWidget(node, widget as BaseDOMWidget<object | string>)
|
||||||
|
// TODO: Add remote support to multi-select widget
|
||||||
|
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3003
|
||||||
|
return widget
|
||||||
|
}
|
||||||
|
|
||||||
|
const addComboWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
|
||||||
|
const defaultValue = getDefaultValue(inputSpec)
|
||||||
|
const comboOptions = inputSpec.options ?? []
|
||||||
|
const widget = node.addWidget(
|
||||||
|
'combo',
|
||||||
|
inputSpec.name,
|
||||||
|
defaultValue,
|
||||||
|
() => {},
|
||||||
|
{
|
||||||
|
values: comboOptions
|
||||||
|
}
|
||||||
|
) as IComboWidget
|
||||||
|
|
||||||
|
if (inputSpec.remote) {
|
||||||
|
const remoteWidget = useRemoteWidget({
|
||||||
|
remoteConfig: inputSpec.remote,
|
||||||
|
defaultValue,
|
||||||
|
node,
|
||||||
|
widget
|
||||||
|
})
|
||||||
|
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
|
||||||
|
|
||||||
|
const origOptions = widget.options
|
||||||
|
widget.options = new Proxy(origOptions as Record<string | symbol, any>, {
|
||||||
|
get(target, prop: string | symbol) {
|
||||||
|
if (prop !== 'values') return target[prop]
|
||||||
|
return remoteWidget.getValue()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputSpec.control_after_generate) {
|
||||||
|
widget.linkedWidgets = addValueControlWidgets(
|
||||||
|
node,
|
||||||
|
widget,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
transformInputSpecV2ToV1(inputSpec)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget
|
||||||
|
}
|
||||||
|
|
||||||
export const useComboWidget = () => {
|
export const useComboWidget = () => {
|
||||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||||
node: LGraphNode,
|
node: LGraphNode,
|
||||||
@@ -29,48 +102,9 @@ export const useComboWidget = () => {
|
|||||||
if (!isComboInputSpec(inputSpec)) {
|
if (!isComboInputSpec(inputSpec)) {
|
||||||
throw new Error(`Invalid input data: ${inputSpec}`)
|
throw new Error(`Invalid input data: ${inputSpec}`)
|
||||||
}
|
}
|
||||||
|
return inputSpec.multi_select
|
||||||
const comboOptions = inputSpec.options ?? []
|
? addMultiSelectWidget(node, inputSpec)
|
||||||
const defaultValue = getDefaultValue(inputSpec)
|
: addComboWidget(node, inputSpec)
|
||||||
|
|
||||||
const widget = node.addWidget(
|
|
||||||
'combo',
|
|
||||||
inputSpec.name,
|
|
||||||
defaultValue,
|
|
||||||
() => {},
|
|
||||||
{
|
|
||||||
values: comboOptions
|
|
||||||
}
|
|
||||||
) as IComboWidget
|
|
||||||
|
|
||||||
if (inputSpec.remote) {
|
|
||||||
const remoteWidget = useRemoteWidget({
|
|
||||||
remoteConfig: inputSpec.remote,
|
|
||||||
defaultValue,
|
|
||||||
node,
|
|
||||||
widget
|
|
||||||
})
|
|
||||||
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
|
|
||||||
|
|
||||||
const origOptions = widget.options
|
|
||||||
widget.options = new Proxy(origOptions as Record<string | symbol, any>, {
|
|
||||||
get(target, prop: string | symbol) {
|
|
||||||
if (prop !== 'values') return target[prop]
|
|
||||||
return remoteWidget.getValue()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputSpec.control_after_generate) {
|
|
||||||
widget.linkedWidgets = addValueControlWidgets(
|
|
||||||
node,
|
|
||||||
widget,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
transformInputSpecV2ToV1(inputSpec)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return widget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return widgetConstructor
|
return widgetConstructor
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ const zRemoteWidgetConfig = z.object({
|
|||||||
timeout: z.number().gte(0).optional(),
|
timeout: z.number().gte(0).optional(),
|
||||||
max_retries: z.number().gte(0).optional()
|
max_retries: z.number().gte(0).optional()
|
||||||
})
|
})
|
||||||
|
const zMultiSelectOption = z.object({
|
||||||
|
placeholder: z.string().optional(),
|
||||||
|
chip: z.boolean().optional()
|
||||||
|
})
|
||||||
|
|
||||||
export const zBaseInputOptions = z
|
export const zBaseInputOptions = z
|
||||||
.object({
|
.object({
|
||||||
@@ -72,7 +76,9 @@ export const zComboInputOptions = zBaseInputOptions.extend({
|
|||||||
allow_batch: z.boolean().optional(),
|
allow_batch: z.boolean().optional(),
|
||||||
video_upload: z.boolean().optional(),
|
video_upload: z.boolean().optional(),
|
||||||
options: z.array(zComboOption).optional(),
|
options: z.array(zComboOption).optional(),
|
||||||
remote: zRemoteWidgetConfig.optional()
|
remote: zRemoteWidgetConfig.optional(),
|
||||||
|
/** Whether the widget is a multi-select widget. */
|
||||||
|
multi_select: zMultiSelectOption.optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()])
|
const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()])
|
||||||
|
|||||||
@@ -5,47 +5,64 @@ import type {
|
|||||||
IWidgetOptions
|
IWidgetOptions
|
||||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import { type Component, toRaw } from 'vue'
|
||||||
|
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
import { generateUUID } from '@/utils/formatUtil'
|
import { generateUUID } from '@/utils/formatUtil'
|
||||||
|
|
||||||
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
export interface BaseDOMWidget<V extends object | string>
|
||||||
extends ICustomWidget {
|
extends ICustomWidget {
|
||||||
// ICustomWidget properties
|
// ICustomWidget properties
|
||||||
type: 'custom'
|
type: 'custom'
|
||||||
element: T
|
options: DOMWidgetOptions<V>
|
||||||
options: DOMWidgetOptions<T, V>
|
|
||||||
value: 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
|
callback?: (value: V) => void
|
||||||
// DOMWidget properties
|
|
||||||
|
// BaseDOMWidget properties
|
||||||
/** The unique ID of the widget. */
|
/** The unique ID of the widget. */
|
||||||
id: string
|
readonly id: string
|
||||||
/** The node that the widget belongs to. */
|
/** The node that the widget belongs to. */
|
||||||
node: LGraphNode
|
readonly node: LGraphNode
|
||||||
/** Whether the widget is visible. */
|
/** Whether the widget is visible. */
|
||||||
isVisible(): boolean
|
isVisible(): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DOMWidgetOptions<
|
/**
|
||||||
T extends HTMLElement,
|
* A DOM widget that wraps a custom HTML element as a litegraph widget.
|
||||||
V extends object | string
|
*/
|
||||||
> extends IWidgetOptions {
|
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
|
hideOnZoom?: boolean
|
||||||
selectOn?: string[]
|
selectOn?: string[]
|
||||||
onHide?: (widget: DOMWidget<T, V>) => void
|
onHide?: (widget: BaseDOMWidget<V>) => void
|
||||||
getValue?: () => V
|
getValue?: () => V
|
||||||
setValue?: (value: V) => void
|
setValue?: (value: V) => void
|
||||||
getMinHeight?: () => number
|
getMinHeight?: () => number
|
||||||
getMaxHeight?: () => number
|
getMaxHeight?: () => number
|
||||||
getHeight?: () => string | 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
|
* @deprecated Use `afterResize` instead. This callback is a legacy API
|
||||||
* that fires before resize happens, but it is no longer supported. Now it
|
* 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
|
* The resize logic has been upstreamed to litegraph in
|
||||||
* https://github.com/Comfy-Org/ComfyUI_frontend/pull/2557
|
* https://github.com/Comfy-Org/ComfyUI_frontend/pull/2557
|
||||||
*/
|
*/
|
||||||
beforeResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
beforeResize?: (this: BaseDOMWidget<V>, node: LGraphNode) => void
|
||||||
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
afterResize?: (this: BaseDOMWidget<V>, node: LGraphNode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isDOMWidget = <T extends HTMLElement, V extends object | string>(
|
export const isDOMWidget = <T extends HTMLElement, V extends object | string>(
|
||||||
widget: IWidget
|
widget: IWidget
|
||||||
): widget is DOMWidget<T, V> => 'element' in widget && !!widget.element
|
): widget is DOMWidget<T, V> => 'element' in widget && !!widget.element
|
||||||
|
|
||||||
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
export const isComponentWidget = <V extends object | string>(
|
||||||
implements DOMWidget<T, V>
|
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 type: 'custom'
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly element: T
|
readonly options: DOMWidgetOptions<V>
|
||||||
readonly options: DOMWidgetOptions<T, V>
|
|
||||||
computedHeight?: number
|
computedHeight?: number
|
||||||
y: number = 0
|
y: number = 0
|
||||||
callback?: (value: V) => void
|
callback?: (value: V) => void
|
||||||
@@ -80,13 +100,11 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
node: LGraphNode
|
node: LGraphNode
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
element: T
|
options: DOMWidgetOptions<V>
|
||||||
options: DOMWidgetOptions<T, V>
|
|
||||||
}) {
|
}) {
|
||||||
// @ts-expect-error custom widget type
|
// @ts-expect-error custom widget type
|
||||||
this.type = obj.type
|
this.type = obj.type
|
||||||
this.name = obj.name
|
this.name = obj.name
|
||||||
this.element = obj.element
|
|
||||||
this.options = obj.options
|
this.options = obj.options
|
||||||
|
|
||||||
this.id = obj.id
|
this.id = obj.id
|
||||||
@@ -102,6 +120,42 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
this.callback?.(this.value)
|
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 */
|
/** Extract DOM widget size info */
|
||||||
computeLayoutSize(node: LGraphNode) {
|
computeLayoutSize(node: LGraphNode) {
|
||||||
// @ts-expect-error custom widget type
|
// @ts-expect-error custom widget type
|
||||||
@@ -144,25 +198,63 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
minWidth: 0
|
minWidth: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isVisible(): boolean {
|
export class ComponentWidgetImpl<V extends object | string>
|
||||||
return (
|
extends BaseDOMWidgetImpl<V>
|
||||||
!_.isNil(this.computedHeight) &&
|
implements ComponentWidget<V>
|
||||||
this.computedHeight > 0 &&
|
{
|
||||||
!['converted-widget', 'hidden'].includes(this.type) &&
|
readonly component: Component
|
||||||
!this.node.collapsed
|
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 {
|
computeLayoutSize() {
|
||||||
this.options.onDraw?.(this)
|
const minHeight = this.options.getMinHeight?.() ?? 50
|
||||||
|
const maxHeight = this.options.getMaxHeight?.()
|
||||||
|
return {
|
||||||
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
|
minWidth: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(): void {
|
serializeValue(): V {
|
||||||
useDomWidgetStore().unregisterWidget(this.id)
|
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 <
|
LGraphNode.prototype.addDOMWidget = function <
|
||||||
T extends HTMLElement,
|
T extends HTMLElement,
|
||||||
V extends object | string
|
V extends object | string
|
||||||
@@ -171,22 +263,18 @@ LGraphNode.prototype.addDOMWidget = function <
|
|||||||
name: string,
|
name: string,
|
||||||
type: string,
|
type: string,
|
||||||
element: T,
|
element: T,
|
||||||
options: DOMWidgetOptions<T, V> = {}
|
options: DOMWidgetOptions<V> = {}
|
||||||
): DOMWidget<T, 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`.
|
// Note: Before `LGraphNode.configure` is called, `this.id` is always `-1`.
|
||||||
const widget = this.addCustomWidget(
|
addWidget(this, widget as unknown as BaseDOMWidget<object | string>)
|
||||||
new DOMWidgetImpl({
|
|
||||||
id: generateUUID(),
|
|
||||||
node: this,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
element,
|
|
||||||
options: {
|
|
||||||
hideOnZoom: true,
|
|
||||||
...options
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
|
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
|
||||||
// Some custom nodes are explicitly expecting getter and setter of `value`
|
// 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
|
return widget
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { markRaw, ref } from 'vue'
|
import { markRaw, ref } from 'vue'
|
||||||
|
|
||||||
import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
||||||
import type { DOMWidget } from '@/scripts/domWidget'
|
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||||
|
|
||||||
export interface DomWidgetState extends PositionConfig {
|
export interface DomWidgetState extends PositionConfig {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -18,17 +18,15 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
|||||||
|
|
||||||
// Map to reference actual widget instances
|
// Map to reference actual widget instances
|
||||||
// Widgets are stored as raw values to avoid reactivity issues
|
// Widgets are stored as raw values to avoid reactivity issues
|
||||||
const widgetInstances = ref(
|
const widgetInstances = ref(new Map<string, BaseDOMWidget<object | string>>())
|
||||||
new Map<string, DOMWidget<HTMLElement, object | string>>()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register a widget with the store
|
// Register a widget with the store
|
||||||
const registerWidget = <T extends HTMLElement, V extends object | string>(
|
const registerWidget = <V extends object | string>(
|
||||||
widget: DOMWidget<T, V>
|
widget: BaseDOMWidget<V>
|
||||||
) => {
|
) => {
|
||||||
widgetInstances.value.set(
|
widgetInstances.value.set(
|
||||||
widget.id,
|
widget.id,
|
||||||
markRaw(widget as unknown as DOMWidget<HTMLElement, object | string>)
|
markRaw(widget) as unknown as BaseDOMWidget<object | string>
|
||||||
)
|
)
|
||||||
widgetStates.value.set(widget.id, {
|
widgetStates.value.set(widget.id, {
|
||||||
visible: true,
|
visible: true,
|
||||||
|
|||||||
2
src/types/litegraph-augmentation.d.ts
vendored
2
src/types/litegraph-augmentation.d.ts
vendored
@@ -134,7 +134,7 @@ declare module '@comfyorg/litegraph' {
|
|||||||
name: string,
|
name: string,
|
||||||
type: string,
|
type: string,
|
||||||
element: T,
|
element: T,
|
||||||
options?: DOMWidgetOptions<T, V>
|
options?: DOMWidgetOptions<V>
|
||||||
): DOMWidget<T, V>
|
): DOMWidget<T, V>
|
||||||
|
|
||||||
animatedImages?: boolean
|
animatedImages?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user