mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Manage style of DOM widgets in Vue (#2946)
This commit is contained in:
@@ -1,15 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- Create a new stacking context for widgets to avoid z-index issues -->
|
||||||
<DomWidget v-for="widget in widgets" :key="widget.id" :widget="widget" />
|
<div class="isolate">
|
||||||
|
<DomWidget
|
||||||
|
v-for="widget in widgets"
|
||||||
|
:key="widget.id"
|
||||||
|
:widget="widget"
|
||||||
|
:widget-state="domWidgetStore.widgetStates.get(widget.id)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||||
|
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 { DOMWidget } from '@/scripts/domWidget'
|
import { DOMWidget } from '@/scripts/domWidget'
|
||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
const domWidgetStore = useDomWidgetStore()
|
const domWidgetStore = useDomWidgetStore()
|
||||||
const widgets = computed(() =>
|
const widgets = computed(() =>
|
||||||
@@ -19,4 +28,47 @@ const widgets = computed(() =>
|
|||||||
>
|
>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MARGIN = 10
|
||||||
|
const updateWidgets = () => {
|
||||||
|
const lgCanvas = canvasStore.canvas
|
||||||
|
if (!lgCanvas) return
|
||||||
|
|
||||||
|
const lowQuality = lgCanvas.low_quality
|
||||||
|
for (const widget of domWidgetStore.widgetInstances.values()) {
|
||||||
|
const node = widget.node as LGraphNode
|
||||||
|
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
||||||
|
|
||||||
|
if (!widgetState) continue
|
||||||
|
|
||||||
|
widgetState.visible =
|
||||||
|
lgCanvas.isNodeVisible(node) &&
|
||||||
|
!(widget.options.hideOnZoom && lowQuality) &&
|
||||||
|
widget.isVisible()
|
||||||
|
|
||||||
|
widgetState.pos = [node.pos[0] + MARGIN, node.pos[1] + MARGIN + widget.y]
|
||||||
|
widgetState.size = [
|
||||||
|
(widget.width ?? node.width) - MARGIN * 2,
|
||||||
|
(widget.computedHeight ?? 50) - MARGIN * 2
|
||||||
|
]
|
||||||
|
// TODO: optimize this logic as it's O(n), where n is the number of nodes
|
||||||
|
widgetState.zIndex = lgCanvas.graph.nodes.indexOf(node)
|
||||||
|
widgetState.readonly = lgCanvas.read_only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
watch(
|
||||||
|
() => canvasStore.canvas,
|
||||||
|
(lgCanvas) => {
|
||||||
|
if (!lgCanvas) return
|
||||||
|
|
||||||
|
lgCanvas.onDrawForeground = useChainCallback(
|
||||||
|
lgCanvas.onDrawForeground,
|
||||||
|
() => {
|
||||||
|
updateWidgets()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,102 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="widgetElement" />
|
<div
|
||||||
|
class="dom-widget"
|
||||||
|
ref="widgetElement"
|
||||||
|
:style="style"
|
||||||
|
v-show="widgetState.visible"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||||
|
import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||||
|
import { useDomClipping } from '@/composables/element/useDomClipping'
|
||||||
import type { DOMWidget } from '@/scripts/domWidget'
|
import type { DOMWidget } from '@/scripts/domWidget'
|
||||||
|
import { DomWidgetState } from '@/stores/domWidgetStore'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
const { widget } = defineProps<{
|
const { widget, widgetState } = defineProps<{
|
||||||
widget: DOMWidget<HTMLElement, any>
|
widget: DOMWidget<HTMLElement, any>
|
||||||
|
widgetState: DomWidgetState
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const widgetElement = ref<HTMLElement>()
|
const widgetElement = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const { style: positionStyle, updatePositionWithTransform } =
|
||||||
|
useAbsolutePosition()
|
||||||
|
const { style: clippingStyle, updateClipPath } = useDomClipping()
|
||||||
|
const style = computed<CSSProperties>(() => ({
|
||||||
|
...positionStyle.value,
|
||||||
|
...(enableDomClipping.value ? clippingStyle.value : {}),
|
||||||
|
zIndex: widgetState.zIndex,
|
||||||
|
pointerEvents: widgetState.readonly ? 'none' : 'auto'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const enableDomClipping = computed(() =>
|
||||||
|
settingStore.get('Comfy.DOMClippingEnabled')
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateDomClipping = () => {
|
||||||
|
const lgCanvas = canvasStore.canvas
|
||||||
|
const selectedNode = Object.values(
|
||||||
|
lgCanvas.selected_nodes ?? {}
|
||||||
|
)[0] as LGraphNode
|
||||||
|
const node = widget.node
|
||||||
|
const isSelected = selectedNode === node
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => widgetState,
|
||||||
|
(newState) => {
|
||||||
|
updatePositionWithTransform(newState)
|
||||||
|
if (enableDomClipping.value) {
|
||||||
|
updateDomClipping()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => widgetState.visible,
|
||||||
|
(newVisible, oldVisible) => {
|
||||||
|
if (!newVisible && oldVisible) {
|
||||||
|
widget.options.onHide?.(widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
widgetElement.value.appendChild(widget.element)
|
widgetElement.value.appendChild(widget.element)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dom-widget > * {
|
||||||
|
@apply h-full w-full;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ export function useAbsolutePosition() {
|
|||||||
height: '0px'
|
height: '0px'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the position of the element on the litegraph canvas.
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
* @param extraStyle
|
||||||
|
*/
|
||||||
const updatePosition = (
|
const updatePosition = (
|
||||||
config: PositionConfig,
|
config: PositionConfig,
|
||||||
extraStyle?: CSSProperties
|
extraStyle?: CSSProperties
|
||||||
@@ -41,8 +47,36 @@ export function useAbsolutePosition() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the position and size of the element on the litegraph canvas,
|
||||||
|
* with CSS transform scaling applied.
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
* @param extraStyle
|
||||||
|
*/
|
||||||
|
const updatePositionWithTransform = (
|
||||||
|
config: PositionConfig,
|
||||||
|
extraStyle?: CSSProperties
|
||||||
|
) => {
|
||||||
|
const { pos, size, scale = canvasStore.canvas?.ds?.scale ?? 1 } = config
|
||||||
|
const [left, top] = app.canvasPosToClientPos(pos)
|
||||||
|
const [width, height] = size
|
||||||
|
|
||||||
|
style.value = {
|
||||||
|
...style.value,
|
||||||
|
transformOrigin: '0 0',
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${top}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
...extraStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
style,
|
style,
|
||||||
updatePosition
|
updatePosition,
|
||||||
|
updatePositionWithTransform
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/composables/element/useDomClipping.ts
Normal file
123
src/composables/element/useDomClipping.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { CSSProperties, ref } from 'vue'
|
||||||
|
|
||||||
|
interface Rect {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the intersection between two rectangles
|
||||||
|
*/
|
||||||
|
function intersect(a: Rect, b: Rect): [number, number, number, number] | null {
|
||||||
|
const x1 = Math.max(a.x, b.x)
|
||||||
|
const y1 = Math.max(a.y, b.y)
|
||||||
|
const x2 = Math.min(a.x + a.width, b.x + b.width)
|
||||||
|
const y2 = Math.min(a.y + a.height, b.y + b.height)
|
||||||
|
|
||||||
|
if (x1 >= x2 || y1 >= y2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [x1, y1, x2 - x1, y2 - y1]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClippingOptions {
|
||||||
|
margin?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDomClipping = (options: ClippingOptions = {}) => {
|
||||||
|
const style = ref<CSSProperties>({})
|
||||||
|
const { margin = 4 } = options
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a clip path for an element based on its intersection with a selected area
|
||||||
|
*/
|
||||||
|
const calculateClipPath = (
|
||||||
|
elementRect: DOMRect,
|
||||||
|
canvasRect: DOMRect,
|
||||||
|
isSelected: boolean,
|
||||||
|
selectedArea?: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
scale: number
|
||||||
|
offset: [number, number]
|
||||||
|
}
|
||||||
|
): string => {
|
||||||
|
if (!isSelected && selectedArea) {
|
||||||
|
const { scale, offset } = selectedArea
|
||||||
|
|
||||||
|
// Get intersection in browser space
|
||||||
|
const intersection = intersect(
|
||||||
|
{
|
||||||
|
x: elementRect.left - canvasRect.left,
|
||||||
|
y: elementRect.top - canvasRect.top,
|
||||||
|
width: elementRect.width,
|
||||||
|
height: elementRect.height
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: (selectedArea.x + offset[0] - margin) * scale,
|
||||||
|
y: (selectedArea.y + offset[1] - margin) * scale,
|
||||||
|
width: (selectedArea.width + 2 * margin) * scale,
|
||||||
|
height: (selectedArea.height + 2 * margin) * scale
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!intersection) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert intersection to canvas scale (element has scale transform)
|
||||||
|
const clipX =
|
||||||
|
(intersection[0] - elementRect.left + canvasRect.left) / scale + 'px'
|
||||||
|
const clipY =
|
||||||
|
(intersection[1] - elementRect.top + canvasRect.top) / scale + 'px'
|
||||||
|
const clipWidth = intersection[2] / scale + 'px'
|
||||||
|
const clipHeight = intersection[3] / scale + 'px'
|
||||||
|
|
||||||
|
return `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the clip-path style based on element and selection information
|
||||||
|
*/
|
||||||
|
const updateClipPath = (
|
||||||
|
element: HTMLElement,
|
||||||
|
canvasElement: HTMLCanvasElement,
|
||||||
|
isSelected: boolean,
|
||||||
|
selectedArea?: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
scale: number
|
||||||
|
offset: [number, number]
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const elementRect = element.getBoundingClientRect()
|
||||||
|
const canvasRect = canvasElement.getBoundingClientRect()
|
||||||
|
|
||||||
|
const clipPath = calculateClipPath(
|
||||||
|
elementRect,
|
||||||
|
canvasRect,
|
||||||
|
isSelected,
|
||||||
|
selectedArea
|
||||||
|
)
|
||||||
|
|
||||||
|
style.value = {
|
||||||
|
clipPath: clipPath || 'none',
|
||||||
|
willChange: 'clip-path'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
style,
|
||||||
|
updateClipPath
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,15 @@
|
|||||||
import { LGraphCanvas, LGraphNode } from '@comfyorg/litegraph'
|
import { LGraphNode } from '@comfyorg/litegraph'
|
||||||
import type { Vector4 } from '@comfyorg/litegraph'
|
|
||||||
import type { ISerialisedNode } from '@comfyorg/litegraph/dist/types/serialisation'
|
|
||||||
import type {
|
import type {
|
||||||
ICustomWidget,
|
ICustomWidget,
|
||||||
IWidgetOptions
|
IWidgetOptions
|
||||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
|
||||||
import { generateRandomSuffix } from '@/utils/formatUtil'
|
import { generateRandomSuffix } from '@/utils/formatUtil'
|
||||||
|
|
||||||
interface Rect {
|
|
||||||
height: number
|
|
||||||
width: number
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||||
extends ICustomWidget<T> {
|
extends ICustomWidget<T> {
|
||||||
// ICustomWidget properties
|
// ICustomWidget properties
|
||||||
@@ -36,6 +27,10 @@ export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
|||||||
// DOMWidget properties
|
// DOMWidget properties
|
||||||
/** The unique ID of the widget. */
|
/** The unique ID of the widget. */
|
||||||
id: string
|
id: string
|
||||||
|
/** The node that the widget belongs to. */
|
||||||
|
node: LGraphNode
|
||||||
|
/** Whether the widget is visible. */
|
||||||
|
isVisible(): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DOMWidgetOptions<
|
export interface DOMWidgetOptions<
|
||||||
@@ -62,92 +57,6 @@ export interface DOMWidgetOptions<
|
|||||||
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function intersect(a: Rect, b: Rect): Vector4 | null {
|
|
||||||
const x = Math.max(a.x, b.x)
|
|
||||||
const num1 = Math.min(a.x + a.width, b.x + b.width)
|
|
||||||
const y = Math.max(a.y, b.y)
|
|
||||||
const num2 = Math.min(a.y + a.height, b.y + b.height)
|
|
||||||
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]
|
|
||||||
else return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getClipPath(
|
|
||||||
node: LGraphNode,
|
|
||||||
element: HTMLElement,
|
|
||||||
canvasRect: DOMRect
|
|
||||||
): string {
|
|
||||||
const selectedNode: LGraphNode = Object.values(
|
|
||||||
app.canvas.selected_nodes ?? {}
|
|
||||||
)[0] as LGraphNode
|
|
||||||
if (selectedNode && selectedNode !== node) {
|
|
||||||
const elRect = element.getBoundingClientRect()
|
|
||||||
const MARGIN = 4
|
|
||||||
const { offset, scale } = app.canvas.ds
|
|
||||||
const { renderArea } = selectedNode
|
|
||||||
|
|
||||||
// Get intersection in browser space
|
|
||||||
const intersection = intersect(
|
|
||||||
{
|
|
||||||
x: elRect.left - canvasRect.left,
|
|
||||||
y: elRect.top - canvasRect.top,
|
|
||||||
width: elRect.width,
|
|
||||||
height: elRect.height
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: (renderArea[0] + offset[0] - MARGIN) * scale,
|
|
||||||
y: (renderArea[1] + offset[1] - MARGIN) * scale,
|
|
||||||
width: (renderArea[2] + 2 * MARGIN) * scale,
|
|
||||||
height: (renderArea[3] + 2 * MARGIN) * scale
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!intersection) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert intersection to canvas scale (element has scale transform)
|
|
||||||
const clipX =
|
|
||||||
(intersection[0] - elRect.left + canvasRect.left) / scale + 'px'
|
|
||||||
const clipY = (intersection[1] - elRect.top + canvasRect.top) / scale + 'px'
|
|
||||||
const clipWidth = intersection[2] / scale + 'px'
|
|
||||||
const clipHeight = intersection[3] / scale + 'px'
|
|
||||||
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
|
||||||
const elementWidgets = new Set<LGraphNode>()
|
|
||||||
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes
|
|
||||||
LGraphCanvas.prototype.computeVisibleNodes = function (
|
|
||||||
nodes?: LGraphNode[],
|
|
||||||
out?: LGraphNode[]
|
|
||||||
): LGraphNode[] {
|
|
||||||
const visibleNodes = computeVisibleNodes.call(this, nodes, out)
|
|
||||||
|
|
||||||
for (const node of app.graph.nodes) {
|
|
||||||
if (elementWidgets.has(node)) {
|
|
||||||
const hidden = visibleNodes.indexOf(node) === -1
|
|
||||||
for (const w of node.widgets ?? []) {
|
|
||||||
if (w.element) {
|
|
||||||
w.element.dataset.isInVisibleNodes = hidden ? 'false' : 'true'
|
|
||||||
const shouldOtherwiseHide = w.element.dataset.shouldHide === 'true'
|
|
||||||
const wasHidden = w.element.hidden
|
|
||||||
const actualHidden = hidden || shouldOtherwiseHide || node.collapsed
|
|
||||||
w.element.hidden = actualHidden
|
|
||||||
w.element.style.display = actualHidden ? 'none' : ''
|
|
||||||
if (actualHidden && !wasHidden) {
|
|
||||||
w.options.onHide?.(w as DOMWidget<HTMLElement, object>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||||
implements DOMWidget<T, V>
|
implements DOMWidget<T, V>
|
||||||
{
|
{
|
||||||
@@ -159,10 +68,12 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
callback?: (value: V) => void
|
callback?: (value: V) => void
|
||||||
|
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
readonly node: LGraphNode
|
||||||
mouseDownHandler?: (event: MouseEvent) => void
|
mouseDownHandler?: (event: MouseEvent) => void
|
||||||
|
|
||||||
constructor(obj: {
|
constructor(obj: {
|
||||||
id: string
|
id: string
|
||||||
|
node: LGraphNode
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
element: T
|
element: T
|
||||||
@@ -175,6 +86,7 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
this.options = obj.options
|
this.options = obj.options
|
||||||
|
|
||||||
this.id = obj.id
|
this.id = obj.id
|
||||||
|
this.node = obj.node
|
||||||
|
|
||||||
if (this.element.blur) {
|
if (this.element.blur) {
|
||||||
this.mouseDownHandler = (event) => {
|
this.mouseDownHandler = (event) => {
|
||||||
@@ -238,59 +150,16 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(
|
isVisible(): boolean {
|
||||||
ctx: CanvasRenderingContext2D,
|
return (
|
||||||
node: LGraphNode,
|
!_.isNil(this.computedHeight) &&
|
||||||
widgetWidth: number,
|
this.computedHeight > 0 &&
|
||||||
y: number
|
!['converted-widget', 'hidden'].includes(this.type) &&
|
||||||
): void {
|
!this.node.collapsed
|
||||||
const { offset, scale } = app.canvas.ds
|
)
|
||||||
const hidden =
|
}
|
||||||
(!!this.options.hideOnZoom && app.canvas.low_quality) ||
|
|
||||||
(this.computedHeight ?? 0) <= 0 ||
|
|
||||||
// @ts-expect-error custom widget type
|
|
||||||
this.type === 'converted-widget' ||
|
|
||||||
// @ts-expect-error custom widget type
|
|
||||||
this.type === 'hidden' ||
|
|
||||||
node.collapsed
|
|
||||||
|
|
||||||
this.element.dataset.shouldHide = hidden ? 'true' : 'false'
|
|
||||||
const isInVisibleNodes = this.element.dataset.isInVisibleNodes === 'true'
|
|
||||||
const actualHidden = hidden || !isInVisibleNodes
|
|
||||||
const wasHidden = this.element.hidden
|
|
||||||
this.element.hidden = actualHidden
|
|
||||||
this.element.style.display = actualHidden ? 'none' : ''
|
|
||||||
|
|
||||||
if (actualHidden && !wasHidden) {
|
|
||||||
this.options.onHide?.(this)
|
|
||||||
}
|
|
||||||
if (actualHidden) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const elRect = ctx.canvas.getBoundingClientRect()
|
|
||||||
const margin = 10
|
|
||||||
const top = node.pos[0] + offset[0] + margin
|
|
||||||
const left = node.pos[1] + offset[1] + margin + y
|
|
||||||
|
|
||||||
Object.assign(this.element.style, {
|
|
||||||
transformOrigin: '0 0',
|
|
||||||
transform: `scale(${scale})`,
|
|
||||||
left: `${top * scale}px`,
|
|
||||||
top: `${left * scale}px`,
|
|
||||||
width: `${widgetWidth - margin * 2}px`,
|
|
||||||
height: `${(this.computedHeight ?? 50) - margin * 2}px`,
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: app.graph.nodes.indexOf(node),
|
|
||||||
pointerEvents: app.canvas.read_only ? 'none' : 'auto'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (useSettingStore().get('Comfy.DOMClippingEnabled')) {
|
|
||||||
const clipPath = getClipPath(node, this.element, elRect)
|
|
||||||
this.element.style.clipPath = clipPath ?? 'none'
|
|
||||||
this.element.style.willChange = 'clip-path'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
draw(): void {
|
||||||
this.options.onDraw?.(this)
|
this.options.onDraw?.(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,18 +184,15 @@ LGraphNode.prototype.addDOMWidget = function <
|
|||||||
): DOMWidget<T, V> {
|
): DOMWidget<T, V> {
|
||||||
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
||||||
|
|
||||||
element.hidden = true
|
|
||||||
element.style.display = 'none'
|
|
||||||
|
|
||||||
const { nodeData } = this.constructor
|
const { nodeData } = this.constructor
|
||||||
const tooltip = (nodeData?.input.required?.[name] ??
|
const tooltip = nodeData?.inputs?.[name]?.tooltip
|
||||||
nodeData?.input.optional?.[name])?.[1]?.tooltip
|
|
||||||
if (tooltip && !element.title) {
|
if (tooltip && !element.title) {
|
||||||
element.title = tooltip
|
element.title = tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = new DOMWidgetImpl({
|
const widget = new DOMWidgetImpl({
|
||||||
id: `${this.id}:${name}:${generateRandomSuffix()}`,
|
id: `${this.id}:${name}:${generateRandomSuffix()}`,
|
||||||
|
node: this,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
element,
|
element,
|
||||||
@@ -356,14 +222,10 @@ LGraphNode.prototype.addDOMWidget = function <
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.addCustomWidget(widget)
|
this.addCustomWidget(widget)
|
||||||
elementWidgets.add(this)
|
|
||||||
|
|
||||||
const onRemoved = this.onRemoved
|
this.onRemoved = useChainCallback(this.onRemoved, () => {
|
||||||
this.onRemoved = function (this: LGraphNode) {
|
|
||||||
widget.onRemove()
|
widget.onRemove()
|
||||||
elementWidgets.delete(this)
|
})
|
||||||
onRemoved?.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onResize = useChainCallback(this.onResize, () => {
|
this.onResize = useChainCallback(this.onResize, () => {
|
||||||
options.beforeResize?.call(widget, this)
|
options.beforeResize?.call(widget, this)
|
||||||
|
|||||||
@@ -4,9 +4,18 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { markRaw, ref } from 'vue'
|
import { markRaw, ref } from 'vue'
|
||||||
|
|
||||||
|
import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
||||||
import type { DOMWidget } from '@/scripts/domWidget'
|
import type { DOMWidget } from '@/scripts/domWidget'
|
||||||
|
|
||||||
|
export interface DomWidgetState extends PositionConfig {
|
||||||
|
visible: boolean
|
||||||
|
readonly: boolean
|
||||||
|
zIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
export const useDomWidgetStore = defineStore('domWidget', () => {
|
export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||||
|
const widgetStates = ref<Map<string, DomWidgetState>>(new Map())
|
||||||
|
|
||||||
// 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(
|
||||||
@@ -21,14 +30,23 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
|||||||
widget.id,
|
widget.id,
|
||||||
markRaw(widget as unknown as DOMWidget<HTMLElement, object | string>)
|
markRaw(widget as unknown as DOMWidget<HTMLElement, object | string>)
|
||||||
)
|
)
|
||||||
|
widgetStates.value.set(widget.id, {
|
||||||
|
visible: true,
|
||||||
|
readonly: false,
|
||||||
|
zIndex: 0,
|
||||||
|
pos: [0, 0],
|
||||||
|
size: [0, 0]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister a widget from the store
|
// Unregister a widget from the store
|
||||||
const unregisterWidget = (widgetId: string) => {
|
const unregisterWidget = (widgetId: string) => {
|
||||||
widgetInstances.value.delete(widgetId)
|
widgetInstances.value.delete(widgetId)
|
||||||
|
widgetStates.value.delete(widgetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
widgetStates,
|
||||||
widgetInstances,
|
widgetInstances,
|
||||||
registerWidget,
|
registerWidget,
|
||||||
unregisterWidget
|
unregisterWidget
|
||||||
|
|||||||
Reference in New Issue
Block a user