Implement DOMWidget for vue (#6006)

![vue-dom-widget](https://github.com/user-attachments/assets/d0c0e5f6-bacb-4fd9-957e-4f19e8071c3d)

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:
AustinMroz
2025-10-10 14:11:38 -07:00
committed by GitHub
parent d7796fcda4
commit 2599136296
4 changed files with 40 additions and 6 deletions

View File

@@ -12,6 +12,7 @@ import type {
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isDOMWidget } from '@/scripts/domWidget'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { WidgetValue } from '@/types/simplifiedWidget'
@@ -38,6 +39,7 @@ export interface SafeWidgetData {
callback?: ((value: unknown) => void) | undefined
spec?: InputSpec
slotMetadata?: WidgetSlotMetadata
isDOMWidget?: boolean
}
export interface VueNodeData {
@@ -156,7 +158,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
options: widget.options ? { ...widget.options } : undefined,
callback: widget.callback,
spec,
slotMetadata: slotInfo
slotMetadata: slotInfo,
isDOMWidget: isDOMWidget(widget)
}
} catch (error) {
return {

View File

@@ -62,6 +62,7 @@ import type {
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
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 {
getComponent,
@@ -127,7 +128,8 @@ const processedWidgets = computed((): ProcessedWidget[] => {
if (!shouldRenderAsVue(widget)) continue
const vueComponent =
getComponent(widget.type, widget.name) || WidgetInputText
getComponent(widget.type, widget.name) ||
(widget.isDOMWidget ? WidgetDOM : WidgetInputText)
const slotMetadata = widget.slotMetadata

View File

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

View File

@@ -3,6 +3,8 @@
*/
import type { Component } from 'vue'
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
import WidgetAudioUI from '../components/WidgetAudioUI.vue'
import WidgetButton from '../components/WidgetButton.vue'
import WidgetChart from '../components/WidgetChart.vue'
@@ -169,11 +171,9 @@ export const isEssential = (type: string): boolean => {
return widgets.get(canonicalType)?.essential || false
}
export const shouldRenderAsVue = (widget: {
type?: string
options?: Record<string, unknown>
}): boolean => {
export const shouldRenderAsVue = (widget: Partial<SafeWidgetData>): boolean => {
if (widget.options?.canvasOnly) return false
if (widget.isDOMWidget) return true
if (!widget.type) return false
return isSupported(widget.type)
}