mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Add dynamic tags widget selection based on ComfyUI version
- Introduced a mechanism to dynamically import either the legacy or modern tags widget based on the ComfyUI frontend version. - Updated the `addTagsWidget` function in both `tags_widget.js` and `legacy_tags_widget.js` to enhance tag rendering and widget height management. - Improved styling and layout for tags, ensuring better alignment and responsiveness. - Added a new serialization method to handle potential issues with ComfyUI's serialization process. - Enhanced the overall user experience by providing a more modern and flexible tags widget implementation.
This commit is contained in:
144
web/comfyui/DomWidget.vue
Normal file
144
web/comfyui/DomWidget.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="dom-widget"
|
||||||
|
:title="tooltip"
|
||||||
|
ref="widgetElement"
|
||||||
|
:style="style"
|
||||||
|
v-show="widgetState.visible"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-if="isComponentWidget(widget)"
|
||||||
|
:is="widget.component"
|
||||||
|
:modelValue="widget.value"
|
||||||
|
@update:modelValue="emit('update:widgetValue', $event)"
|
||||||
|
:widget="widget"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useEventListener } from '@vueuse/core'
|
||||||
|
import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||||
|
import { useDomClipping } from '@/composables/element/useDomClipping'
|
||||||
|
import {
|
||||||
|
type BaseDOMWidget,
|
||||||
|
isComponentWidget,
|
||||||
|
isDOMWidget
|
||||||
|
} from '@/scripts/domWidget'
|
||||||
|
import { DomWidgetState } from '@/stores/domWidgetStore'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
|
const { widget, widgetState } = defineProps<{
|
||||||
|
widget: BaseDOMWidget<string | object>
|
||||||
|
widgetState: DomWidgetState
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:widgetValue', value: string | object): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const widgetElement = ref<HTMLElement | undefined>()
|
||||||
|
|
||||||
|
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
|
||||||
|
if (!lgCanvas || !widgetElement.value) return
|
||||||
|
|
||||||
|
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
|
||||||
|
if (!selectedNode) return
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isDOMWidget(widget)) {
|
||||||
|
if (widget.element.blur) {
|
||||||
|
useEventListener(document, 'mousedown', (event) => {
|
||||||
|
if (!widget.element.contains(event.target as HTMLElement)) {
|
||||||
|
widget.element.blur()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
|
||||||
|
useEventListener(widget.element, evt, () => {
|
||||||
|
const lgCanvas = canvasStore.canvas
|
||||||
|
lgCanvas?.selectNode(widget.node)
|
||||||
|
lgCanvas?.bringToFront(widget.node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputSpec = widget.node.constructor.nodeData
|
||||||
|
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isDOMWidget(widget) && widgetElement.value) {
|
||||||
|
widgetElement.value.appendChild(widget.element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dom-widget > * {
|
||||||
|
@apply h-full w-full;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,193 +1,121 @@
|
|||||||
import { LGraphCanvas, LGraphNode } from '@comfyorg/litegraph'
|
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||||
import type { Size, Vector4 } from '@comfyorg/litegraph'
|
|
||||||
import type { ISerialisedNode } from '@comfyorg/litegraph/dist/types/serialisation'
|
|
||||||
import type {
|
import type {
|
||||||
ICustomWidget,
|
ICustomWidget,
|
||||||
|
IWidget,
|
||||||
IWidgetOptions
|
IWidgetOptions
|
||||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { type Component, toRaw } from 'vue'
|
||||||
|
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
|
import { generateUUID } from '@/utils/formatUtil'
|
||||||
|
|
||||||
import { app } from './app'
|
export interface BaseDOMWidget<V extends object | string>
|
||||||
|
extends ICustomWidget {
|
||||||
|
// ICustomWidget properties
|
||||||
|
type: 'custom'
|
||||||
|
options: DOMWidgetOptions<V>
|
||||||
|
value: V
|
||||||
|
callback?: (value: V) => void
|
||||||
|
|
||||||
const SIZE = Symbol()
|
// BaseDOMWidget properties
|
||||||
|
/** The unique ID of the widget. */
|
||||||
interface Rect {
|
readonly id: string
|
||||||
height: number
|
/** The node that the widget belongs to. */
|
||||||
width: number
|
readonly node: LGraphNode
|
||||||
x: number
|
/** Whether the widget is visible. */
|
||||||
y: number
|
isVisible(): boolean
|
||||||
|
/** The margin of the widget. */
|
||||||
|
margin: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DOM widget that wraps a custom HTML element as a litegraph widget.
|
||||||
|
*/
|
||||||
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||||
extends ICustomWidget<T> {
|
extends BaseDOMWidget<V> {
|
||||||
// All unrecognized types will be treated the same way as 'custom' in litegraph internally.
|
|
||||||
type: 'custom'
|
|
||||||
name: string
|
|
||||||
element: T
|
element: T
|
||||||
options: DOMWidgetOptions<T, V>
|
|
||||||
value: V
|
|
||||||
y?: number
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Legacy property used by some extensions for customtext
|
* @deprecated Legacy property used by some extensions for customtext
|
||||||
* (textarea) widgets. Use `element` instead as it provides the same
|
* (textarea) widgets. Use {@link element} instead as it provides the same
|
||||||
* functionality and works for all DOMWidget types.
|
* functionality and works for all DOMWidget types.
|
||||||
*/
|
*/
|
||||||
inputEl?: T
|
inputEl?: T
|
||||||
callback?: (value: V) => void
|
|
||||||
/**
|
|
||||||
* Draw the widget on the canvas.
|
|
||||||
*/
|
|
||||||
draw?: (
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
node: LGraphNode,
|
|
||||||
widgetWidth: number,
|
|
||||||
y: number,
|
|
||||||
widgetHeight: number
|
|
||||||
) => void
|
|
||||||
/**
|
|
||||||
* TODO(huchenlei): Investigate when is this callback fired. `onRemove` is
|
|
||||||
* on litegraph's IBaseWidget definition, but not called in litegraph.
|
|
||||||
* Currently only called in widgetInputs.ts.
|
|
||||||
*/
|
|
||||||
onRemove?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DOMWidgetOptions<
|
/**
|
||||||
T extends HTMLElement,
|
* A DOM widget that wraps a Vue component as a litegraph widget.
|
||||||
V extends object | string
|
*/
|
||||||
> extends IWidgetOptions {
|
export interface ComponentWidget<V extends object | string>
|
||||||
|
extends BaseDOMWidget<V> {
|
||||||
|
readonly component: Component
|
||||||
|
readonly inputSpec: InputSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DOMWidgetOptions<V extends object | string>
|
||||||
|
extends IWidgetOptions {
|
||||||
|
/**
|
||||||
|
* Whether to render a placeholder rectangle when zoomed out.
|
||||||
|
*/
|
||||||
hideOnZoom?: boolean
|
hideOnZoom?: boolean
|
||||||
selectOn?: string[]
|
selectOn?: string[]
|
||||||
onHide?: (widget: DOMWidget<T, V>) => void
|
onHide?: (widget: BaseDOMWidget<V>) => void
|
||||||
getValue?: () => V
|
getValue?: () => V
|
||||||
setValue?: (value: V) => void
|
setValue?: (value: V) => void
|
||||||
getMinHeight?: () => number
|
getMinHeight?: () => number
|
||||||
getMaxHeight?: () => number
|
getMaxHeight?: () => number
|
||||||
getHeight?: () => string | number
|
getHeight?: () => string | number
|
||||||
onDraw?: (widget: DOMWidget<T, V>) => void
|
onDraw?: (widget: BaseDOMWidget<V>) => void
|
||||||
beforeResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
margin?: number
|
||||||
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
/**
|
||||||
|
* @deprecated Use `afterResize` instead. This callback is a legacy API
|
||||||
|
* that fires before resize happens, but it is no longer supported. Now it
|
||||||
|
* fires after resize happens.
|
||||||
|
* The resize logic has been upstreamed to litegraph in
|
||||||
|
* https://github.com/Comfy-Org/ComfyUI_frontend/pull/2557
|
||||||
|
*/
|
||||||
|
beforeResize?: (this: BaseDOMWidget<V>, node: LGraphNode) => void
|
||||||
|
afterResize?: (this: BaseDOMWidget<V>, node: LGraphNode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function intersect(a: Rect, b: Rect): Vector4 | null {
|
export const isDOMWidget = <T extends HTMLElement, V extends object | string>(
|
||||||
const x = Math.max(a.x, b.x)
|
widget: IWidget
|
||||||
const num1 = Math.min(a.x + a.width, b.x + b.width)
|
): widget is DOMWidget<T, V> => 'element' in widget && !!widget.element
|
||||||
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(
|
export const isComponentWidget = <V extends object | string>(
|
||||||
node: LGraphNode,
|
widget: IWidget
|
||||||
element: HTMLElement,
|
): widget is ComponentWidget<V> => 'component' in widget && !!widget.component
|
||||||
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
|
abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||||
const intersection = intersect(
|
implements BaseDOMWidget<V>
|
||||||
{
|
|
||||||
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 isCollapsed = w.element.dataset.collapsed === 'true'
|
|
||||||
const wasHidden = w.element.hidden
|
|
||||||
const actualHidden = hidden || shouldOtherwiseHide || isCollapsed
|
|
||||||
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>
|
|
||||||
implements DOMWidget<T, V>
|
|
||||||
{
|
{
|
||||||
type: 'custom'
|
static readonly DEFAULT_MARGIN = 10
|
||||||
name: string
|
readonly type: 'custom'
|
||||||
element: T
|
readonly name: string
|
||||||
options: DOMWidgetOptions<T, V>
|
readonly options: DOMWidgetOptions<V>
|
||||||
computedHeight?: number
|
computedHeight?: number
|
||||||
|
y: number = 0
|
||||||
callback?: (value: V) => void
|
callback?: (value: V) => void
|
||||||
private mouseDownHandler?: (event: MouseEvent) => void
|
|
||||||
|
|
||||||
constructor(
|
readonly id: string
|
||||||
name: string,
|
readonly node: LGraphNode
|
||||||
type: string,
|
|
||||||
element: T,
|
constructor(obj: {
|
||||||
options: DOMWidgetOptions<T, V> = {}
|
id: string
|
||||||
) {
|
node: LGraphNode
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
options: DOMWidgetOptions<V>
|
||||||
|
}) {
|
||||||
// @ts-expect-error custom widget type
|
// @ts-expect-error custom widget type
|
||||||
this.type = type
|
this.type = obj.type
|
||||||
this.name = name
|
this.name = obj.name
|
||||||
this.element = element
|
this.options = obj.options
|
||||||
this.options = options
|
|
||||||
|
|
||||||
if (element.blur) {
|
this.id = obj.id
|
||||||
this.mouseDownHandler = (event) => {
|
this.node = obj.node
|
||||||
if (!element.contains(event.target as HTMLElement)) {
|
|
||||||
element.blur()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', this.mouseDownHandler)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get value(): V {
|
get value(): V {
|
||||||
@@ -199,6 +127,67 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
this.callback?.(this.value)
|
this.callback?.(this.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get margin(): number {
|
||||||
|
return this.options.margin ?? BaseDOMWidgetImpl.DEFAULT_MARGIN
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible(): boolean {
|
||||||
|
return (
|
||||||
|
!_.isNil(this.computedHeight) &&
|
||||||
|
this.computedHeight > 0 &&
|
||||||
|
!['converted-widget', 'hidden'].includes(this.type) &&
|
||||||
|
!this.node.collapsed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
_node: LGraphNode,
|
||||||
|
widget_width: number,
|
||||||
|
y: number,
|
||||||
|
widget_height: number,
|
||||||
|
lowQuality?: boolean
|
||||||
|
): void {
|
||||||
|
if (this.options.hideOnZoom && lowQuality && this.isVisible()) {
|
||||||
|
// Draw a placeholder rectangle
|
||||||
|
const originalFillStyle = ctx.fillStyle
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR
|
||||||
|
ctx.rect(
|
||||||
|
this.margin,
|
||||||
|
y + this.margin,
|
||||||
|
widget_width - this.margin * 2,
|
||||||
|
(this.computedHeight ?? widget_height) - 2 * this.margin
|
||||||
|
)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.fillStyle = originalFillStyle
|
||||||
|
}
|
||||||
|
this.options.onDraw?.(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(): void {
|
||||||
|
useDomWidgetStore().unregisterWidget(this.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||||
|
extends BaseDOMWidgetImpl<V>
|
||||||
|
implements DOMWidget<T, V>
|
||||||
|
{
|
||||||
|
readonly element: T
|
||||||
|
|
||||||
|
constructor(obj: {
|
||||||
|
id: string
|
||||||
|
node: LGraphNode
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
element: T
|
||||||
|
options: DOMWidgetOptions<V>
|
||||||
|
}) {
|
||||||
|
super(obj)
|
||||||
|
this.element = obj.element
|
||||||
|
}
|
||||||
|
|
||||||
/** Extract DOM widget size info */
|
/** Extract DOM widget size info */
|
||||||
computeLayoutSize(node: LGraphNode) {
|
computeLayoutSize(node: LGraphNode) {
|
||||||
// @ts-expect-error custom widget type
|
// @ts-expect-error custom widget type
|
||||||
@@ -241,69 +230,61 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
|||||||
minWidth: 0
|
minWidth: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
draw(
|
export class ComponentWidgetImpl<V extends object | string>
|
||||||
ctx: CanvasRenderingContext2D,
|
extends BaseDOMWidgetImpl<V>
|
||||||
node: LGraphNode,
|
implements ComponentWidget<V>
|
||||||
widgetWidth: number,
|
{
|
||||||
y: number
|
readonly component: Component
|
||||||
): void {
|
readonly inputSpec: InputSpec
|
||||||
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'
|
|
||||||
|
|
||||||
this.element.dataset.shouldHide = hidden ? 'true' : 'false'
|
constructor(obj: {
|
||||||
const isInVisibleNodes = this.element.dataset.isInVisibleNodes === 'true'
|
id: string
|
||||||
const isCollapsed = this.element.dataset.collapsed === 'true'
|
node: LGraphNode
|
||||||
const actualHidden = hidden || !isInVisibleNodes || isCollapsed
|
name: string
|
||||||
const wasHidden = this.element.hidden
|
component: Component
|
||||||
this.element.hidden = actualHidden
|
inputSpec: InputSpec
|
||||||
this.element.style.display = actualHidden ? 'none' : ''
|
options: DOMWidgetOptions<V>
|
||||||
|
}) {
|
||||||
if (actualHidden && !wasHidden) {
|
super({
|
||||||
this.options.onHide?.(this)
|
...obj,
|
||||||
}
|
type: 'custom'
|
||||||
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'
|
|
||||||
})
|
})
|
||||||
|
this.component = obj.component
|
||||||
if (useSettingStore().get('Comfy.DOMClippingEnabled')) {
|
this.inputSpec = obj.inputSpec
|
||||||
const clipPath = getClipPath(node, this.element, elRect)
|
|
||||||
this.element.style.clipPath = clipPath ?? 'none'
|
|
||||||
this.element.style.willChange = 'clip-path'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.onDraw?.(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(): void {
|
computeLayoutSize() {
|
||||||
if (this.mouseDownHandler) {
|
const minHeight = this.options.getMinHeight?.() ?? 50
|
||||||
document.removeEventListener('mousedown', this.mouseDownHandler)
|
const maxHeight = this.options.getMaxHeight?.()
|
||||||
|
return {
|
||||||
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
|
minWidth: 0
|
||||||
}
|
}
|
||||||
this.element.remove()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serializeValue(): V {
|
||||||
|
return toRaw(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addWidget = <W extends BaseDOMWidget<object | string>>(
|
||||||
|
node: LGraphNode,
|
||||||
|
widget: W
|
||||||
|
) => {
|
||||||
|
node.addCustomWidget(widget)
|
||||||
|
node.onRemoved = useChainCallback(node.onRemoved, () => {
|
||||||
|
widget.onRemove?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
node.onResize = useChainCallback(node.onResize, () => {
|
||||||
|
widget.options.beforeResize?.call(widget, node)
|
||||||
|
widget.options.afterResize?.call(widget, node)
|
||||||
|
})
|
||||||
|
|
||||||
|
useDomWidgetStore().registerWidget(widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
LGraphNode.prototype.addDOMWidget = function <
|
LGraphNode.prototype.addDOMWidget = function <
|
||||||
@@ -314,24 +295,19 @@ LGraphNode.prototype.addDOMWidget = function <
|
|||||||
name: string,
|
name: string,
|
||||||
type: string,
|
type: string,
|
||||||
element: T,
|
element: T,
|
||||||
options: DOMWidgetOptions<T, V> = {}
|
options: DOMWidgetOptions<V> = {}
|
||||||
): DOMWidget<T, V> {
|
): DOMWidget<T, V> {
|
||||||
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
const widget = new DOMWidgetImpl({
|
||||||
|
id: generateUUID(),
|
||||||
|
node: this,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
element,
|
||||||
|
options: { hideOnZoom: true, ...options }
|
||||||
|
})
|
||||||
|
// Note: Before `LGraphNode.configure` is called, `this.id` is always `-1`.
|
||||||
|
addWidget(this, widget as unknown as BaseDOMWidget<object | string>)
|
||||||
|
|
||||||
if (!element.parentElement) {
|
|
||||||
app.canvasContainer.append(element)
|
|
||||||
}
|
|
||||||
element.hidden = true
|
|
||||||
element.style.display = 'none'
|
|
||||||
|
|
||||||
const { nodeData } = this.constructor
|
|
||||||
const tooltip = (nodeData?.input.required?.[name] ??
|
|
||||||
nodeData?.input.optional?.[name])?.[1]?.tooltip
|
|
||||||
if (tooltip && !element.title) {
|
|
||||||
element.title = tooltip
|
|
||||||
}
|
|
||||||
|
|
||||||
const widget = new DOMWidgetImpl(name, type, element, options)
|
|
||||||
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
|
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
|
||||||
// Some custom nodes are explicitly expecting getter and setter of `value`
|
// Some custom nodes are explicitly expecting getter and setter of `value`
|
||||||
// property to be on instance instead of prototype.
|
// property to be on instance instead of prototype.
|
||||||
@@ -345,55 +321,5 @@ LGraphNode.prototype.addDOMWidget = function <
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ensure selectOn exists before iteration
|
|
||||||
const selectEvents = options.selectOn ?? ['focus', 'click']
|
|
||||||
for (const evt of selectEvents) {
|
|
||||||
element.addEventListener(evt, () => {
|
|
||||||
app.canvas.selectNode(this)
|
|
||||||
app.canvas.bringToFront(this)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addCustomWidget(widget)
|
|
||||||
elementWidgets.add(this)
|
|
||||||
|
|
||||||
const collapse = this.collapse
|
|
||||||
this.collapse = function (this: LGraphNode, force?: boolean) {
|
|
||||||
collapse.call(this, force)
|
|
||||||
if (this.collapsed) {
|
|
||||||
element.hidden = true
|
|
||||||
element.style.display = 'none'
|
|
||||||
}
|
|
||||||
element.dataset.collapsed = this.collapsed ? 'true' : 'false'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { onConfigure } = this
|
|
||||||
this.onConfigure = function (
|
|
||||||
this: LGraphNode,
|
|
||||||
serializedNode: ISerialisedNode
|
|
||||||
) {
|
|
||||||
onConfigure?.call(this, serializedNode)
|
|
||||||
element.dataset.collapsed = this.collapsed ? 'true' : 'false'
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRemoved = this.onRemoved
|
|
||||||
this.onRemoved = function (this: LGraphNode) {
|
|
||||||
element.remove()
|
|
||||||
elementWidgets.delete(this)
|
|
||||||
onRemoved?.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore index with symbol
|
|
||||||
if (!this[SIZE]) {
|
|
||||||
// @ts-ignore index with symbol
|
|
||||||
this[SIZE] = true
|
|
||||||
const onResize = this.onResize
|
|
||||||
this.onResize = function (this: LGraphNode, size: Size) {
|
|
||||||
options.beforeResize?.call(widget, this)
|
|
||||||
onResize?.call(this, size)
|
|
||||||
options.afterResize?.call(widget, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget
|
return widget
|
||||||
}
|
}
|
||||||
|
|||||||
399
web/comfyui/legacyDomWidget.ts
Normal file
399
web/comfyui/legacyDomWidget.ts
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
import { LGraphCanvas, LGraphNode } from '@comfyorg/litegraph'
|
||||||
|
import type { Size, Vector4 } from '@comfyorg/litegraph'
|
||||||
|
import type { ISerialisedNode } from '@comfyorg/litegraph/dist/types/serialisation'
|
||||||
|
import type {
|
||||||
|
ICustomWidget,
|
||||||
|
IWidgetOptions
|
||||||
|
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
|
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
|
import { app } from './app'
|
||||||
|
|
||||||
|
const SIZE = Symbol()
|
||||||
|
|
||||||
|
interface Rect {
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||||
|
extends ICustomWidget<T> {
|
||||||
|
// All unrecognized types will be treated the same way as 'custom' in litegraph internally.
|
||||||
|
type: 'custom'
|
||||||
|
name: string
|
||||||
|
element: T
|
||||||
|
options: DOMWidgetOptions<T, V>
|
||||||
|
value: V
|
||||||
|
y?: number
|
||||||
|
/**
|
||||||
|
* @deprecated Legacy property used by some extensions for customtext
|
||||||
|
* (textarea) widgets. Use `element` instead as it provides the same
|
||||||
|
* functionality and works for all DOMWidget types.
|
||||||
|
*/
|
||||||
|
inputEl?: T
|
||||||
|
callback?: (value: V) => void
|
||||||
|
/**
|
||||||
|
* Draw the widget on the canvas.
|
||||||
|
*/
|
||||||
|
draw?: (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
node: LGraphNode,
|
||||||
|
widgetWidth: number,
|
||||||
|
y: number,
|
||||||
|
widgetHeight: number
|
||||||
|
) => void
|
||||||
|
/**
|
||||||
|
* TODO(huchenlei): Investigate when is this callback fired. `onRemove` is
|
||||||
|
* on litegraph's IBaseWidget definition, but not called in litegraph.
|
||||||
|
* Currently only called in widgetInputs.ts.
|
||||||
|
*/
|
||||||
|
onRemove?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DOMWidgetOptions<
|
||||||
|
T extends HTMLElement,
|
||||||
|
V extends object | string
|
||||||
|
> extends IWidgetOptions {
|
||||||
|
hideOnZoom?: boolean
|
||||||
|
selectOn?: string[]
|
||||||
|
onHide?: (widget: DOMWidget<T, V>) => void
|
||||||
|
getValue?: () => V
|
||||||
|
setValue?: (value: V) => void
|
||||||
|
getMinHeight?: () => number
|
||||||
|
getMaxHeight?: () => number
|
||||||
|
getHeight?: () => string | number
|
||||||
|
onDraw?: (widget: DOMWidget<T, V>) => void
|
||||||
|
beforeResize?: (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 isCollapsed = w.element.dataset.collapsed === 'true'
|
||||||
|
const wasHidden = w.element.hidden
|
||||||
|
const actualHidden = hidden || shouldOtherwiseHide || isCollapsed
|
||||||
|
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>
|
||||||
|
implements DOMWidget<T, V>
|
||||||
|
{
|
||||||
|
type: 'custom'
|
||||||
|
name: string
|
||||||
|
element: T
|
||||||
|
options: DOMWidgetOptions<T, V>
|
||||||
|
computedHeight?: number
|
||||||
|
callback?: (value: V) => void
|
||||||
|
private mouseDownHandler?: (event: MouseEvent) => void
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
element: T,
|
||||||
|
options: DOMWidgetOptions<T, V> = {}
|
||||||
|
) {
|
||||||
|
// @ts-expect-error custom widget type
|
||||||
|
this.type = type
|
||||||
|
this.name = name
|
||||||
|
this.element = element
|
||||||
|
this.options = options
|
||||||
|
|
||||||
|
if (element.blur) {
|
||||||
|
this.mouseDownHandler = (event) => {
|
||||||
|
if (!element.contains(event.target as HTMLElement)) {
|
||||||
|
element.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', this.mouseDownHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): V {
|
||||||
|
return this.options.getValue?.() ?? ('' as V)
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(v: V) {
|
||||||
|
this.options.setValue?.(v)
|
||||||
|
this.callback?.(this.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract DOM widget size info */
|
||||||
|
computeLayoutSize(node: LGraphNode) {
|
||||||
|
// @ts-expect-error custom widget type
|
||||||
|
if (this.type === 'hidden') {
|
||||||
|
return {
|
||||||
|
minHeight: 0,
|
||||||
|
maxHeight: 0,
|
||||||
|
minWidth: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = getComputedStyle(this.element)
|
||||||
|
let minHeight =
|
||||||
|
this.options.getMinHeight?.() ??
|
||||||
|
parseInt(styles.getPropertyValue('--comfy-widget-min-height'))
|
||||||
|
let maxHeight =
|
||||||
|
this.options.getMaxHeight?.() ??
|
||||||
|
parseInt(styles.getPropertyValue('--comfy-widget-max-height'))
|
||||||
|
|
||||||
|
let prefHeight: string | number =
|
||||||
|
this.options.getHeight?.() ??
|
||||||
|
styles.getPropertyValue('--comfy-widget-height')
|
||||||
|
|
||||||
|
if (typeof prefHeight === 'string' && prefHeight.endsWith?.('%')) {
|
||||||
|
prefHeight =
|
||||||
|
node.size[1] *
|
||||||
|
(parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100)
|
||||||
|
} else {
|
||||||
|
prefHeight =
|
||||||
|
typeof prefHeight === 'number' ? prefHeight : parseInt(prefHeight)
|
||||||
|
|
||||||
|
if (isNaN(minHeight)) {
|
||||||
|
minHeight = prefHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minHeight: isNaN(minHeight) ? 50 : minHeight,
|
||||||
|
maxHeight: isNaN(maxHeight) ? undefined : maxHeight,
|
||||||
|
minWidth: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
node: LGraphNode,
|
||||||
|
widgetWidth: number,
|
||||||
|
y: number
|
||||||
|
): void {
|
||||||
|
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'
|
||||||
|
|
||||||
|
this.element.dataset.shouldHide = hidden ? 'true' : 'false'
|
||||||
|
const isInVisibleNodes = this.element.dataset.isInVisibleNodes === 'true'
|
||||||
|
const isCollapsed = this.element.dataset.collapsed === 'true'
|
||||||
|
const actualHidden = hidden || !isInVisibleNodes || isCollapsed
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.onDraw?.(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(): void {
|
||||||
|
if (this.mouseDownHandler) {
|
||||||
|
document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||||
|
}
|
||||||
|
this.element.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LGraphNode.prototype.addDOMWidget = function <
|
||||||
|
T extends HTMLElement,
|
||||||
|
V extends object | string
|
||||||
|
>(
|
||||||
|
this: LGraphNode,
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
element: T,
|
||||||
|
options: DOMWidgetOptions<T, V> = {}
|
||||||
|
): DOMWidget<T, V> {
|
||||||
|
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
||||||
|
|
||||||
|
if (!element.parentElement) {
|
||||||
|
app.canvasContainer.append(element)
|
||||||
|
}
|
||||||
|
element.hidden = true
|
||||||
|
element.style.display = 'none'
|
||||||
|
|
||||||
|
const { nodeData } = this.constructor
|
||||||
|
const tooltip = (nodeData?.input.required?.[name] ??
|
||||||
|
nodeData?.input.optional?.[name])?.[1]?.tooltip
|
||||||
|
if (tooltip && !element.title) {
|
||||||
|
element.title = tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new DOMWidgetImpl(name, type, element, options)
|
||||||
|
// Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493
|
||||||
|
// Some custom nodes are explicitly expecting getter and setter of `value`
|
||||||
|
// property to be on instance instead of prototype.
|
||||||
|
Object.defineProperty(widget, 'value', {
|
||||||
|
get(this: DOMWidgetImpl<T, V>): V {
|
||||||
|
return this.options.getValue?.() ?? ('' as V)
|
||||||
|
},
|
||||||
|
set(this: DOMWidgetImpl<T, V>, v: V) {
|
||||||
|
this.options.setValue?.(v)
|
||||||
|
this.callback?.(this.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure selectOn exists before iteration
|
||||||
|
const selectEvents = options.selectOn ?? ['focus', 'click']
|
||||||
|
for (const evt of selectEvents) {
|
||||||
|
element.addEventListener(evt, () => {
|
||||||
|
app.canvas.selectNode(this)
|
||||||
|
app.canvas.bringToFront(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addCustomWidget(widget)
|
||||||
|
elementWidgets.add(this)
|
||||||
|
|
||||||
|
const collapse = this.collapse
|
||||||
|
this.collapse = function (this: LGraphNode, force?: boolean) {
|
||||||
|
collapse.call(this, force)
|
||||||
|
if (this.collapsed) {
|
||||||
|
element.hidden = true
|
||||||
|
element.style.display = 'none'
|
||||||
|
}
|
||||||
|
element.dataset.collapsed = this.collapsed ? 'true' : 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onConfigure } = this
|
||||||
|
this.onConfigure = function (
|
||||||
|
this: LGraphNode,
|
||||||
|
serializedNode: ISerialisedNode
|
||||||
|
) {
|
||||||
|
onConfigure?.call(this, serializedNode)
|
||||||
|
element.dataset.collapsed = this.collapsed ? 'true' : 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRemoved = this.onRemoved
|
||||||
|
this.onRemoved = function (this: LGraphNode) {
|
||||||
|
element.remove()
|
||||||
|
elementWidgets.delete(this)
|
||||||
|
onRemoved?.call(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore index with symbol
|
||||||
|
if (!this[SIZE]) {
|
||||||
|
// @ts-ignore index with symbol
|
||||||
|
this[SIZE] = true
|
||||||
|
const onResize = this.onResize
|
||||||
|
this.onResize = function (this: LGraphNode, size: Size) {
|
||||||
|
options.beforeResize?.call(widget, this)
|
||||||
|
onResize?.call(this, size)
|
||||||
|
options.afterResize?.call(widget, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget
|
||||||
|
}
|
||||||
193
web/comfyui/legacy_tags_widget.js
Normal file
193
web/comfyui/legacy_tags_widget.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
export function addTagsWidget(node, name, opts, callback) {
|
||||||
|
// Create container for tags
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "comfy-tags-container";
|
||||||
|
Object.assign(container.style, {
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "4px", // 从8px减小到4px
|
||||||
|
padding: "6px",
|
||||||
|
minHeight: "30px",
|
||||||
|
backgroundColor: "rgba(40, 44, 52, 0.6)", // Darker, more modern background
|
||||||
|
borderRadius: "6px", // Slightly larger radius
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize default value as array
|
||||||
|
const initialTagsData = opts?.defaultVal || [];
|
||||||
|
|
||||||
|
// Function to render tags from array data
|
||||||
|
const renderTags = (tagsData, widget) => {
|
||||||
|
// Clear existing tags
|
||||||
|
while (container.firstChild) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTags = tagsData;
|
||||||
|
|
||||||
|
if (normalizedTags.length === 0) {
|
||||||
|
// Show message when no tags are present
|
||||||
|
const emptyMessage = document.createElement("div");
|
||||||
|
emptyMessage.textContent = "No trigger words detected";
|
||||||
|
Object.assign(emptyMessage.style, {
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "20px 0",
|
||||||
|
color: "rgba(226, 232, 240, 0.8)",
|
||||||
|
fontStyle: "italic",
|
||||||
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
});
|
||||||
|
container.appendChild(emptyMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedTags.forEach((tagData, index) => {
|
||||||
|
const { text, active } = tagData;
|
||||||
|
const tagEl = document.createElement("div");
|
||||||
|
tagEl.className = "comfy-tag";
|
||||||
|
|
||||||
|
updateTagStyle(tagEl, active);
|
||||||
|
|
||||||
|
tagEl.textContent = text;
|
||||||
|
tagEl.title = text; // Set tooltip for full content
|
||||||
|
|
||||||
|
// Add click handler to toggle state
|
||||||
|
tagEl.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Toggle active state for this specific tag using its index
|
||||||
|
const updatedTags = [...widget.value];
|
||||||
|
updatedTags[index].active = !updatedTags[index].active;
|
||||||
|
updateTagStyle(tagEl, updatedTags[index].active);
|
||||||
|
|
||||||
|
widget.value = updatedTags;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(tagEl);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to update tag style based on active state
|
||||||
|
function updateTagStyle(tagEl, active) {
|
||||||
|
const baseStyles = {
|
||||||
|
padding: "4px 12px", // 垂直内边距从6px减小到4px
|
||||||
|
borderRadius: "6px", // Matching container radius
|
||||||
|
maxWidth: "200px", // Increased max width
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontSize: "13px", // Slightly larger font
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s ease", // Smoother transition
|
||||||
|
border: "1px solid transparent",
|
||||||
|
display: "inline-block",
|
||||||
|
boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
||||||
|
margin: "2px", // 从4px减小到2px
|
||||||
|
userSelect: "none", // Add this line to prevent text selection
|
||||||
|
WebkitUserSelect: "none", // For Safari support
|
||||||
|
MozUserSelect: "none", // For Firefox support
|
||||||
|
msUserSelect: "none", // For IE/Edge support
|
||||||
|
};
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
Object.assign(tagEl.style, {
|
||||||
|
...baseStyles,
|
||||||
|
backgroundColor: "rgba(66, 153, 225, 0.9)", // Modern blue
|
||||||
|
color: "white",
|
||||||
|
borderColor: "rgba(66, 153, 225, 0.9)",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(tagEl.style, {
|
||||||
|
...baseStyles,
|
||||||
|
backgroundColor: "rgba(45, 55, 72, 0.7)", // Darker inactive state
|
||||||
|
color: "rgba(226, 232, 240, 0.8)", // Lighter text for contrast
|
||||||
|
borderColor: "rgba(226, 232, 240, 0.2)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hover effect
|
||||||
|
tagEl.onmouseenter = () => {
|
||||||
|
tagEl.style.transform = "translateY(-1px)";
|
||||||
|
tagEl.style.boxShadow = "0 2px 4px rgba(0,0,0,0.15)";
|
||||||
|
};
|
||||||
|
|
||||||
|
tagEl.onmouseleave = () => {
|
||||||
|
tagEl.style.transform = "translateY(0)";
|
||||||
|
tagEl.style.boxShadow = "0 1px 2px rgba(0,0,0,0.1)";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the value as array
|
||||||
|
let widgetValue = initialTagsData;
|
||||||
|
|
||||||
|
// Create widget with initial properties
|
||||||
|
const widget = node.addDOMWidget(name, "tags", container, {
|
||||||
|
getValue: function() {
|
||||||
|
return widgetValue;
|
||||||
|
},
|
||||||
|
setValue: function(v) {
|
||||||
|
widgetValue = v;
|
||||||
|
renderTags(widgetValue, widget);
|
||||||
|
|
||||||
|
// Update container height after rendering
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const minHeight = this.getMinHeight();
|
||||||
|
container.style.height = `${minHeight}px`;
|
||||||
|
|
||||||
|
// Force node to update size
|
||||||
|
node.setSize([node.size[0], node.computeSize()[1]]);
|
||||||
|
node.setDirtyCanvas(true, true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getMinHeight: function() {
|
||||||
|
const minHeight = 150;
|
||||||
|
// If no tags or only showing the empty message, return a minimum height
|
||||||
|
if (widgetValue.length === 0) {
|
||||||
|
return minHeight; // Height for empty state with message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tag elements
|
||||||
|
const tagElements = container.querySelectorAll('.comfy-tag');
|
||||||
|
|
||||||
|
if (tagElements.length === 0) {
|
||||||
|
return minHeight; // Fallback if elements aren't rendered yet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the actual height based on tag positions
|
||||||
|
let maxBottom = 0;
|
||||||
|
|
||||||
|
tagElements.forEach(tag => {
|
||||||
|
const rect = tag.getBoundingClientRect();
|
||||||
|
const tagBottom = rect.bottom - container.getBoundingClientRect().top;
|
||||||
|
maxBottom = Math.max(maxBottom, tagBottom);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add padding (top and bottom padding of container)
|
||||||
|
const computedStyle = window.getComputedStyle(container);
|
||||||
|
const paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
|
||||||
|
const paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
|
||||||
|
|
||||||
|
// Add extra buffer for potential wrapping issues and to ensure no clipping
|
||||||
|
const extraBuffer = 20;
|
||||||
|
|
||||||
|
// Round up to nearest 5px for clean sizing and ensure minimum height
|
||||||
|
return Math.max(minHeight, Math.ceil((maxBottom + paddingBottom + extraBuffer) / 5) * 5);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.value = initialTagsData;
|
||||||
|
|
||||||
|
widget.callback = callback;
|
||||||
|
|
||||||
|
widget.serializeValue = () => {
|
||||||
|
// Add dummy items to avoid the 2-element serialization issue, a bug in comfyui
|
||||||
|
return [...widgetValue,
|
||||||
|
{ text: "__dummy_item__", active: false, _isDummy: true },
|
||||||
|
{ text: "__dummy_item__", active: false, _isDummy: true }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return { minWidth: 300, minHeight: 150, widget };
|
||||||
|
}
|
||||||
@@ -2,20 +2,36 @@ export function addTagsWidget(node, name, opts, callback) {
|
|||||||
// Create container for tags
|
// Create container for tags
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.className = "comfy-tags-container";
|
container.className = "comfy-tags-container";
|
||||||
|
|
||||||
|
// Set initial height
|
||||||
|
const defaultHeight = 150;
|
||||||
|
container.style.setProperty('--comfy-widget-min-height', `${defaultHeight}px`);
|
||||||
|
container.style.setProperty('--comfy-widget-max-height', `${defaultHeight * 2}px`);
|
||||||
|
container.style.setProperty('--comfy-widget-height', `${defaultHeight}px`);
|
||||||
|
|
||||||
Object.assign(container.style, {
|
Object.assign(container.style, {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: "4px", // 从8px减小到4px
|
gap: "4px",
|
||||||
padding: "6px",
|
padding: "6px",
|
||||||
minHeight: "30px",
|
backgroundColor: "rgba(40, 44, 52, 0.6)",
|
||||||
backgroundColor: "rgba(40, 44, 52, 0.6)", // Darker, more modern background
|
borderRadius: "6px",
|
||||||
borderRadius: "6px", // Slightly larger radius
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
overflow: "auto",
|
||||||
|
alignItems: "flex-start" // Ensure tags align at the top of each row
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize default value as array
|
// Initialize default value as array
|
||||||
const initialTagsData = opts?.defaultVal || [];
|
const initialTagsData = opts?.defaultVal || [];
|
||||||
|
|
||||||
|
// Fixed sizes for tag elements to avoid zoom-related calculation issues
|
||||||
|
const TAG_HEIGHT = 26; // Adjusted height of a single tag including margins
|
||||||
|
const TAGS_PER_ROW = 3; // Approximate number of tags per row
|
||||||
|
const ROW_GAP = 2; // Reduced gap between rows
|
||||||
|
const CONTAINER_PADDING = 12; // Top and bottom padding
|
||||||
|
const EMPTY_CONTAINER_HEIGHT = 60; // Height when no tags are present
|
||||||
|
|
||||||
// Function to render tags from array data
|
// Function to render tags from array data
|
||||||
const renderTags = (tagsData, widget) => {
|
const renderTags = (tagsData, widget) => {
|
||||||
// Clear existing tags
|
// Clear existing tags
|
||||||
@@ -38,11 +54,28 @@ export function addTagsWidget(node, name, opts, callback) {
|
|||||||
WebkitUserSelect: "none",
|
WebkitUserSelect: "none",
|
||||||
MozUserSelect: "none",
|
MozUserSelect: "none",
|
||||||
msUserSelect: "none",
|
msUserSelect: "none",
|
||||||
|
width: "100%"
|
||||||
});
|
});
|
||||||
container.appendChild(emptyMessage);
|
container.appendChild(emptyMessage);
|
||||||
|
|
||||||
|
// Set fixed height for empty state
|
||||||
|
updateWidgetHeight(EMPTY_CONTAINER_HEIGHT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a row container approach for better layout control
|
||||||
|
let rowContainer = document.createElement("div");
|
||||||
|
rowContainer.className = "comfy-tags-row";
|
||||||
|
Object.assign(rowContainer.style, {
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "4px",
|
||||||
|
width: "100%",
|
||||||
|
marginBottom: "2px" // Small gap between rows
|
||||||
|
});
|
||||||
|
container.appendChild(rowContainer);
|
||||||
|
|
||||||
|
let tagCount = 0;
|
||||||
normalizedTags.forEach((tagData, index) => {
|
normalizedTags.forEach((tagData, index) => {
|
||||||
const { text, active } = tagData;
|
const { text, active } = tagData;
|
||||||
const tagEl = document.createElement("div");
|
const tagEl = document.createElement("div");
|
||||||
@@ -65,44 +98,75 @@ export function addTagsWidget(node, name, opts, callback) {
|
|||||||
widget.value = updatedTags;
|
widget.value = updatedTags;
|
||||||
});
|
});
|
||||||
|
|
||||||
container.appendChild(tagEl);
|
rowContainer.appendChild(tagEl);
|
||||||
|
tagCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calculate height based on number of tags and fixed sizes
|
||||||
|
const tagsCount = normalizedTags.length;
|
||||||
|
const rows = Math.ceil(tagsCount / TAGS_PER_ROW);
|
||||||
|
const calculatedHeight = CONTAINER_PADDING + (rows * TAG_HEIGHT) + ((rows - 1) * ROW_GAP);
|
||||||
|
|
||||||
|
// Update widget height with calculated value
|
||||||
|
updateWidgetHeight(calculatedHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to update widget height consistently
|
||||||
|
const updateWidgetHeight = (height) => {
|
||||||
|
// Ensure minimum height
|
||||||
|
const finalHeight = Math.max(defaultHeight, height);
|
||||||
|
|
||||||
|
// Update CSS variables
|
||||||
|
container.style.setProperty('--comfy-widget-min-height', `${finalHeight}px`);
|
||||||
|
container.style.setProperty('--comfy-widget-height', `${finalHeight}px`);
|
||||||
|
|
||||||
|
// Force node to update size after a short delay to ensure DOM is updated
|
||||||
|
if (node) {
|
||||||
|
setTimeout(() => {
|
||||||
|
node.setDirtyCanvas(true, true);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to update tag style based on active state
|
// Helper function to update tag style based on active state
|
||||||
function updateTagStyle(tagEl, active) {
|
function updateTagStyle(tagEl, active) {
|
||||||
const baseStyles = {
|
const baseStyles = {
|
||||||
padding: "4px 12px", // 垂直内边距从6px减小到4px
|
padding: "4px 10px", // Slightly reduced horizontal padding
|
||||||
borderRadius: "6px", // Matching container radius
|
borderRadius: "6px",
|
||||||
maxWidth: "200px", // Increased max width
|
maxWidth: "200px",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
fontSize: "13px", // Slightly larger font
|
fontSize: "13px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all 0.2s ease", // Smoother transition
|
transition: "all 0.2s ease",
|
||||||
border: "1px solid transparent",
|
border: "1px solid transparent",
|
||||||
display: "inline-block",
|
display: "inline-flex", // Changed to inline-flex for better text alignment
|
||||||
|
alignItems: "center", // Center text vertically
|
||||||
|
justifyContent: "center", // Center text horizontally
|
||||||
boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
||||||
margin: "2px", // 从4px减小到2px
|
margin: "1px", // Reduced margin
|
||||||
userSelect: "none", // Add this line to prevent text selection
|
userSelect: "none",
|
||||||
WebkitUserSelect: "none", // For Safari support
|
WebkitUserSelect: "none",
|
||||||
MozUserSelect: "none", // For Firefox support
|
MozUserSelect: "none",
|
||||||
msUserSelect: "none", // For IE/Edge support
|
msUserSelect: "none",
|
||||||
|
height: "20px", // Slightly increased height to prevent text cutoff
|
||||||
|
minHeight: "20px", // Ensure consistent height
|
||||||
|
boxSizing: "border-box" // Ensure padding doesn't affect the overall size
|
||||||
};
|
};
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
Object.assign(tagEl.style, {
|
Object.assign(tagEl.style, {
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
backgroundColor: "rgba(66, 153, 225, 0.9)", // Modern blue
|
backgroundColor: "rgba(66, 153, 225, 0.9)",
|
||||||
color: "white",
|
color: "white",
|
||||||
borderColor: "rgba(66, 153, 225, 0.9)",
|
borderColor: "rgba(66, 153, 225, 0.9)",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(tagEl.style, {
|
Object.assign(tagEl.style, {
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
backgroundColor: "rgba(45, 55, 72, 0.7)", // Darker inactive state
|
backgroundColor: "rgba(45, 55, 72, 0.7)",
|
||||||
color: "rgba(226, 232, 240, 0.8)", // Lighter text for contrast
|
color: "rgba(226, 232, 240, 0.8)",
|
||||||
borderColor: "rgba(226, 232, 240, 0.2)",
|
borderColor: "rgba(226, 232, 240, 0.2)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -122,72 +186,48 @@ export function addTagsWidget(node, name, opts, callback) {
|
|||||||
// Store the value as array
|
// Store the value as array
|
||||||
let widgetValue = initialTagsData;
|
let widgetValue = initialTagsData;
|
||||||
|
|
||||||
// Create widget with initial properties
|
// Create widget with new DOM Widget API
|
||||||
const widget = node.addDOMWidget(name, "tags", container, {
|
const widget = node.addDOMWidget(name, "custom", container, {
|
||||||
getValue: function() {
|
getValue: function() {
|
||||||
return widgetValue;
|
return widgetValue;
|
||||||
},
|
},
|
||||||
setValue: function(v) {
|
setValue: function(v) {
|
||||||
widgetValue = v;
|
widgetValue = v;
|
||||||
renderTags(widgetValue, widget);
|
renderTags(widgetValue, widget);
|
||||||
|
|
||||||
// Update container height after rendering
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const minHeight = this.getMinHeight();
|
|
||||||
container.style.height = `${minHeight}px`;
|
|
||||||
|
|
||||||
// Force node to update size
|
|
||||||
node.setSize([node.size[0], node.computeSize()[1]]);
|
|
||||||
node.setDirtyCanvas(true, true);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
getMinHeight: function() {
|
getMinHeight: function() {
|
||||||
const minHeight = 150;
|
return parseInt(container.style.getPropertyValue('--comfy-widget-min-height')) || defaultHeight;
|
||||||
// If no tags or only showing the empty message, return a minimum height
|
|
||||||
if (widgetValue.length === 0) {
|
|
||||||
return minHeight; // Height for empty state with message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all tag elements
|
|
||||||
const tagElements = container.querySelectorAll('.comfy-tag');
|
|
||||||
|
|
||||||
if (tagElements.length === 0) {
|
|
||||||
return minHeight; // Fallback if elements aren't rendered yet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the actual height based on tag positions
|
|
||||||
let maxBottom = 0;
|
|
||||||
|
|
||||||
tagElements.forEach(tag => {
|
|
||||||
const rect = tag.getBoundingClientRect();
|
|
||||||
const tagBottom = rect.bottom - container.getBoundingClientRect().top;
|
|
||||||
maxBottom = Math.max(maxBottom, tagBottom);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add padding (top and bottom padding of container)
|
|
||||||
const computedStyle = window.getComputedStyle(container);
|
|
||||||
const paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
|
|
||||||
const paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
|
|
||||||
|
|
||||||
// Add extra buffer for potential wrapping issues and to ensure no clipping
|
|
||||||
const extraBuffer = 20;
|
|
||||||
|
|
||||||
// Round up to nearest 5px for clean sizing and ensure minimum height
|
|
||||||
return Math.max(minHeight, Math.ceil((maxBottom + paddingBottom + extraBuffer) / 5) * 5);
|
|
||||||
},
|
},
|
||||||
|
getMaxHeight: function() {
|
||||||
|
return parseInt(container.style.getPropertyValue('--comfy-widget-max-height')) || defaultHeight * 2;
|
||||||
|
},
|
||||||
|
getHeight: function() {
|
||||||
|
return parseInt(container.style.getPropertyValue('--comfy-widget-height')) || defaultHeight;
|
||||||
|
},
|
||||||
|
hideOnZoom: true,
|
||||||
|
selectOn: ['click', 'focus'],
|
||||||
|
afterResize: function(node) {
|
||||||
|
// Re-render tags after node resize
|
||||||
|
if (this.value && this.value.length > 0) {
|
||||||
|
renderTags(this.value, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial value
|
||||||
widget.value = initialTagsData;
|
widget.value = initialTagsData;
|
||||||
|
|
||||||
|
// Set callback
|
||||||
widget.callback = callback;
|
widget.callback = callback;
|
||||||
|
|
||||||
|
// Add serialization method to avoid ComfyUI serialization issues
|
||||||
widget.serializeValue = () => {
|
widget.serializeValue = () => {
|
||||||
// Add dummy items to avoid the 2-element serialization issue, a bug in comfyui
|
// Add dummy items to avoid the 2-element serialization issue
|
||||||
return [...widgetValue,
|
return [...widgetValue,
|
||||||
{ text: "__dummy_item__", active: false, _isDummy: true },
|
{ text: "__dummy_item__", active: false, _isDummy: true },
|
||||||
{ text: "__dummy_item__", active: false, _isDummy: true }
|
{ text: "__dummy_item__", active: false, _isDummy: true }
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
return { minWidth: 300, minHeight: 150, widget };
|
return { minWidth: 300, minHeight: defaultHeight, widget };
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,36 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { addTagsWidget } from "./tags_widget.js";
|
|
||||||
|
|
||||||
const CONVERTED_TYPE = 'converted-widget'
|
const CONVERTED_TYPE = 'converted-widget'
|
||||||
|
|
||||||
|
function getComfyUIFrontendVersion() {
|
||||||
|
// 直接访问全局变量
|
||||||
|
return window['__COMFYUI_FRONTEND_VERSION__'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically import the appropriate tags widget based on app version
|
||||||
|
function getTagsWidgetModule() {
|
||||||
|
// Parse app version and compare with 1.12.6
|
||||||
|
const currentVersion = getComfyUIFrontendVersion() || "0.0.0";
|
||||||
|
console.log("currentVersion", currentVersion);
|
||||||
|
const versionParts = currentVersion.split('.').map(part => parseInt(part, 10));
|
||||||
|
const requiredVersion = [1, 12, 6];
|
||||||
|
|
||||||
|
// Compare version numbers
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (versionParts[i] > requiredVersion[i]) {
|
||||||
|
console.log("Using tags_widget.js");
|
||||||
|
return import("./tags_widget.js");
|
||||||
|
} else if (versionParts[i] < requiredVersion[i]) {
|
||||||
|
console.log("Using legacy_tags_widget.js");
|
||||||
|
return import("./legacy_tags_widget.js");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, versions are equal, use the new module
|
||||||
|
return import("./tags_widget.js");
|
||||||
|
}
|
||||||
|
|
||||||
// TriggerWordToggle extension for ComfyUI
|
// TriggerWordToggle extension for ComfyUI
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LoraManager.TriggerWordToggle",
|
name: "LoraManager.TriggerWordToggle",
|
||||||
@@ -26,7 +53,11 @@ app.registerExtension({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wait for node to be properly initialized
|
// Wait for node to be properly initialized
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(async () => {
|
||||||
|
// Dynamically import the appropriate tags widget module
|
||||||
|
const tagsWidgetModule = await getTagsWidgetModule();
|
||||||
|
const { addTagsWidget } = tagsWidgetModule;
|
||||||
|
|
||||||
// Get the widget object directly from the returned object
|
// Get the widget object directly from the returned object
|
||||||
const result = addTagsWidget(node, "toggle_trigger_words", {
|
const result = addTagsWidget(node, "toggle_trigger_words", {
|
||||||
defaultVal: []
|
defaultVal: []
|
||||||
|
|||||||
Reference in New Issue
Block a user