mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
230 lines
6.3 KiB
Vue
230 lines
6.3 KiB
Vue
<template>
|
|
<div
|
|
v-show="widgetState.visible"
|
|
ref="widgetElement"
|
|
class="dom-widget size-full"
|
|
:title="tooltip"
|
|
:style="style"
|
|
>
|
|
<component
|
|
:is="widget.component"
|
|
v-if="isComponentWidget(widget)"
|
|
class="size-full"
|
|
:model-value="widget.value"
|
|
:widget="widget"
|
|
v-bind="widget.props"
|
|
@update:model-value="emit('update:widgetValue', $event)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useElementBounding, useEventListener, whenever } from '@vueuse/core'
|
|
import type { CSSProperties } from 'vue'
|
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
|
|
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
|
import { useDomClipping } from '@/composables/element/useDomClipping'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { isComponentWidget, isDOMWidget } from '@/scripts/domWidget'
|
|
import type { DomWidgetState } from '@/stores/domWidgetStore'
|
|
|
|
const { widgetState } = defineProps<{
|
|
widgetState: DomWidgetState
|
|
}>()
|
|
const widget = widgetState.widget
|
|
|
|
const emit = defineEmits<{
|
|
'update:widgetValue': [value: string | object]
|
|
}>()
|
|
|
|
const widgetElement = ref<HTMLElement | undefined>()
|
|
|
|
/**
|
|
* @note Do NOT convert style to a computed value, as it will cause lag when
|
|
* updating the style on different animation frames. Vue's computed value is
|
|
* evaluated asynchronously.
|
|
*/
|
|
const style = ref<CSSProperties>({})
|
|
const { style: positionStyle, updatePosition } = useAbsolutePosition({
|
|
useTransform: true
|
|
})
|
|
const { style: clippingStyle, updateClipPath } = useDomClipping()
|
|
|
|
const canvasStore = useCanvasStore()
|
|
const settingStore = useSettingStore()
|
|
const enableDomClipping = computed(() =>
|
|
settingStore.get('Comfy.DOMClippingEnabled')
|
|
)
|
|
|
|
const updateDomClipping = () => {
|
|
const lgCanvas = canvasStore.canvas
|
|
if (!lgCanvas || !widgetElement.value) return
|
|
|
|
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
|
|
if (!selectedNode) {
|
|
// Clear clipping when no node is selected
|
|
updateClipPath(widgetElement.value, lgCanvas.canvas, false, undefined)
|
|
return
|
|
}
|
|
|
|
const override = widgetState.positionOverride
|
|
const overrideInGraph =
|
|
override && lgCanvas.graph?.getNodeById(override.node.id)
|
|
const ownerNode = overrideInGraph ? override.node : widgetState.widget.node
|
|
const isSelected = selectedNode === ownerNode
|
|
const renderArea = selectedNode?.renderArea
|
|
const offset = lgCanvas.ds.offset
|
|
const scale = lgCanvas.ds.scale
|
|
const selectedAreaConfig = renderArea
|
|
? {
|
|
x: renderArea[0],
|
|
y: renderArea[1],
|
|
width: renderArea[2],
|
|
height: renderArea[3],
|
|
scale,
|
|
offset: [offset[0], offset[1]] as [number, number]
|
|
}
|
|
: undefined
|
|
|
|
updateClipPath(
|
|
widgetElement.value,
|
|
lgCanvas.canvas,
|
|
isSelected,
|
|
selectedAreaConfig
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @note mapping between canvas position and client position depends on the
|
|
* canvas element's position, so we need to watch the canvas element's position
|
|
* and update the position of the widget accordingly.
|
|
*/
|
|
const { left, top } = useElementBounding(canvasStore.getCanvas().canvas)
|
|
|
|
function composeStyle() {
|
|
const override = widgetState.positionOverride
|
|
const isDisabled = override
|
|
? (override.widget.computedDisabled ?? widget.computedDisabled)
|
|
: widget.computedDisabled
|
|
|
|
style.value = {
|
|
...positionStyle.value,
|
|
...(enableDomClipping.value ? clippingStyle.value : {}),
|
|
zIndex: widgetState.zIndex,
|
|
pointerEvents:
|
|
!widgetState.visible || widgetState.readonly || isDisabled
|
|
? 'none'
|
|
: 'auto',
|
|
opacity: isDisabled ? 0.5 : 1
|
|
}
|
|
}
|
|
|
|
watch([() => widgetState.pos, () => widgetState.size, left, top], () => {
|
|
updatePosition(widgetState)
|
|
if (enableDomClipping.value) {
|
|
updateDomClipping()
|
|
}
|
|
composeStyle()
|
|
})
|
|
|
|
watch(
|
|
[
|
|
() => widgetState.zIndex,
|
|
() => widgetState.readonly,
|
|
() => widgetState.positionOverride,
|
|
enableDomClipping
|
|
],
|
|
() => {
|
|
if (enableDomClipping.value) {
|
|
updateDomClipping()
|
|
}
|
|
composeStyle()
|
|
}
|
|
)
|
|
|
|
// Recompose style when clippingStyle updates asynchronously via RAF.
|
|
// updateClipPath() schedules clip-path calculation in a requestAnimationFrame,
|
|
// so clippingStyle.value updates after the main watcher has already composed
|
|
// style. This watcher ensures the new clip-path is applied to the DOM.
|
|
watch(clippingStyle, composeStyle, { deep: true })
|
|
|
|
watch(
|
|
() => widgetState.visible,
|
|
(newVisible, oldVisible) => {
|
|
if (!newVisible && oldVisible) {
|
|
widget.options.onHide?.(widget)
|
|
}
|
|
}
|
|
)
|
|
useEventListener(document, 'mousedown', (event) => {
|
|
if (!isDOMWidget(widget) || !widgetState.visible || !widget.element.blur) {
|
|
return
|
|
}
|
|
if (!widget.element.contains(event.target as HTMLElement)) {
|
|
widget.element.blur()
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
if (!isDOMWidget(widget)) {
|
|
return
|
|
}
|
|
useEventListener(
|
|
widget.element,
|
|
widget.options.selectOn ?? ['focus', 'click'],
|
|
() => {
|
|
const lgCanvas = canvasStore.canvas
|
|
if (!lgCanvas) return
|
|
|
|
const override = widgetState.positionOverride
|
|
const overrideInGraph =
|
|
override && lgCanvas.graph?.getNodeById(override.node.id)
|
|
const ownerNode = overrideInGraph
|
|
? override.node
|
|
: widgetState.widget.node
|
|
|
|
lgCanvas.selectNode(ownerNode)
|
|
lgCanvas.bringToFront(ownerNode)
|
|
}
|
|
)
|
|
})
|
|
|
|
const inputSpec = widget.node.constructor.nodeData
|
|
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
|
|
|
// Mount DOM element when widget is or becomes visible
|
|
const mountElementIfVisible = () => {
|
|
if (!(widgetState.visible && isDOMWidget(widget) && widgetElement.value)) {
|
|
return
|
|
}
|
|
// Only append if not already a child
|
|
if (widgetElement.value.contains(widget.element)) {
|
|
return
|
|
}
|
|
|
|
widget.element.classList.add('h-full', 'w-full')
|
|
widgetElement.value.appendChild(widget.element)
|
|
}
|
|
|
|
// Check on mount - but only after next tick to ensure visibility is calculated
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
mountElementIfVisible()
|
|
}).catch((error) => {
|
|
console.error('Error mounting DOM widget element:', error)
|
|
})
|
|
})
|
|
|
|
// And watch for visibility changes
|
|
watch(
|
|
() => widgetState.visible,
|
|
() => {
|
|
mountElementIfVisible()
|
|
}
|
|
)
|
|
|
|
whenever(() => !canvasStore.linearMode, mountElementIfVisible)
|
|
</script>
|