mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
live preview - String length and concatenate node
This commit is contained in:
344
src/composables/useLivePreview.ts
Normal file
344
src/composables/useLivePreview.ts
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import type {
|
||||||
|
IBaseWidget,
|
||||||
|
TWidgetValue
|
||||||
|
} from '@/lib/litegraph/src/types/widgets'
|
||||||
|
|
||||||
|
interface PropagationOptions {
|
||||||
|
/**
|
||||||
|
* Find output by name instead of index
|
||||||
|
*/
|
||||||
|
outputName?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly specify output index (default: 0)
|
||||||
|
*/
|
||||||
|
outputIndex?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to call node.setOutputData (default: false)
|
||||||
|
*/
|
||||||
|
setOutputData?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to update target widget values (default: true)
|
||||||
|
*/
|
||||||
|
updateWidget?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to call widget.callback after updating (default: false)
|
||||||
|
*/
|
||||||
|
callWidgetCallback?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to call targetNode.onExecuted (default: false)
|
||||||
|
*/
|
||||||
|
callOnExecuted?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom function to build the message for onExecuted
|
||||||
|
*/
|
||||||
|
messageBuilder?: (
|
||||||
|
targetNode: LGraphNode,
|
||||||
|
value: TWidgetValue,
|
||||||
|
link: any
|
||||||
|
) => any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom handlers for specific node types
|
||||||
|
* Return true if handled, false to continue with default behavior
|
||||||
|
*/
|
||||||
|
customHandlers?: Map<
|
||||||
|
string,
|
||||||
|
(node: LGraphNode, value: TWidgetValue, link: any) => boolean
|
||||||
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable reentry protection (default: true)
|
||||||
|
*/
|
||||||
|
preventReentry?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculator function type for live preview nodes
|
||||||
|
* Takes input values and returns the computed output value
|
||||||
|
*/
|
||||||
|
type LivePreviewCalculator = (inputValues: any[]) => TWidgetValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for setting up a live preview node
|
||||||
|
*/
|
||||||
|
interface LivePreviewNodeConfig {
|
||||||
|
/**
|
||||||
|
* The calculator function that computes output from inputs
|
||||||
|
*/
|
||||||
|
calculator: LivePreviewCalculator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional output index (default: 0)
|
||||||
|
*/
|
||||||
|
outputIndex?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional propagation options to use when propagating the result
|
||||||
|
*/
|
||||||
|
propagationOptions?: Omit<PropagationOptions, 'outputIndex' | 'setOutputData'>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing live preview functionality in ComfyUI nodes
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // In a node extension:
|
||||||
|
* const { setupLivePreviewNode, propagateLivePreview } = useLivePreview()
|
||||||
|
*
|
||||||
|
* // For computation nodes:
|
||||||
|
* setupLivePreviewNode(node, {
|
||||||
|
* calculator: (inputs) => {
|
||||||
|
* const [a, b] = inputs
|
||||||
|
* return a + b
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* // For simple propagation:
|
||||||
|
* propagateLivePreview(node, value, {
|
||||||
|
* updateWidget: true,
|
||||||
|
* callOnExecuted: true
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const propagationFlags = new WeakMap<LGraphNode, Set<string>>()
|
||||||
|
const nodeCalculators = new WeakMap<LGraphNode, LivePreviewNodeConfig>()
|
||||||
|
|
||||||
|
export function useLivePreview() {
|
||||||
|
function getPropagationKey(outputIndex: number): string {
|
||||||
|
return `propagating_${outputIndex}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodePropagating(node: LGraphNode, outputIndex: number): boolean {
|
||||||
|
const flags = propagationFlags.get(node)
|
||||||
|
return flags?.has(getPropagationKey(outputIndex)) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNodePropagating(
|
||||||
|
node: LGraphNode,
|
||||||
|
outputIndex: number,
|
||||||
|
value: boolean
|
||||||
|
): void {
|
||||||
|
if (!propagationFlags.has(node)) {
|
||||||
|
propagationFlags.set(node, new Set())
|
||||||
|
}
|
||||||
|
const flags = propagationFlags.get(node)!
|
||||||
|
const key = getPropagationKey(outputIndex)
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
flags.add(key)
|
||||||
|
} else {
|
||||||
|
flags.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectNodeInputValues(node: LGraphNode): any[] {
|
||||||
|
const inputValues: any[] = []
|
||||||
|
const graph = node.graph as LGraph
|
||||||
|
|
||||||
|
if (!graph || !node.inputs) {
|
||||||
|
return inputValues
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const input of node.inputs) {
|
||||||
|
if (input.link != null) {
|
||||||
|
const link = graph.links[input.link]
|
||||||
|
if (link) {
|
||||||
|
const sourceNode = graph.getNodeById(link.origin_id)
|
||||||
|
if (sourceNode && sourceNode.getOutputData) {
|
||||||
|
const outputData = sourceNode.getOutputData(link.origin_slot)
|
||||||
|
inputValues.push(outputData)
|
||||||
|
} else {
|
||||||
|
inputValues.push(undefined)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inputValues.push(undefined)
|
||||||
|
}
|
||||||
|
} else if (input.widget) {
|
||||||
|
const widget = node.widgets?.find((w) => w.name === input.widget?.name)
|
||||||
|
inputValues.push(widget?.value)
|
||||||
|
} else {
|
||||||
|
inputValues.push(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputValues
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerNodeRecalculation(node: LGraphNode): void {
|
||||||
|
const config = nodeCalculators.get(node)
|
||||||
|
if (!config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputValues = collectNodeInputValues(node)
|
||||||
|
|
||||||
|
const hasValidInputs = inputValues.some((v) => v !== undefined)
|
||||||
|
if (!hasValidInputs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = config.calculator(inputValues)
|
||||||
|
if (result !== undefined) {
|
||||||
|
propagateLivePreview(node, result, {
|
||||||
|
outputIndex: config.outputIndex ?? 0,
|
||||||
|
setOutputData: true,
|
||||||
|
...config.propagationOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error calculating live preview for node ${node.type}:`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function propagateLivePreview(
|
||||||
|
sourceNode: LGraphNode,
|
||||||
|
value: TWidgetValue,
|
||||||
|
options: PropagationOptions = {}
|
||||||
|
): void {
|
||||||
|
const {
|
||||||
|
outputName,
|
||||||
|
outputIndex: explicitOutputIndex,
|
||||||
|
setOutputData = false,
|
||||||
|
updateWidget = true,
|
||||||
|
callWidgetCallback = false,
|
||||||
|
callOnExecuted = false,
|
||||||
|
messageBuilder,
|
||||||
|
customHandlers,
|
||||||
|
preventReentry = true
|
||||||
|
} = options
|
||||||
|
|
||||||
|
let outputIndex = explicitOutputIndex ?? 0
|
||||||
|
|
||||||
|
if (outputName && sourceNode.outputs) {
|
||||||
|
const foundIndex = sourceNode.outputs.findIndex(
|
||||||
|
(output) => output.name === outputName
|
||||||
|
)
|
||||||
|
if (foundIndex >= 0) {
|
||||||
|
outputIndex = foundIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preventReentry && isNodePropagating(sourceNode, outputIndex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preventReentry) {
|
||||||
|
setNodePropagating(sourceNode, outputIndex, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (setOutputData && sourceNode.setOutputData && value !== undefined) {
|
||||||
|
sourceNode.setOutputData(outputIndex, value as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = sourceNode.outputs?.[outputIndex]
|
||||||
|
if (!output || !output.links || output.links.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph = sourceNode.graph as LGraph
|
||||||
|
if (!graph) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const linkId of output.links) {
|
||||||
|
const link = graph.links[linkId]
|
||||||
|
if (!link) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNode = graph.getNodeById(link.target_id)
|
||||||
|
if (!targetNode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customHandlers?.has(targetNode.type)) {
|
||||||
|
const handler = customHandlers.get(targetNode.type)!
|
||||||
|
const handled = handler(targetNode, value, link)
|
||||||
|
if (handled) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateWidget) {
|
||||||
|
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||||
|
if (targetInput?.widget) {
|
||||||
|
const targetWidget = targetNode.widgets?.find(
|
||||||
|
(w: IBaseWidget) => w.name === targetInput.widget?.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (targetWidget) {
|
||||||
|
targetWidget.value = value
|
||||||
|
|
||||||
|
if (callWidgetCallback && targetWidget.callback) {
|
||||||
|
targetWidget.callback(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCalculator = nodeCalculators.has(targetNode)
|
||||||
|
|
||||||
|
if (hasCalculator) {
|
||||||
|
triggerNodeRecalculation(targetNode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callOnExecuted && targetNode.onExecuted) {
|
||||||
|
const message = messageBuilder
|
||||||
|
? messageBuilder(targetNode, value, link)
|
||||||
|
: { text: [value] }
|
||||||
|
|
||||||
|
targetNode.onExecuted(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (preventReentry) {
|
||||||
|
setNodePropagating(sourceNode, outputIndex, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupLivePreviewNode(
|
||||||
|
node: LGraphNode,
|
||||||
|
config: LivePreviewNodeConfig
|
||||||
|
): void {
|
||||||
|
nodeCalculators.set(node, config)
|
||||||
|
|
||||||
|
const originalOnExecuted = node.onExecuted
|
||||||
|
node.onExecuted = function (message: any) {
|
||||||
|
if (originalOnExecuted) {
|
||||||
|
originalOnExecuted.call(this, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.text && Array.isArray(message.text)) {
|
||||||
|
const result = config.calculator(message.text)
|
||||||
|
if (result !== undefined) {
|
||||||
|
propagateLivePreview(this, result, {
|
||||||
|
outputIndex: config.outputIndex ?? 0,
|
||||||
|
setOutputData: true,
|
||||||
|
...config.propagationOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
propagateLivePreview,
|
||||||
|
setupLivePreviewNode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import './matchType'
|
|||||||
import './nodeTemplates'
|
import './nodeTemplates'
|
||||||
import './noteNode'
|
import './noteNode'
|
||||||
import './previewAny'
|
import './previewAny'
|
||||||
|
import './stringOperations'
|
||||||
import './rerouteNode'
|
import './rerouteNode'
|
||||||
import './saveImageExtraOutput'
|
import './saveImageExtraOutput'
|
||||||
import './saveMesh'
|
import './saveMesh'
|
||||||
|
|||||||
58
src/extensions/core/stringOperations.ts
Normal file
58
src/extensions/core/stringOperations.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
|
import { useLivePreview } from '@/composables/useLivePreview'
|
||||||
|
|
||||||
|
const { setupLivePreviewNode } = useLivePreview()
|
||||||
|
|
||||||
|
useExtensionService().registerExtension({
|
||||||
|
name: 'Comfy.StringLength',
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData.name === 'StringLength') {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
if (onNodeCreated) {
|
||||||
|
onNodeCreated.call(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up live preview with calculator
|
||||||
|
setupLivePreviewNode(this, {
|
||||||
|
calculator: (inputs) => {
|
||||||
|
const inputString = inputs[0]
|
||||||
|
if (inputString == null) return undefined
|
||||||
|
return String(inputString).length
|
||||||
|
},
|
||||||
|
propagationOptions: {
|
||||||
|
updateWidget: true,
|
||||||
|
callOnExecuted: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useExtensionService().registerExtension({
|
||||||
|
name: 'Comfy.StringConcatenate',
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData.name === 'StringConcatenate') {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
if (onNodeCreated) {
|
||||||
|
onNodeCreated.call(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up live preview with calculator
|
||||||
|
setupLivePreviewNode(this, {
|
||||||
|
calculator: (inputs) => {
|
||||||
|
const [string_a, string_b, delimiter] = inputs
|
||||||
|
if (string_a == null && string_b == null) return undefined
|
||||||
|
return [string_a ?? '', string_b ?? ''].join(delimiter || '')
|
||||||
|
},
|
||||||
|
propagationOptions: {
|
||||||
|
updateWidget: true,
|
||||||
|
callOnExecuted: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -67,7 +67,9 @@ import type {
|
|||||||
VueNodeData,
|
VueNodeData,
|
||||||
WidgetSlotMetadata
|
WidgetSlotMetadata
|
||||||
} from '@/composables/graph/useGraphNodeManager'
|
} from '@/composables/graph/useGraphNodeManager'
|
||||||
|
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
|
import { useLivePreview } from '@/composables/useLivePreview'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
@@ -83,12 +85,16 @@ import { cn } from '@/utils/tailwindUtil'
|
|||||||
|
|
||||||
import InputSlot from './InputSlot.vue'
|
import InputSlot from './InputSlot.vue'
|
||||||
|
|
||||||
|
const { propagateLivePreview } = useLivePreview()
|
||||||
|
|
||||||
interface NodeWidgetsProps {
|
interface NodeWidgetsProps {
|
||||||
nodeData?: VueNodeData
|
nodeData?: VueNodeData
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodeData } = defineProps<NodeWidgetsProps>()
|
const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||||
|
|
||||||
|
const { nodeManager } = useVueNodeLifecycle()
|
||||||
|
|
||||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||||
useCanvasInteractions()
|
useCanvasInteractions()
|
||||||
function handleWidgetPointerEvent(event: PointerEvent) {
|
function handleWidgetPointerEvent(event: PointerEvent) {
|
||||||
@@ -113,6 +119,24 @@ const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
|
|||||||
nodeType.value
|
nodeType.value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function propagateToDownstreamVue(
|
||||||
|
sourceNodeId: string,
|
||||||
|
widgetName: string,
|
||||||
|
value: WidgetValue
|
||||||
|
): void {
|
||||||
|
const lgNode = nodeManager.value?.getNode(sourceNodeId)
|
||||||
|
if (!lgNode || !value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateLivePreview(lgNode, value, {
|
||||||
|
outputName: widgetName,
|
||||||
|
updateWidget: true,
|
||||||
|
callWidgetCallback: false,
|
||||||
|
callOnExecuted: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
interface ProcessedWidget {
|
interface ProcessedWidget {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
@@ -170,6 +194,10 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
if (widget.type !== 'asset') {
|
if (widget.type !== 'asset') {
|
||||||
widget.callback?.(value)
|
widget.callback?.(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nodeData?.id && nodeManager.value) {
|
||||||
|
propagateToDownstreamVue(nodeData.id, widget.name, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltipText = getWidgetTooltip(widget)
|
const tooltipText = getWidgetTooltip(widget)
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { isStringInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
|||||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||||
|
import type { WidgetValue } from '@/types/simplifiedWidget'
|
||||||
|
import { useLivePreview } from '@/composables/useLivePreview'
|
||||||
|
|
||||||
|
const { propagateLivePreview } = useLivePreview()
|
||||||
|
|
||||||
const TRACKPAD_DETECTION_THRESHOLD = 50
|
const TRACKPAD_DETECTION_THRESHOLD = 50
|
||||||
|
|
||||||
@@ -119,6 +123,22 @@ export const useStringWidget = () => {
|
|||||||
const defaultVal = inputSpec.default ?? ''
|
const defaultVal = inputSpec.default ?? ''
|
||||||
const multiline = inputSpec.multiline
|
const multiline = inputSpec.multiline
|
||||||
|
|
||||||
|
const propagateCallback = (value: WidgetValue) => {
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple propagation: just send the value downstream
|
||||||
|
// - Nodes with calculators will automatically recalculate
|
||||||
|
// - Passive nodes (like PreviewAny) will receive onExecuted
|
||||||
|
propagateLivePreview(node, value, {
|
||||||
|
outputName: inputSpec.name,
|
||||||
|
setOutputData: true,
|
||||||
|
updateWidget: true,
|
||||||
|
callOnExecuted: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const widget = multiline
|
const widget = multiline
|
||||||
? addMultilineWidget(node, inputSpec.name, {
|
? addMultilineWidget(node, inputSpec.name, {
|
||||||
defaultVal,
|
defaultVal,
|
||||||
@@ -130,6 +150,23 @@ export const useStringWidget = () => {
|
|||||||
widget.dynamicPrompts = inputSpec.dynamicPrompts
|
widget.dynamicPrompts = inputSpec.dynamicPrompts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalCallback = widget.callback
|
||||||
|
widget.callback = function (value: WidgetValue) {
|
||||||
|
if (originalCallback) {
|
||||||
|
;(originalCallback as any).call(this, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = node.inputs?.find(
|
||||||
|
(input) => input.widget?.name === inputSpec.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (input?.link) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateCallback(value)
|
||||||
|
}
|
||||||
|
|
||||||
return widget
|
return widget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user