feat: add clear button to autocomplete text widget and fix external value change sync

- Add clear button inside autocomplete text widget that shows when text exists
- Support both Canvas mode and Vue DOM mode with appropriate styling
- Fix clear button visibility when value is changed externally (e.g., via 'send lora to workflow')
- Implement dual notification mechanism: CustomEvent + onSetValue callback
- Update widget interface to include onSetValue property
This commit is contained in:
Will Miao
2026-02-06 09:15:16 +08:00
parent b313f36be9
commit 1606a3ff46
4 changed files with 317 additions and 53 deletions

View File

@@ -1,17 +1,31 @@
<template>
<div class="autocomplete-text-widget">
<textarea
ref="textareaRef"
:placeholder="placeholder"
:spellcheck="spellcheck ?? false"
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]"
@input="onInput"
/>
<div class="input-wrapper">
<textarea
ref="textareaRef"
:placeholder="placeholder"
:spellcheck="spellcheck ?? false"
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]"
@input="onInput"
/>
<button
v-if="showClearButton"
type="button"
class="clear-button"
title="Clear text"
@click="clearText"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useAutocomplete } from '@/composables/useAutocomplete'
// Access LiteGraph global for initial mode detection
@@ -20,6 +34,7 @@ declare const LiteGraph: { vueNodesMode?: boolean } | undefined
export interface AutocompleteTextWidgetInterface {
inputEl?: HTMLTextAreaElement
callback?: (v: string) => void
onSetValue?: (v: string) => void
}
const props = defineProps<{
@@ -41,6 +56,10 @@ const onModeChange = (event: Event) => {
}
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const hasText = ref(false)
// Show clear button when there is text
const showClearButton = computed(() => hasText.value)
// Initialize autocomplete with direct ref access
useAutocomplete(
@@ -49,17 +68,60 @@ useAutocomplete(
{ showPreview: props.showPreview ?? true }
)
const updateHasTextState = () => {
hasText.value = textareaRef.value ? textareaRef.value.value.length > 0 : false
}
const onInput = () => {
// Update hasText state
updateHasTextState()
// Call widget callback when text changes
if (textareaRef.value && typeof props.widget.callback === 'function') {
props.widget.callback(textareaRef.value.value)
}
}
// Handle external value changes (e.g., from "send lora to workflow")
const onExternalValueChange = (event: CustomEvent<{ value: string }>) => {
updateHasTextState()
}
// Setup widget.onSetValue callback for external value changes
const setupWidgetOnSetValue = () => {
if (props.widget) {
props.widget.onSetValue = (value: string) => {
// The DOM value is already set by setValue, just update our state
hasText.value = value.length > 0
}
}
}
const clearText = () => {
if (textareaRef.value) {
textareaRef.value.value = ''
hasText.value = false
textareaRef.value.focus()
// Trigger callback with empty value
if (typeof props.widget.callback === 'function') {
props.widget.callback('')
}
// Dispatch input event to ensure autocomplete handles the change
textareaRef.value.dispatchEvent(new Event('input'))
}
}
onMounted(() => {
// Register textarea reference with widget
if (textareaRef.value) {
props.widget.inputEl = textareaRef.value
// Initialize hasText state
hasText.value = textareaRef.value.value.length > 0
// Listen for external value change events from setValue
textareaRef.value.addEventListener('lora-manager:autocomplete-value-changed', onExternalValueChange as EventListener)
}
// Setup callback for input changes
@@ -67,6 +129,9 @@ onMounted(() => {
props.widget.callback(textareaRef.value.value)
}
// Setup widget.onSetValue callback
setupWidgetOnSetValue()
// Listen for custom event dispatched by main.ts
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
})
@@ -76,6 +141,16 @@ onUnmounted(() => {
if (props.widget.inputEl === textareaRef.value) {
props.widget.inputEl = undefined
}
// Remove external value change event listener
if (textareaRef.value) {
textareaRef.value.removeEventListener('lora-manager:autocomplete-value-changed', onExternalValueChange as EventListener)
}
// Clean up onSetValue callback
if (props.widget) {
props.widget.onSetValue = undefined
}
// Remove event listener
document.removeEventListener('lora-manager:vue-mode-change', onModeChange)
@@ -91,6 +166,13 @@ onUnmounted(() => {
box-sizing: border-box;
}
.input-wrapper {
position: relative;
flex: 1;
display: flex;
width: 100%;
}
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */
.text-input {
flex: 1;
@@ -122,4 +204,54 @@ onUnmounted(() => {
.text-input:focus {
outline: none;
}
/* Clear button styles */
.clear-button {
position: absolute;
right: 4px;
top: 4px;
width: 18px;
height: 18px;
padding: 0;
margin: 0;
border: none;
border-radius: 50%;
background: rgba(128, 128, 128, 0.5);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s ease, background-color 0.2s ease;
z-index: 10;
}
.clear-button:hover {
opacity: 1;
background: rgba(255, 100, 100, 0.8);
}
.clear-button svg {
width: 12px;
height: 12px;
}
/* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button {
right: 8px;
top: 8px;
width: 20px;
height: 20px;
background: rgba(107, 114, 128, 0.6);
}
.text-input.vue-dom-mode ~ .clear-button:hover {
background: oklch(62% 0.18 25);
}
.text-input.vue-dom-mode ~ .clear-button svg {
width: 14px;
height: 14px;
}
</style>

View File

@@ -416,6 +416,14 @@ function createAutocompleteTextWidgetFactory(
setValue(v: string) {
if (widget.inputEl) {
widget.inputEl.value = v ?? ''
// Notify Vue component of value change via custom event
widget.inputEl.dispatchEvent(new CustomEvent('lora-manager:autocomplete-value-changed', {
detail: { value: v ?? '' }
}))
}
// Also call onSetValue if defined (for Vue component integration)
if (typeof widget.onSetValue === 'function') {
widget.onSetValue(v ?? '')
}
},
serialize: true,