fix(ui): make autocomplete text widget scrollable in Nodes 2.0 mode

In Vue/Node 2.0 mode, the AutocompleteTextWidget's textarea wheel events were intercepted by TransformPane @wheel.capture before reaching the @wheel handler, causing canvas zoom instead of text scrolling.

- Add lm-wheel-scrollable class in Vue mode to hook into the window capture-phase handler (enableListWheelScroll) which scrolls the textarea manually before TransformPane can react.
- Add maxHeight prop and container max-height for Lora Loader/Stacker/WanVideo nodes (modelType === 'loras'), matching canvas mode's height cap. Prompt/Text nodes remain uncapped.
This commit is contained in:
Will Miao
2026-06-06 08:12:09 +08:00
parent d9ee9b3155
commit dd5b213adc
4 changed files with 36 additions and 20 deletions

View File

@@ -5,7 +5,8 @@
ref="textareaRef" ref="textareaRef"
:placeholder="placeholder" :placeholder="placeholder"
:spellcheck="spellcheck ?? false" :spellcheck="spellcheck ?? false"
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]" :class="['text-input', { 'vue-dom-mode': isVueDomMode, 'lm-wheel-scrollable': isVueDomMode }]"
:style="maxHeight && isVueDomMode ? { maxHeight: maxHeight + 'px' } : undefined"
@input="onInput" @input="onInput"
@wheel="onWheel" @wheel="onWheel"
/> />
@@ -47,6 +48,7 @@ const props = defineProps<{
placeholder?: string placeholder?: string
showPreview?: boolean showPreview?: boolean
spellcheck?: boolean spellcheck?: boolean
maxHeight?: number
}>() }>()
// Reactive ref for Vue DOM mode // Reactive ref for Vue DOM mode

View File

@@ -655,13 +655,16 @@ function createAutocompleteTextWidgetFactory(
// Get spellcheck setting from ComfyUI settings (default: false) // Get spellcheck setting from ComfyUI settings (default: false)
const spellcheck = app.ui?.settings?.getSettingValue?.('Comfy.TextareaWidget.Spellcheck') ?? false const spellcheck = app.ui?.settings?.getSettingValue?.('Comfy.TextareaWidget.Spellcheck') ?? false
const maxHeight = modelType === 'loras' ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : undefined
const vueApp = createApp(AutocompleteTextWidget, { const vueApp = createApp(AutocompleteTextWidget, {
widget, widget,
node, node,
modelType, modelType,
placeholder: inputOptions.placeholder || widgetName, placeholder: inputOptions.placeholder || widgetName,
showPreview: true, showPreview: true,
spellcheck spellcheck,
maxHeight
}) })
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
@@ -673,6 +676,10 @@ function createAutocompleteTextWidgetFactory(
const appKey = instanceId const appKey = instanceId
vueApps.set(appKey, vueApp) vueApps.set(appKey, vueApp)
if (maxHeight) {
container.style.maxHeight = `${maxHeight}px`
}
widget.onRemove = createVueWidgetCleanup(vueApp, () => { widget.onRemove = createVueWidgetCleanup(vueApp, () => {
vueApps.delete(appKey) vueApps.delete(appKey)
}) })

View File

@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-5514bf46] { .autocomplete-text-widget[data-v-1c610e5d] {
background: transparent; background: transparent;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
.input-wrapper[data-v-5514bf46] { .input-wrapper[data-v-1c610e5d] {
position: relative; position: relative;
flex: 1; flex: 1;
display: flex; display: flex;
@@ -2133,7 +2133,7 @@ to { transform: rotate(360deg);
} }
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */ /* Canvas mode styles (default) - matches built-in comfy-multiline-input */
.text-input[data-v-5514bf46] { .text-input[data-v-1c610e5d] {
flex: 1; flex: 1;
width: 100%; width: 100%;
background-color: var(--comfy-input-bg, #222); background-color: var(--comfy-input-bg, #222);
@@ -2150,7 +2150,7 @@ to { transform: rotate(360deg);
} }
/* 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-5514bf46] { .text-input.vue-dom-mode[data-v-1c610e5d] {
background-color: var(--color-charcoal-400, #313235); background-color: var(--color-charcoal-400, #313235);
color: #fff; color: #fff;
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */ padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
@@ -2159,12 +2159,12 @@ to { transform: rotate(360deg);
font-size: 12px; font-size: 12px;
font-family: inherit; font-family: inherit;
} }
.text-input[data-v-5514bf46]:focus { .text-input[data-v-1c610e5d]:focus {
outline: none; outline: none;
} }
/* Clear button styles */ /* Clear button styles */
.clear-button[data-v-5514bf46] { .clear-button[data-v-1c610e5d] {
position: absolute; position: absolute;
right: 6px; right: 6px;
bottom: 6px; /* Changed from top to bottom */ bottom: 6px; /* Changed from top to bottom */
@@ -2187,31 +2187,31 @@ to { transform: rotate(360deg);
} }
/* Show clear button when hovering over input wrapper */ /* Show clear button when hovering over input wrapper */
.input-wrapper:hover .clear-button[data-v-5514bf46] { .input-wrapper:hover .clear-button[data-v-1c610e5d] {
opacity: 0.7; opacity: 0.7;
pointer-events: auto; pointer-events: auto;
} }
.clear-button[data-v-5514bf46]:hover { .clear-button[data-v-1c610e5d]:hover {
opacity: 1; opacity: 1;
background: rgba(255, 100, 100, 0.8); background: rgba(255, 100, 100, 0.8);
} }
.clear-button svg[data-v-5514bf46] { .clear-button svg[data-v-1c610e5d] {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
/* Vue DOM mode adjustments for clear button */ /* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button[data-v-5514bf46] { .text-input.vue-dom-mode ~ .clear-button[data-v-1c610e5d] {
right: 8px; right: 8px;
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */ bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
width: 20px; width: 20px;
height: 20px; height: 20px;
background: rgba(107, 114, 128, 0.6); background: rgba(107, 114, 128, 0.6);
} }
.text-input.vue-dom-mode ~ .clear-button[data-v-5514bf46]:hover { .text-input.vue-dom-mode ~ .clear-button[data-v-1c610e5d]:hover {
background: oklch(62% 0.18 25); background: oklch(62% 0.18 25);
} }
.text-input.vue-dom-mode ~ .clear-button svg[data-v-5514bf46] { .text-input.vue-dom-mode ~ .clear-button svg[data-v-1c610e5d] {
width: 14px; width: 14px;
height: 14px; height: 14px;
}`)); }`));
@@ -14783,7 +14783,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
modelType: {}, modelType: {},
placeholder: {}, placeholder: {},
showPreview: { type: Boolean }, showPreview: { type: Boolean },
spellcheck: { type: Boolean } spellcheck: { type: Boolean },
maxHeight: {}
}, },
setup(__props) { setup(__props) {
const props = __props; const props = __props;
@@ -14913,10 +14914,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
ref: textareaRef, ref: textareaRef,
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, "lm-wheel-scrollable": isVueDomMode.value }]),
style: normalizeStyle(__props.maxHeight && isVueDomMode.value ? { maxHeight: __props.maxHeight + "px" } : void 0),
onInput, onInput,
onWheel onWheel
}, null, 42, _hoisted_3), }, null, 46, _hoisted_3),
showClearButton.value ? (openBlock(), createElementBlock("button", { showClearButton.value ? (openBlock(), createElementBlock("button", {
key: 0, key: 0,
type: "button", type: "button",
@@ -14949,7 +14951,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5514bf46"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-1c610e5d"]]);
function createVueWidgetCleanup(vueApp, onCleanup) { function createVueWidgetCleanup(vueApp, onCleanup) {
let didUnmount = false; let didUnmount = false;
return () => { return () => {
@@ -15799,13 +15801,15 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
); );
widget.metadataWidget = metadataWidget; widget.metadataWidget = metadataWidget;
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$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false;
const maxHeight = modelType === "loras" ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : void 0;
const vueApp = createApp(AutocompleteTextWidget, { const vueApp = createApp(AutocompleteTextWidget, {
widget, widget,
node, node,
modelType, modelType,
placeholder: inputOptions.placeholder || widgetName, placeholder: inputOptions.placeholder || widgetName,
showPreview: true, showPreview: true,
spellcheck spellcheck,
maxHeight
}); });
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
unstyled: true, unstyled: true,
@@ -15814,6 +15818,9 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
vueApp.mount(container); vueApp.mount(container);
const appKey = instanceId; const appKey = instanceId;
vueApps.set(appKey, vueApp); vueApps.set(appKey, vueApp);
if (maxHeight) {
container.style.maxHeight = `${maxHeight}px`;
}
widget.onRemove = createVueWidgetCleanup(vueApp, () => { widget.onRemove = createVueWidgetCleanup(vueApp, () => {
vueApps.delete(appKey); vueApps.delete(appKey);
}); });

File diff suppressed because one or more lines are too long