/** * Composable for managing widget value synchronization between Vue and LiteGraph * Provides consistent pattern for immediate UI updates and LiteGraph callbacks */ import { computed, toValue, ref, watch } from 'vue' import type { Ref } from 'vue' import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' import type { MaybeRefOrGetter } from '@vueuse/core' interface UseWidgetValueOptions { /** The widget configuration from LiteGraph */ widget: SimplifiedWidget /** The current value from parent component (can be a value or a getter function) */ modelValue: MaybeRefOrGetter /** Default value if modelValue is null/undefined */ defaultValue: T /** Emit function from component setup */ emit: (event: 'update:modelValue', value: T) => void /** Optional value transformer before sending to LiteGraph */ transform?: (value: U) => T } interface UseWidgetValueReturn { /** Local value for immediate UI updates */ localValue: Ref /** Handler for user interactions */ onChange: (newValue: U) => void } /** * Manages widget value synchronization with LiteGraph * * @example * ```vue * const { localValue, onChange } = useWidgetValue({ * widget: props.widget, * modelValue: props.modelValue, * defaultValue: '' * }) * ``` */ export function useWidgetValue({ widget, modelValue, defaultValue, emit, transform }: UseWidgetValueOptions): UseWidgetValueReturn { // Ref for immediate UI feedback before value flows back through modelValue const newProcessedValue = ref(null) // Computed that prefers the immediately processed value, then falls back to modelValue const localValue = computed( () => newProcessedValue.value ?? toValue(modelValue) ?? defaultValue ) // Clear newProcessedValue when modelValue updates (allowing external changes to flow through) watch( () => toValue(modelValue), () => { newProcessedValue.value = null } ) // Handle user changes const onChange = (newValue: U) => { // Handle different PrimeVue component signatures let processedValue: T if (transform) { processedValue = transform(newValue) } else { // Ensure type safety - only cast when types are compatible if ( typeof newValue === typeof defaultValue || newValue === null || newValue === undefined ) { processedValue = (newValue ?? defaultValue) as T } else { console.warn( `useWidgetValue: Type mismatch for widget ${widget.name}. Expected ${typeof defaultValue}, got ${typeof newValue}` ) processedValue = defaultValue } } // Set for immediate UI feedback newProcessedValue.value = processedValue // Emit to parent component emit('update:modelValue', processedValue) } return { localValue: localValue as Ref, onChange } } /** * Type-specific helper for string widgets */ export function useStringWidgetValue( widget: SimplifiedWidget, modelValue: string | (() => string), emit: (event: 'update:modelValue', value: string) => void ) { return useWidgetValue({ widget, modelValue, defaultValue: '', emit, transform: (value: string | undefined) => String(value || '') // Handle undefined from PrimeVue }) } /** * Type-specific helper for number widgets */ export function useNumberWidgetValue( widget: SimplifiedWidget, modelValue: number | (() => number), emit: (event: 'update:modelValue', value: number) => void ) { return useWidgetValue({ widget, modelValue, defaultValue: 0, emit, transform: (value: number | number[]) => { // Handle PrimeVue Slider which can emit number | number[] if (Array.isArray(value)) { return value.length > 0 ? (value[0] ?? 0) : 0 } return Number(value) || 0 } }) } /** * Type-specific helper for boolean widgets */ export function useBooleanWidgetValue( widget: SimplifiedWidget, modelValue: boolean | (() => boolean), emit: (event: 'update:modelValue', value: boolean) => void ) { return useWidgetValue({ widget, modelValue, defaultValue: false, emit, transform: (value: boolean) => Boolean(value) }) }