mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Implement DOMWidget for vue (#6006)
 Did testing on about a dozen custom nodes. Most just work. - Some custom nodes have copy/pasted the `addDOMWidget` call with types like `customtext` and get converted to textareas -> Not feasible to fix here. Can open PRs into custom nodes if complaints arise. - Only the KJNodes spline editor had mouse issues -> Can investigate/open PR into KJNodes later. - Many nodes don't resize gracefully. Probably best handled in a future PR. - Some expect to be handled like textareas. These currently have minsize and don't scale. - Others, like VHS previews, scale self properly, but don't update height inside a drag operation -> node height can be set to less than fit. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6006-Implement-DOMWidget-for-vue-2886d73d3650817ca497c15d87d70f4f) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
|||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
import { isDOMWidget } from '@/scripts/domWidget'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import type { WidgetValue } from '@/types/simplifiedWidget'
|
import type { WidgetValue } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export interface SafeWidgetData {
|
|||||||
callback?: ((value: unknown) => void) | undefined
|
callback?: ((value: unknown) => void) | undefined
|
||||||
spec?: InputSpec
|
spec?: InputSpec
|
||||||
slotMetadata?: WidgetSlotMetadata
|
slotMetadata?: WidgetSlotMetadata
|
||||||
|
isDOMWidget?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VueNodeData {
|
export interface VueNodeData {
|
||||||
@@ -156,7 +158,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
options: widget.options ? { ...widget.options } : undefined,
|
options: widget.options ? { ...widget.options } : undefined,
|
||||||
callback: widget.callback,
|
callback: widget.callback,
|
||||||
spec,
|
spec,
|
||||||
slotMetadata: slotInfo
|
slotMetadata: slotInfo,
|
||||||
|
isDOMWidget: isDOMWidget(widget)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import type {
|
|||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
|
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
||||||
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
|
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
|
||||||
import {
|
import {
|
||||||
getComponent,
|
getComponent,
|
||||||
@@ -127,7 +128,8 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
if (!shouldRenderAsVue(widget)) continue
|
if (!shouldRenderAsVue(widget)) continue
|
||||||
|
|
||||||
const vueComponent =
|
const vueComponent =
|
||||||
getComponent(widget.type, widget.name) || WidgetInputText
|
getComponent(widget.type, widget.name) ||
|
||||||
|
(widget.isDOMWidget ? WidgetDOM : WidgetInputText)
|
||||||
|
|
||||||
const slotMetadata = widget.slotMetadata
|
const slotMetadata = widget.slotMetadata
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
|
import { isDOMWidget } from '@/scripts/domWidget'
|
||||||
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
|
// Button widgets don't have a v-model value, they trigger actions
|
||||||
|
const props = defineProps<{
|
||||||
|
widget: SimplifiedWidget<void>
|
||||||
|
nodeId: string
|
||||||
|
readonly?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const domEl = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const { canvas } = useCanvasStore()
|
||||||
|
onMounted(() => {
|
||||||
|
if (!domEl.value) return
|
||||||
|
const node = canvas?.graph?.getNodeById(props.nodeId) ?? undefined
|
||||||
|
if (!node) return
|
||||||
|
const widget = node.widgets?.find((w) => w.name === props.widget.name)
|
||||||
|
if (!widget || !isDOMWidget(widget)) return
|
||||||
|
domEl.value.replaceChildren(widget.element)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="domEl" />
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
|
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
|
||||||
|
|
||||||
import WidgetAudioUI from '../components/WidgetAudioUI.vue'
|
import WidgetAudioUI from '../components/WidgetAudioUI.vue'
|
||||||
import WidgetButton from '../components/WidgetButton.vue'
|
import WidgetButton from '../components/WidgetButton.vue'
|
||||||
import WidgetChart from '../components/WidgetChart.vue'
|
import WidgetChart from '../components/WidgetChart.vue'
|
||||||
@@ -169,11 +171,9 @@ export const isEssential = (type: string): boolean => {
|
|||||||
return widgets.get(canonicalType)?.essential || false
|
return widgets.get(canonicalType)?.essential || false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shouldRenderAsVue = (widget: {
|
export const shouldRenderAsVue = (widget: Partial<SafeWidgetData>): boolean => {
|
||||||
type?: string
|
|
||||||
options?: Record<string, unknown>
|
|
||||||
}): boolean => {
|
|
||||||
if (widget.options?.canvasOnly) return false
|
if (widget.options?.canvasOnly) return false
|
||||||
|
if (widget.isDOMWidget) return true
|
||||||
if (!widget.type) return false
|
if (!widget.type) return false
|
||||||
return isSupported(widget.type)
|
return isSupported(widget.type)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user