fix(autocomplete): fix value persistence in DOM text widgets

Remove multiple sources of truth and async sync chains that caused
values to be lost during load/switch workflow or reload page.

Changes:
- Remove internalValue state variable from main.ts
- Update getValue/setValue to read/write DOM directly via widget.inputEl
- Remove textValue reactive ref and v-model from Vue component
- Remove serializeValue, onSetValue, and watch callbacks
- Register textarea reference on mount, clean up on unmount
- Simplify AutocompleteTextWidgetInterface

Follows ComfyUI built-in addMultilineWidget pattern:
- Single source of truth (DOM element value only)
- Direct sync (no intermediate variables or async chains)

Also adds documentation:
- docs/dom-widgets/value-persistence-best-practices.md
- docs/dom-widgets/README.md
- Update docs/dom_widget_dev_guide.md with reference
This commit is contained in:
Will Miao
2026-01-26 23:22:37 +08:00
parent 7249c9fd4b
commit 9032226724
7 changed files with 260 additions and 119 deletions

View File

@@ -0,0 +1,28 @@
# DOM Widgets Documentation
Documentation for custom DOM widget development in ComfyUI LoRA Manager.
## Files
- **[Value Persistence Best Practices](value-persistence-best-practices.md)** - Essential guide for implementing text input DOM widgets that persist values correctly
## Key Lessons
### Common Anti-Patterns
**Don't**: Create internal state variables
**Don't**: Use v-model for text inputs
**Don't**: Add serializeValue, onSetValue callbacks
**Don't**: Watch props.widget.value
### Best Practices
**Do**: Use DOM element as single source of truth
**Do**: Store DOM reference on widget.inputEl
**Do**: Direct getValue/setValue to DOM
**Do**: Clean up reference on unmount
## Related Documentation
- [DOM Widget Development Guide](../dom_widget_dev_guide.md) - Comprehensive guide for building DOM widgets
- [ComfyUI Built-in Example](../../../../code/ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/composables/useStringWidget.ts) - Reference implementation

View File

@@ -0,0 +1,144 @@
# DOM Widget Value Persistence - Best Practices
## Problem
DOM widgets with text inputs failed to persist values after:
- Loading workflows
- Switching workflows
- Reloading pages
## Root Cause
**Multiple sources of truth** causing sync issues:
- Internal state variable (`internalValue` in main.ts)
- Vue reactive ref (`textValue` in component)
- DOM element value (actual textarea)
- ComfyUI widget value (`props.widget.value`)
**Broken sync chains:**
```
getValue() → internalValue (not actual DOM value)
setValue(v) → internalValue → onSetValue() → textValue.value (async chain)
serializeValue() → textValue.value (different from getValue)
watch() → another sync layer
```
## Solution
Follow ComfyUI built-in `addMultilineWidget` pattern:
### ✅ Do
1. **Single source of truth**: Use the DOM element directly
```typescript
// main.ts
const widget = node.addDOMWidget(name, type, container, {
getValue() {
return widget.inputEl?.value ?? ''
},
setValue(v: string) {
if (widget.inputEl) {
widget.inputEl.value = v ?? ''
}
}
})
```
2. **Register DOM reference** when component mounts
```typescript
// Vue component
onMounted(() => {
if (textareaRef.value) {
props.widget.inputEl = textareaRef.value
}
})
```
3. **Clean up reference** on unmount
```typescript
onUnmounted(() => {
if (props.widget.inputEl === textareaRef.value) {
props.widget.inputEl = undefined
}
})
```
4. **Simplify interface** - only expose what's needed
```typescript
export interface MyWidgetInterface {
inputEl?: HTMLTextAreaElement
callback?: (v: string) => void
}
```
### ❌ Don't
1. **Don't create internal state variables**
```typescript
// Wrong
let internalValue = ''
getValue() { return internalValue }
```
2. **Don't use v-model** for text inputs in DOM widgets
```html
<!-- Wrong -->
<textarea v-model="textValue" />
<!-- Right -->
<textarea ref="textareaRef" @input="onInput" />
```
3. **Don't add serializeValue** - getValue/setValue handle it
```typescript
// Wrong
props.widget.serializeValue = async () => textValue.value
```
4. **Don't add onSetValue** callback
```typescript
// Wrong
setValue(v: string) {
internalValue = v
widget.onSetValue?.(v) // Unnecessary layer
}
```
5. **Don't watch props.widget.value** - creates race conditions
```typescript
// Wrong
watch(() => props.widget.value, (newValue) => {
textValue.value = newValue
})
```
6. **Don't restore from props.widget.value** in onMounted
```typescript
// Wrong
onMounted(() => {
if (props.widget.value) {
textValue.value = props.widget.value
}
})
```
## Key Principles
1. **One source of truth**: DOM element value only
2. **Direct sync**: getValue/setValue read/write DOM directly
3. **No async chains**: Eliminate intermediate variables
4. **Match built-in patterns**: Study ComfyUI's `addMultilineWidget` implementation
5. **Minimal interface**: Only expose `inputEl` and `callback`
## Testing Checklist
- [ ] Load workflow - value restores correctly
- [ ] Switch workflow - value persists
- [ ] Reload page - value persists
- [ ] Type in widget - callback fires
- [ ] No console errors
## References
- ComfyUI built-in: `/home/miao/code/ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/composables/useStringWidget.ts`
- Example fix: `vue-widgets/src/components/AutocompleteTextWidget.vue` (after fix)

View File

@@ -240,6 +240,8 @@ inputEl.addEventListener("change", () => {
}); });
``` ```
> **⚠️ Important**: For Vue-based DOM widgets with text inputs, follow the [Value Persistence Best Practices](dom-widgets/value-persistence-best-practices.md) to avoid sync issues. Key takeaway: use DOM element as single source of truth, avoid internal state variables and v-model.
### 5.3 The Restoration Mechanism (`configure`) ### 5.3 The Restoration Mechanism (`configure`)
* **`configure(data)`**: When a Workflow is loaded, `LGraphNode` calls its `configure(data)` method. * **`configure(data)`**: When a Workflow is loaded, `LGraphNode` calls its `configure(data)` method.

View File

@@ -2,7 +2,6 @@
<div class="autocomplete-text-widget"> <div class="autocomplete-text-widget">
<textarea <textarea
ref="textareaRef" ref="textareaRef"
v-model="textValue"
:placeholder="placeholder" :placeholder="placeholder"
:spellcheck="spellcheck ?? false" :spellcheck="spellcheck ?? false"
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]" :class="['text-input', { 'vue-dom-mode': isVueDomMode }]"
@@ -15,16 +14,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { useAutocomplete } from '@/composables/useAutocomplete' import { useAutocomplete } from '@/composables/useAutocomplete'
// Access LiteGraph global for initial mode detection // Access LiteGraph global for initial mode detection
declare const LiteGraph: { vueNodesMode?: boolean } | undefined declare const LiteGraph: { vueNodesMode?: boolean } | undefined
export interface AutocompleteTextWidgetInterface { export interface AutocompleteTextWidgetInterface {
serializeValue?: () => Promise<string> inputEl?: HTMLTextAreaElement
value?: string
onSetValue?: (v: string) => void
callback?: (v: string) => void callback?: (v: string) => void
} }
@@ -46,20 +43,10 @@ const onModeChange = (event: Event) => {
isVueDomMode.value = customEvent.detail.isVueDomMode isVueDomMode.value = customEvent.detail.isVueDomMode
} }
onMounted(() => {
// Listen for custom event dispatched by main.ts
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
})
onUnmounted(() => {
document.removeEventListener('lora-manager:vue-mode-change', onModeChange)
})
const textareaRef = ref<HTMLTextAreaElement | null>(null) const textareaRef = ref<HTMLTextAreaElement | null>(null)
const textValue = ref('')
// Initialize autocomplete with direct ref access // Initialize autocomplete with direct ref access
const { isInitialized } = useAutocomplete( useAutocomplete(
textareaRef, textareaRef,
props.modelType ?? 'loras', props.modelType ?? 'loras',
{ showPreview: props.showPreview ?? true } { showPreview: props.showPreview ?? true }
@@ -67,37 +54,35 @@ const { isInitialized } = useAutocomplete(
const onInput = () => { const onInput = () => {
// Call widget callback when text changes // Call widget callback when text changes
if (typeof props.widget.callback === 'function') { if (textareaRef.value && typeof props.widget.callback === 'function') {
props.widget.callback(textValue.value) props.widget.callback(textareaRef.value.value)
} }
} }
onMounted(() => { onMounted(() => {
// Setup serialization // Register textarea reference with widget
props.widget.serializeValue = async () => textValue.value if (textareaRef.value) {
props.widget.inputEl = textareaRef.value
// Handle external value updates (e.g., loading workflow, paste)
props.widget.onSetValue = (v: string) => {
if (v !== textValue.value) {
textValue.value = v ?? ''
}
} }
// Restore from saved value if exists // Setup callback for input changes
if (props.widget.value !== undefined && props.widget.value !== null) { if (textareaRef.value && typeof props.widget.callback === 'function') {
textValue.value = props.widget.value props.widget.callback(textareaRef.value.value)
} }
// Listen for custom event dispatched by main.ts
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
}) })
// Watch for external value changes and sync onUnmounted(() => {
watch( // Clean up textarea reference
() => props.widget.value, if (props.widget.inputEl === textareaRef.value) {
(newValue) => { props.widget.inputEl = undefined
if (newValue !== undefined && newValue !== textValue.value) {
textValue.value = newValue ?? ''
} }
}
) // Remove event listener
document.removeEventListener('lora-manager:vue-mode-change', onModeChange)
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -423,20 +423,17 @@ function createAutocompleteTextWidgetFactory(
forwardMiddleMouseToCanvas(container) forwardMiddleMouseToCanvas(container)
let internalValue = ''
const widget = node.addDOMWidget( const widget = node.addDOMWidget(
widgetName, widgetName,
`AUTOCOMPLETE_TEXT_${modelType.toUpperCase()}`, `AUTOCOMPLETE_TEXT_${modelType.toUpperCase()}`,
container, container,
{ {
getValue() { getValue() {
return internalValue return widget.inputEl?.value ?? ''
}, },
setValue(v: string) { setValue(v: string) {
internalValue = v ?? '' if (widget.inputEl) {
if (typeof widget.onSetValue === 'function') { widget.inputEl.value = v ?? ''
widget.onSetValue(v)
} }
}, },
serialize: true, serialize: true,

View File

@@ -1725,7 +1725,7 @@ to {
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-d5278afc] { .autocomplete-text-widget[data-v-82697d49] {
background: transparent; background: transparent;
height: 100%; height: 100%;
display: flex; display: flex;
@@ -1734,7 +1734,7 @@ to {
} }
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */ /* Canvas mode styles (default) - matches built-in comfy-multiline-input */
.text-input[data-v-d5278afc] { .text-input[data-v-82697d49] {
flex: 1; flex: 1;
width: 100%; width: 100%;
background-color: var(--comfy-input-bg, #222); background-color: var(--comfy-input-bg, #222);
@@ -1751,7 +1751,7 @@ to {
} }
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */ /* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
.text-input.vue-dom-mode[data-v-d5278afc] { .text-input.vue-dom-mode[data-v-82697d49] {
background-color: var(--color-charcoal-400, #313235); background-color: var(--color-charcoal-400, #313235);
color: #fff; color: #fff;
padding: 8px 12px; padding: 8px 12px;
@@ -1760,7 +1760,7 @@ to {
font-size: 12px; font-size: 12px;
font-family: inherit; font-family: inherit;
} }
.text-input[data-v-d5278afc]:focus { .text-input[data-v-82697d49]:focus {
outline: none; outline: none;
}`)); }`));
document.head.appendChild(elementStyle); document.head.appendChild(elementStyle);
@@ -1770,7 +1770,7 @@ to {
} }
})(); })();
var _a; var _a;
import { app as app$1 } from "../../../scripts/app.js"; import { app } from "../../../scripts/app.js";
/** /**
* @vue/shared v3.5.26 * @vue/shared v3.5.26
* (c) 2018-present Yuxi (Evan) You and Vue contributors * (c) 2018-present Yuxi (Evan) You and Vue contributors
@@ -3317,7 +3317,7 @@ function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher
cleanups.push(cleanupFn); cleanups.push(cleanupFn);
} }
} }
function watch$1(source, cb, options = EMPTY_OBJ) { function watch$2(source, cb, options = EMPTY_OBJ) {
const { immediate, deep, once, scheduler, augmentJob, call } = options; const { immediate, deep, once, scheduler, augmentJob, call } = options;
const reactiveGetter = (source2) => { const reactiveGetter = (source2) => {
if (deep) return source2; if (deep) return source2;
@@ -3914,7 +3914,7 @@ const useSSRContext = () => {
return ctx; return ctx;
} }
}; };
function watch(source, cb, options) { function watch$1(source, cb, options) {
return doWatch(source, cb, options); return doWatch(source, cb, options);
} }
function doWatch(source, cb, options = EMPTY_OBJ) { function doWatch(source, cb, options = EMPTY_OBJ) {
@@ -3964,7 +3964,7 @@ function doWatch(source, cb, options = EMPTY_OBJ) {
} }
} }
}; };
const watchHandle = watch$1(source, cb, baseWatchOptions); const watchHandle = watch$2(source, cb, baseWatchOptions);
if (isInSSRComponentSetup) { if (isInSSRComponentSetup) {
if (ssrCleanup) { if (ssrCleanup) {
ssrCleanup.push(watchHandle); ssrCleanup.push(watchHandle);
@@ -5319,12 +5319,12 @@ function createWatcher(raw, ctx, publicThis, key) {
const handler = ctx[raw]; const handler = ctx[raw];
if (isFunction(handler)) { if (isFunction(handler)) {
{ {
watch(getter, handler); watch$1(getter, handler);
} }
} }
} else if (isFunction(raw)) { } else if (isFunction(raw)) {
{ {
watch(getter, raw.bind(publicThis)); watch$1(getter, raw.bind(publicThis));
} }
} else if (isObject(raw)) { } else if (isObject(raw)) {
if (isArray(raw)) { if (isArray(raw)) {
@@ -5332,7 +5332,7 @@ function createWatcher(raw, ctx, publicThis, key) {
} else { } else {
const handler = isFunction(raw.handler) ? raw.handler.bind(publicThis) : ctx[raw.handler]; const handler = isFunction(raw.handler) ? raw.handler.bind(publicThis) : ctx[raw.handler];
if (isFunction(handler)) { if (isFunction(handler)) {
watch(getter, handler, raw); watch$1(getter, handler, raw);
} }
} }
} else ; } else ;
@@ -9649,7 +9649,7 @@ function useStyle(css3) {
onStyleMounted === null || onStyleMounted === void 0 || onStyleMounted(_name); onStyleMounted === null || onStyleMounted === void 0 || onStyleMounted(_name);
} }
if (isLoaded.value) return; if (isLoaded.value) return;
stop = watch(cssRef, function(value) { stop = watch$1(cssRef, function(value) {
styleRef.value.textContent = value; styleRef.value.textContent = value;
onStyleUpdated === null || onStyleUpdated === void 0 || onStyleUpdated(_name); onStyleUpdated === null || onStyleUpdated === void 0 || onStyleUpdated(_name);
}, { }, {
@@ -10104,7 +10104,7 @@ function setupConfig(app2, PrimeVue2) {
isThemeChanged.value = true; isThemeChanged.value = true;
} }
}); });
var stopConfigWatcher = watch(PrimeVue2.config, function(newValue, oldValue) { var stopConfigWatcher = watch$1(PrimeVue2.config, function(newValue, oldValue) {
PrimeVueService.emit("config:change", { PrimeVueService.emit("config:change", {
newValue, newValue,
oldValue oldValue
@@ -10113,7 +10113,7 @@ function setupConfig(app2, PrimeVue2) {
immediate: true, immediate: true,
deep: true deep: true
}); });
var stopRippleWatcher = watch(function() { var stopRippleWatcher = watch$1(function() {
return PrimeVue2.config.ripple; return PrimeVue2.config.ripple;
}, function(newValue, oldValue) { }, function(newValue, oldValue) {
PrimeVueService.emit("config:ripple:change", { PrimeVueService.emit("config:ripple:change", {
@@ -10124,7 +10124,7 @@ function setupConfig(app2, PrimeVue2) {
immediate: true, immediate: true,
deep: true deep: true
}); });
var stopThemeWatcher = watch(function() { var stopThemeWatcher = watch$1(function() {
return PrimeVue2.config.theme; return PrimeVue2.config.theme;
}, function(newValue, oldValue) { }, function(newValue, oldValue) {
if (!isThemeChanged.value) { if (!isThemeChanged.value) {
@@ -10142,7 +10142,7 @@ function setupConfig(app2, PrimeVue2) {
immediate: true, immediate: true,
deep: false deep: false
}); });
var stopUnstyledWatcher = watch(function() { var stopUnstyledWatcher = watch$1(function() {
return PrimeVue2.config.unstyled; return PrimeVue2.config.unstyled;
}, function(newValue, oldValue) { }, function(newValue, oldValue) {
if (!newValue && PrimeVue2.config.theme) { if (!newValue && PrimeVue2.config.theme) {
@@ -10730,7 +10730,7 @@ const _sfc_main$e = /* @__PURE__ */ defineComponent({
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener("keydown", handleKeydown); document.removeEventListener("keydown", handleKeydown);
}); });
watch(() => props.visible, (isVisible) => { watch$1(() => props.visible, (isVisible) => {
if (isVisible) { if (isVisible) {
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
} else { } else {
@@ -10830,7 +10830,7 @@ const _sfc_main$d = /* @__PURE__ */ defineComponent({
searchQuery.value = ""; searchQuery.value = "";
(_a2 = searchInputRef.value) == null ? void 0 : _a2.focus(); (_a2 = searchInputRef.value) == null ? void 0 : _a2.focus();
}; };
watch(() => props.visible, (isVisible) => { watch$1(() => props.visible, (isVisible) => {
if (isVisible) { if (isVisible) {
nextTick(() => { nextTick(() => {
var _a2; var _a2;
@@ -10957,7 +10957,7 @@ const _sfc_main$c = /* @__PURE__ */ defineComponent({
searchQuery.value = ""; searchQuery.value = "";
(_a2 = searchInputRef.value) == null ? void 0 : _a2.focus(); (_a2 = searchInputRef.value) == null ? void 0 : _a2.focus();
}; };
watch(() => props.visible, (isVisible) => { watch$1(() => props.visible, (isVisible) => {
if (isVisible) { if (isVisible) {
nextTick(() => { nextTick(() => {
var _a2; var _a2;
@@ -11192,7 +11192,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
const newSelected = props.selected.includes(key) ? props.selected.filter((k2) => k2 !== key) : [...props.selected, key]; const newSelected = props.selected.includes(key) ? props.selected.filter((k2) => k2 !== key) : [...props.selected, key];
emit2("update:selected", newSelected); emit2("update:selected", newSelected);
}; };
watch(() => props.visible, (isVisible) => { watch$1(() => props.visible, (isVisible) => {
if (isVisible) { if (isVisible) {
searchQuery.value = ""; searchQuery.value = "";
expandedKeys.value = /* @__PURE__ */ new Set(); expandedKeys.value = /* @__PURE__ */ new Set();
@@ -11452,7 +11452,7 @@ function useLoraPoolState(widget) {
refreshPreview(); refreshPreview();
}, 300); }, 300);
}; };
watch([ watch$1([
selectedBaseModels, selectedBaseModels,
includeTags, includeTags,
excludeTags, excludeTags,
@@ -12473,7 +12473,7 @@ function useLoraRandomizerState(widget) {
}; };
const isClipStrengthDisabled = computed(() => !useCustomClipRange.value); const isClipStrengthDisabled = computed(() => !useCustomClipRange.value);
const isRecommendedStrengthEnabled = computed(() => useRecommendedStrength.value); const isRecommendedStrengthEnabled = computed(() => useRecommendedStrength.value);
watch([ watch$1([
countMode, countMode,
countFixed, countFixed,
countMin, countMin,
@@ -12598,7 +12598,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
state.rollMode.value = "fixed"; state.rollMode.value = "fixed";
} }
}; };
watch(() => { watch$1(() => {
var _a2, _b; var _a2, _b;
return (_b = (_a2 = props.node.widgets) == null ? void 0 : _a2.find((w2) => w2.name === "loras")) == null ? void 0 : _b.value; return (_b = (_a2 = props.node.widgets) == null ? void 0 : _a2.find((w2) => w2.name === "loras")) == null ? void 0 : _b.value;
}, (newVal) => { }, (newVal) => {
@@ -13000,12 +13000,12 @@ function useLoraCyclerState(widget) {
} }
}; };
const isClipStrengthDisabled = computed(() => !useCustomClipRange.value); const isClipStrengthDisabled = computed(() => !useCustomClipRange.value);
watch(modelStrength, (newValue) => { watch$1(modelStrength, (newValue) => {
if (!useCustomClipRange.value) { if (!useCustomClipRange.value) {
clipStrength.value = newValue; clipStrength.value = newValue;
} }
}); });
watch([ watch$1([
currentIndex, currentIndex,
totalCount, totalCount,
poolConfigHash, poolConfigHash,
@@ -13396,67 +13396,53 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
const customEvent = event; const customEvent = event;
isVueDomMode.value = customEvent.detail.isVueDomMode; isVueDomMode.value = customEvent.detail.isVueDomMode;
}; };
onMounted(() => {
document.addEventListener("lora-manager:vue-mode-change", onModeChange);
});
onUnmounted(() => {
document.removeEventListener("lora-manager:vue-mode-change", onModeChange);
});
const textareaRef = ref(null); const textareaRef = ref(null);
const textValue = ref("");
useAutocomplete( useAutocomplete(
textareaRef, textareaRef,
props.modelType ?? "loras", props.modelType ?? "loras",
{ showPreview: props.showPreview ?? true } { showPreview: props.showPreview ?? true }
); );
const onInput = () => { const onInput = () => {
if (typeof props.widget.callback === "function") { if (textareaRef.value && typeof props.widget.callback === "function") {
props.widget.callback(textValue.value); props.widget.callback(textareaRef.value.value);
} }
}; };
onMounted(() => { onMounted(() => {
props.widget.serializeValue = async () => textValue.value; if (textareaRef.value) {
props.widget.onSetValue = (v2) => { props.widget.inputEl = textareaRef.value;
if (v2 !== textValue.value) {
textValue.value = v2 ?? "";
} }
}; if (textareaRef.value && typeof props.widget.callback === "function") {
if (props.widget.value !== void 0 && props.widget.value !== null) { props.widget.callback(textareaRef.value.value);
textValue.value = props.widget.value;
} }
document.addEventListener("lora-manager:vue-mode-change", onModeChange);
}); });
watch( onUnmounted(() => {
() => props.widget.value, if (props.widget.inputEl === textareaRef.value) {
(newValue) => { props.widget.inputEl = void 0;
if (newValue !== void 0 && newValue !== textValue.value) {
textValue.value = newValue ?? "";
} }
} document.removeEventListener("lora-manager:vue-mode-change", onModeChange);
); });
return (_ctx, _cache) => { return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", _hoisted_1, [ return openBlock(), createElementBlock("div", _hoisted_1, [
withDirectives(createBaseVNode("textarea", { createBaseVNode("textarea", {
ref_key: "textareaRef", ref_key: "textareaRef",
ref: textareaRef, ref: textareaRef,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => textValue.value = $event),
placeholder: __props.placeholder, placeholder: __props.placeholder,
spellcheck: __props.spellcheck ?? false, spellcheck: __props.spellcheck ?? false,
class: normalizeClass(["text-input", { "vue-dom-mode": isVueDomMode.value }]), class: normalizeClass(["text-input", { "vue-dom-mode": isVueDomMode.value }]),
onInput, onInput,
onPointerdown: _cache[1] || (_cache[1] = withModifiers(() => { onPointerdown: _cache[0] || (_cache[0] = withModifiers(() => {
}, ["stop"])), }, ["stop"])),
onPointermove: _cache[2] || (_cache[2] = withModifiers(() => { onPointermove: _cache[1] || (_cache[1] = withModifiers(() => {
}, ["stop"])), }, ["stop"])),
onPointerup: _cache[3] || (_cache[3] = withModifiers(() => { onPointerup: _cache[2] || (_cache[2] = withModifiers(() => {
}, ["stop"])) }, ["stop"]))
}, null, 42, _hoisted_2), [ }, null, 42, _hoisted_2)
[vModelText, textValue.value]
])
]); ]);
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d5278afc"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-82697d49"]]);
const LORA_PROVIDER_NODE_TYPES$1 = [ const LORA_PROVIDER_NODE_TYPES$1 = [
"Lora Stacker (LoraManager)", "Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)", "Lora Randomizer (LoraManager)",
@@ -13519,7 +13505,7 @@ function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCa
updateDownstreamLoaders2(node); updateDownstreamLoaders2(node);
}; };
} }
const app = {}; const app$1 = {};
const ROOT_GRAPH_ID = "root"; const ROOT_GRAPH_ID = "root";
const LORA_PROVIDER_NODE_TYPES = [ const LORA_PROVIDER_NODE_TYPES = [
"Lora Stacker (LoraManager)", "Lora Stacker (LoraManager)",
@@ -13539,7 +13525,7 @@ function getNodeGraphId(node) {
if (!node) { if (!node) {
return ROOT_GRAPH_ID; return ROOT_GRAPH_ID;
} }
return getGraphId(node.graph || app.graph); return getGraphId(node.graph || app$1.graph);
} }
function getNodeReference(node) { function getNodeReference(node) {
if (!node) { if (!node) {
@@ -13740,7 +13726,7 @@ function forwardMiddleMouseToCanvas(container) {
if (!container) return; if (!container) return;
container.addEventListener("pointerdown", (event) => { container.addEventListener("pointerdown", (event) => {
if (event.button === 1) { if (event.button === 1) {
const canvas = app$1.canvas; const canvas = app.canvas;
if (canvas && typeof canvas.processMouseDown === "function") { if (canvas && typeof canvas.processMouseDown === "function") {
canvas.processMouseDown(event); canvas.processMouseDown(event);
} }
@@ -13748,7 +13734,7 @@ function forwardMiddleMouseToCanvas(container) {
}); });
container.addEventListener("pointermove", (event) => { container.addEventListener("pointermove", (event) => {
if ((event.buttons & 4) === 4) { if ((event.buttons & 4) === 4) {
const canvas = app$1.canvas; const canvas = app.canvas;
if (canvas && typeof canvas.processMouseMove === "function") { if (canvas && typeof canvas.processMouseMove === "function") {
canvas.processMouseMove(event); canvas.processMouseMove(event);
} }
@@ -13756,7 +13742,7 @@ function forwardMiddleMouseToCanvas(container) {
}); });
container.addEventListener("pointerup", (event) => { container.addEventListener("pointerup", (event) => {
if (event.button === 1) { if (event.button === 1) {
const canvas = app$1.canvas; const canvas = app.canvas;
if (canvas && typeof canvas.processMouseUp === "function") { if (canvas && typeof canvas.processMouseUp === "function") {
canvas.processMouseUp(event); canvas.processMouseUp(event);
} }
@@ -14015,11 +14001,11 @@ function createJsonDisplayWidget(node) {
const widgetInputOptions = /* @__PURE__ */ new Map(); const widgetInputOptions = /* @__PURE__ */ new Map();
const initVueDomModeListener = () => { const initVueDomModeListener = () => {
var _a2, _b; var _a2, _b;
if ((_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) { if ((_b = (_a2 = app.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) {
app$1.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => { app.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
var _a3, _b2, _c; var _a3, _b2, _c;
const isVueDomMode = ((_c = (_b2 = (_a3 = app$1.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false; const isVueDomMode = ((_c = (_b2 = (_a3 = app.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false;
document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", { document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", {
detail: { isVueDomMode } detail: { isVueDomMode }
})); }));
@@ -14027,12 +14013,12 @@ const initVueDomModeListener = () => {
}); });
} }
}; };
if ((_a = app$1.ui) == null ? void 0 : _a.settings) { if ((_a = app.ui) == null ? void 0 : _a.settings) {
initVueDomModeListener(); initVueDomModeListener();
} else { } else {
const checkAppReady = setInterval(() => { const checkAppReady = setInterval(() => {
var _a2; var _a2;
if ((_a2 = app$1.ui) == null ? void 0 : _a2.settings) { if ((_a2 = app.ui) == null ? void 0 : _a2.settings) {
initVueDomModeListener(); initVueDomModeListener();
clearInterval(checkAppReady); clearInterval(checkAppReady);
} }
@@ -14048,19 +14034,18 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
container.style.flexDirection = "column"; container.style.flexDirection = "column";
container.style.overflow = "hidden"; container.style.overflow = "hidden";
forwardMiddleMouseToCanvas(container); forwardMiddleMouseToCanvas(container);
let internalValue = "";
const widget = node.addDOMWidget( const widget = node.addDOMWidget(
widgetName, widgetName,
`AUTOCOMPLETE_TEXT_${modelType.toUpperCase()}`, `AUTOCOMPLETE_TEXT_${modelType.toUpperCase()}`,
container, container,
{ {
getValue() { getValue() {
return internalValue; var _a3;
return ((_a3 = widget.inputEl) == null ? void 0 : _a3.value) ?? "";
}, },
setValue(v2) { setValue(v2) {
internalValue = v2 ?? ""; if (widget.inputEl) {
if (typeof widget.onSetValue === "function") { widget.inputEl.value = v2 ?? "";
widget.onSetValue(v2);
} }
}, },
serialize: true, serialize: true,
@@ -14074,7 +14059,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
} }
} }
); );
const spellcheck = ((_c = (_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false; const spellcheck = ((_c = (_b = (_a2 = app.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false;
const vueApp = createApp(AutocompleteTextWidget, { const vueApp = createApp(AutocompleteTextWidget, {
widget, widget,
node, node,
@@ -14099,7 +14084,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
}; };
return { widget }; return { widget };
} }
app$1.registerExtension({ app.registerExtension({
name: "LoraManager.VueWidgets", name: "LoraManager.VueWidgets",
getCustomWidgets() { getCustomWidgets() {
return { return {

File diff suppressed because one or more lines are too long