Skip to content

Commit f2eada8

Browse files
author
Chenlei Hu
committed
Add multi-select widget
1 parent 4cd3f0b commit f2eada8

File tree

4 files changed

+144
-69
lines changed

4 files changed

+144
-69
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<template>
2+
<div>
3+
<MultiSelect
4+
v-model="selectedItems"
5+
:options="options"
6+
filter
7+
:placeholder="placeholder"
8+
:maxSelectedLabels="3"
9+
class="w-full"
10+
/>
11+
</div>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import MultiSelect from 'primevue/multiselect'
16+
import { computed, defineModel } from 'vue'
17+
18+
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
19+
import type { ComponentWidget } from '@/scripts/domWidget'
20+
21+
const selectedItems = defineModel<string[]>({ required: true })
22+
const { widget } = defineProps<{
23+
widget: ComponentWidget<string[]>
24+
}>()
25+
const options = computed(
26+
() => (widget.inputSpec as ComboInputSpec).options ?? []
27+
)
28+
const placeholder = computed(
29+
() => (widget.inputSpec as ComboInputSpec).placeholder ?? 'Select items'
30+
)
31+
</script>

src/composables/widgets/useComboWidget.ts

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import type { LGraphNode } from '@comfyorg/litegraph'
22
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
3+
import { ref } from 'vue'
34

5+
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
46
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
57
import {
68
ComboInputSpec,
79
type InputSpec,
810
isComboInputSpec
911
} from '@/schemas/nodeDef/nodeDefSchemaV2'
12+
import {
13+
type BaseDOMWidget,
14+
ComponentWidgetImpl,
15+
addWidget
16+
} from '@/scripts/domWidget'
1017
import {
1118
type ComfyWidgetConstructorV2,
1219
addValueControlWidgets
1320
} from '@/scripts/widgets'
21+
import { generateUUID } from '@/utils/formatUtil'
1422

1523
import { useRemoteWidget } from './useRemoteWidget'
1624

@@ -21,56 +29,81 @@ const getDefaultValue = (inputSpec: ComboInputSpec) => {
2129
return undefined
2230
}
2331

24-
export const useComboWidget = () => {
25-
const widgetConstructor: ComfyWidgetConstructorV2 = (
26-
node: LGraphNode,
27-
inputSpec: InputSpec
28-
) => {
29-
if (!isComboInputSpec(inputSpec)) {
30-
throw new Error(`Invalid input data: ${inputSpec}`)
32+
const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
33+
const widgetValue = ref<string[]>([])
34+
const widget = new ComponentWidgetImpl({
35+
id: generateUUID(),
36+
node,
37+
name: inputSpec.name,
38+
component: MultiSelectWidget,
39+
inputSpec,
40+
options: {
41+
getValue: () => widgetValue.value,
42+
setValue: (value: string[]) => {
43+
widgetValue.value = value
44+
}
3145
}
46+
})
47+
addWidget(node, widget as BaseDOMWidget<object | string>)
48+
// TODO: Add remote support to multi-select widget
49+
return widget
50+
}
3251

33-
const comboOptions = inputSpec.options ?? []
34-
const defaultValue = getDefaultValue(inputSpec)
52+
const addComboWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
53+
const defaultValue = getDefaultValue(inputSpec)
54+
const comboOptions = inputSpec.options ?? []
55+
const widget = node.addWidget(
56+
'combo',
57+
inputSpec.name,
58+
defaultValue,
59+
() => {},
60+
{
61+
values: comboOptions
62+
}
63+
) as IComboWidget
3564

36-
const widget = node.addWidget(
37-
'combo',
38-
inputSpec.name,
65+
if (inputSpec.remote) {
66+
const remoteWidget = useRemoteWidget({
67+
remoteConfig: inputSpec.remote,
3968
defaultValue,
40-
() => {},
41-
{
42-
values: comboOptions
69+
node,
70+
widget
71+
})
72+
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
73+
74+
const origOptions = widget.options
75+
widget.options = new Proxy(origOptions as Record<string | symbol, any>, {
76+
get(target, prop: string | symbol) {
77+
if (prop !== 'values') return target[prop]
78+
return remoteWidget.getValue()
4379
}
44-
) as IComboWidget
80+
})
81+
}
4582

46-
if (inputSpec.remote) {
47-
const remoteWidget = useRemoteWidget({
48-
remoteConfig: inputSpec.remote,
49-
defaultValue,
50-
node,
51-
widget
52-
})
53-
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
83+
if (inputSpec.control_after_generate) {
84+
widget.linkedWidgets = addValueControlWidgets(
85+
node,
86+
widget,
87+
undefined,
88+
undefined,
89+
transformInputSpecV2ToV1(inputSpec)
90+
)
91+
}
5492

55-
const origOptions = widget.options
56-
widget.options = new Proxy(origOptions as Record<string | symbol, any>, {
57-
get(target, prop: string | symbol) {
58-
if (prop !== 'values') return target[prop]
59-
return remoteWidget.getValue()
60-
}
61-
})
62-
}
93+
return widget
94+
}
6395

64-
if (inputSpec.control_after_generate) {
65-
widget.linkedWidgets = addValueControlWidgets(
66-
node,
67-
widget,
68-
undefined,
69-
undefined,
70-
transformInputSpecV2ToV1(inputSpec)
71-
)
96+
export const useComboWidget = () => {
97+
const widgetConstructor: ComfyWidgetConstructorV2 = (
98+
node: LGraphNode,
99+
inputSpec: InputSpec
100+
) => {
101+
if (!isComboInputSpec(inputSpec)) {
102+
throw new Error(`Invalid input data: ${inputSpec}`)
72103
}
73-
return widget
104+
return inputSpec.multi_select
105+
? addMultiSelectWidget(node, inputSpec)
106+
: addComboWidget(node, inputSpec)
74107
}
75108

76109
return widgetConstructor

src/schemas/nodeDefSchema.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ export const zComboInputOptions = zBaseInputOptions.extend({
7272
allow_batch: z.boolean().optional(),
7373
video_upload: z.boolean().optional(),
7474
options: z.array(zComboOption).optional(),
75-
remote: zRemoteWidgetConfig.optional()
75+
remote: zRemoteWidgetConfig.optional(),
76+
/** Whether the widget is a multi-select widget. */
77+
multi_select: z.boolean().optional(),
78+
/** Placeholder when no item is selected in multi-select widget. */
79+
placeholder: z.string().optional()
7680
})
7781

7882
const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()])

src/scripts/domWidget.ts

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import _ from 'lodash'
88
import type { Component } from 'vue'
99

1010
import { useChainCallback } from '@/composables/functional/useChainCallback'
11+
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
1112
import { useDomWidgetStore } from '@/stores/domWidgetStore'
1213
import { generateUUID } from '@/utils/formatUtil'
1314

@@ -21,9 +22,9 @@ export interface BaseDOMWidget<V extends object | string>
2122

2223
// BaseDOMWidget properties
2324
/** The unique ID of the widget. */
24-
id: string
25+
readonly id: string
2526
/** The node that the widget belongs to. */
26-
node: LGraphNode
27+
readonly node: LGraphNode
2728
/** Whether the widget is visible. */
2829
isVisible(): boolean
2930
}
@@ -47,7 +48,8 @@ export interface DOMWidget<T extends HTMLElement, V extends object | string>
4748
*/
4849
export interface ComponentWidget<V extends object | string>
4950
extends BaseDOMWidget<V> {
50-
component: Component
51+
readonly component: Component
52+
readonly inputSpec: InputSpec
5153
}
5254

5355
export interface DOMWidgetOptions<V extends object | string>
@@ -203,19 +205,22 @@ export class ComponentWidgetImpl<V extends object | string>
203205
implements ComponentWidget<V>
204206
{
205207
readonly component: Component
208+
readonly inputSpec: InputSpec
206209

207210
constructor(obj: {
208211
id: string
209212
node: LGraphNode
210213
name: string
211214
component: Component
215+
inputSpec: InputSpec
212216
options: DOMWidgetOptions<V>
213217
}) {
214218
super({
215219
...obj,
216220
type: 'custom'
217221
})
218222
this.component = obj.component
223+
this.inputSpec = obj.inputSpec
219224
}
220225

221226
computeLayoutSize() {
@@ -229,6 +234,23 @@ export class ComponentWidgetImpl<V extends object | string>
229234
}
230235
}
231236

237+
export const addWidget = <W extends BaseDOMWidget<object | string>>(
238+
node: LGraphNode,
239+
widget: W
240+
) => {
241+
node.addCustomWidget(widget)
242+
node.onRemoved = useChainCallback(node.onRemoved, () => {
243+
widget.onRemove?.()
244+
})
245+
246+
node.onResize = useChainCallback(node.onResize, () => {
247+
widget.options.beforeResize?.call(widget, node)
248+
widget.options.afterResize?.call(widget, node)
249+
})
250+
251+
useDomWidgetStore().registerWidget(widget)
252+
}
253+
232254
LGraphNode.prototype.addDOMWidget = function <
233255
T extends HTMLElement,
234256
V extends object | string
@@ -239,20 +261,16 @@ LGraphNode.prototype.addDOMWidget = function <
239261
element: T,
240262
options: DOMWidgetOptions<V> = {}
241263
): DOMWidget<T, V> {
264+
const widget = new DOMWidgetImpl({
265+
id: generateUUID(),
266+
node: this,
267+
name,
268+
type,
269+
element,
270+
options: { hideOnZoom: true, ...options }
271+
})
242272
// Note: Before `LGraphNode.configure` is called, `this.id` is always `-1`.
243-
const widget = this.addCustomWidget(
244-
new DOMWidgetImpl({
245-
id: generateUUID(),
246-
node: this,
247-
name,
248-
type,
249-
element,
250-
options: {
251-
hideOnZoom: true,
252-
...options
253-
}
254-
})
255-
)
273+
addWidget(this, widget as unknown as BaseDOMWidget<object | string>)
256274

257275
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
258276
// Some custom nodes are explicitly expecting getter and setter of `value`
@@ -267,16 +285,5 @@ LGraphNode.prototype.addDOMWidget = function <
267285
}
268286
})
269287

270-
this.onRemoved = useChainCallback(this.onRemoved, () => {
271-
widget.onRemove()
272-
})
273-
274-
this.onResize = useChainCallback(this.onResize, () => {
275-
options.beforeResize?.call(widget, this)
276-
options.afterResize?.call(widget, this)
277-
})
278-
279-
useDomWidgetStore().registerWidget(widget)
280-
281288
return widget
282289
}

0 commit comments

Comments
 (0)