diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js index a6db8a52..18828616 100644 --- a/web/comfyui/loras_widget.js +++ b/web/comfyui/loras_widget.js @@ -5,13 +5,13 @@ import { updateWidgetHeight, shouldShowClipEntry, syncClipStrengthIfCollapsed, - LORA_ENTRY_HEIGHT, - HEADER_HEIGHT, - CONTAINER_PADDING, - EMPTY_CONTAINER_HEIGHT + LORA_ENTRY_HEIGHT, + HEADER_HEIGHT, + CONTAINER_PADDING, + EMPTY_CONTAINER_HEIGHT } from "./loras_widget_utils.js"; import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js"; -import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js"; +import { forwardMiddleMouseToCanvas, forwardWheelToCanvas, enableListWheelScroll } from "./utils.js"; import { PreviewTooltip } from "./preview_tooltip.js"; import { ensureLmStyles } from "./lm_styles_loader.js"; import { getStrengthStepPreference } from "./settings.js"; @@ -24,11 +24,18 @@ export function addLorasWidget(node, name, opts, callback) { container.className = "lm-loras-container"; forwardMiddleMouseToCanvas(container); + // Capture-phase handler: scroll the list with the wheel when it overflows. + // Falls through to forwardWheelToCanvas (canvas zoom) when the list is short. + enableListWheelScroll(container); forwardWheelToCanvas(container); // Set initial height using CSS variables approach const defaultHeight = 200; + // Content height (capped at 12 rows by renderLoras), kept up to date and used + // to fix the widget/node height in both Canvas and Nodes 2.0 (Vue) modes. + let currentContentHeight = defaultHeight; + // Check if this is a randomizer node (lock button instead of drag handle) const isRandomizerNode = opts?.isRandomizerNode === true; @@ -198,7 +205,7 @@ export function addLorasWidget(node, name, opts, callback) { container.appendChild(emptyMessage); // Set fixed height for empty state - updateWidgetHeight(container, EMPTY_CONTAINER_HEIGHT, defaultHeight, node); + currentContentHeight = updateWidgetHeight(container, EMPTY_CONTAINER_HEIGHT, defaultHeight, node); return; } @@ -645,7 +652,7 @@ export function addLorasWidget(node, name, opts, callback) { // Calculate height based on number of loras and fixed sizes const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 12) * LORA_ENTRY_HEIGHT); - updateWidgetHeight(container, calculatedHeight, defaultHeight, node); + currentContentHeight = updateWidgetHeight(container, calculatedHeight, defaultHeight, node); // After all LoRA elements are created, apply selection state as the last step // This ensures the selection state is not overwritten @@ -727,12 +734,31 @@ export function addLorasWidget(node, name, opts, callback) { widgetValue = updatedValue; renderLoras(widgetValue, widget); }, + // The list area is capped at 12 rows (see calculatedHeight); beyond that the + // container scrolls. Report that capped height as both the min and preferred + // size so the node height stays fixed to the list, matching Canvas mode. + getMinHeight: () => currentContentHeight, + getHeight: () => currentContentHeight, hideOnZoom: true, selectOn: ['click', 'focus'] }); widget.value = defaultValue; - + + // Canonical LiteGraph sizing hook (Canvas mode): fix the widget to the capped + // content height. Rows beyond the 12-row cap scroll inside the container. + widget.computeSize = (width) => [width, currentContentHeight]; + + // Nodes 2.0 / Vue mode reads computeLayoutSize for the node's size. Pin both + // the min and max to the capped content height so the list area is fixed to + // 12 rows (scrolling beyond), matching Canvas mode, instead of the layout + // engine measuring the full DOM and locking the node fully expanded. + widget.computeLayoutSize = () => ({ + minHeight: currentContentHeight, + maxHeight: currentContentHeight, + minWidth: 400, + }); + widget.callback = callback; widget.onRemove = () => { diff --git a/web/comfyui/loras_widget_utils.js b/web/comfyui/loras_widget_utils.js index 91484965..dec430fa 100644 --- a/web/comfyui/loras_widget_utils.js +++ b/web/comfyui/loras_widget_utils.js @@ -18,21 +18,28 @@ export function formatLoraValue(loras) { return loras; } -// Function to update widget height consistently +// Resolve the capped (12-row) height of the widget and physically cap the +// container so the list area never grows past it. +// `height` is the raw content height (already capped at 12 rows by the caller); +// the result never drops below defaultHeight. +// The max-height is the reliable lever: Nodes 2.0 / Vue mode measures the +// rendered DOM to size the node, so without an actual height cap on the element +// the list always shows every row. max-height bounds the element regardless of +// what the layout engine measures, and the overflow makes the extra rows scroll. +// Returns the resolved height so callers can also report it to ComfyUI. export function updateWidgetHeight(container, height, defaultHeight, node) { - // Ensure minimum height - const finalHeight = Math.max(defaultHeight, height); - - // Update CSS variables - container.style.setProperty('--comfy-widget-min-height', `${finalHeight}px`); - container.style.setProperty('--comfy-widget-height', `${finalHeight}px`); - - // Force node to update size after a short delay to ensure DOM is updated + const cappedHeight = Math.max(defaultHeight, height); + + container.style.maxHeight = `${cappedHeight}px`; + + // Force node to redraw after a short delay to ensure the DOM is updated. if (node) { setTimeout(() => { node.setDirtyCanvas(true, true); }, 10); } + + return cappedHeight; } // Determine if clip entry should be shown - now based on expanded property or initial diff values diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js index 34d3acb1..86963e88 100644 --- a/web/comfyui/utils.js +++ b/web/comfyui/utils.js @@ -784,6 +784,59 @@ export function forwardWheelToCanvas(container, options = {}) { }, { passive: false }); } +// Marks elements whose wheel scrolling must win over the canvas zoom. +const LIST_WHEEL_SCROLL_CLASS = 'lm-wheel-scrollable'; +let listWheelScrollHookInstalled = false; + +/** + * Keep vertical wheel scrolling inside a scrollable widget container instead of + * letting ComfyUI zoom the canvas. + * + * In Nodes 2.0 / Vue mode ComfyUI's wheel→zoom handler runs on the document / + * canvas in the capture phase, which is *outer* than the widget, so a listener + * on the container (even in capture) fires too late. The reliable place to win + * is a single hook on `window` in the capture phase — the very first step of + * event dispatch. When the wheel is over a marked, scrollable element we scroll + * it manually and fully consume the event; otherwise we leave it alone so canvas + * zoom keeps working. + * @param {HTMLElement} container - The scrollable element (overflow: auto) + */ +export function enableListWheelScroll(container) { + if (!container) return; + container.classList.add(LIST_WHEEL_SCROLL_CLASS); + + if (listWheelScrollHookInstalled) return; + listWheelScrollHookInstalled = true; + + window.addEventListener('wheel', (event) => { + // Let pinch/zoom and horizontal gestures fall through to the canvas. + if (event.ctrlKey) return; + if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return; + + const target = event.target; + if (!target || typeof target.closest !== 'function') return; + const el = target.closest(`.${LIST_WHEEL_SCROLL_CLASS}`); + if (!el) return; + + const canScrollY = el.scrollHeight > el.clientHeight + 1; + if (!canScrollY) return; // Nothing to scroll → allow canvas zoom. + + // Translate the wheel delta to pixels (line / page modes → approx px). + const unit = event.deltaMode === 1 + ? 16 + : event.deltaMode === 2 + ? el.clientHeight + : 1; + + el.scrollTop += event.deltaY * unit; + + // Consume the event so neither ComfyUI's zoom nor forwardWheelToCanvas react. + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + }, { capture: true, passive: false }); +} + // Get connected Lora Pool node from pool_config input export function getConnectedPoolConfigNode(node) { if (!node?.inputs) {