mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-09 12:39:23 -03:00
fix(ui): make Lora Loader list scrollable in Nodes 2.0 mode
In Nodes 2.0 / Vue node mode the Lora Loader list could not be capped and the node grew to show every row, unlike classic mode which fixes the list area to 12 rows. The Vue layout engine measures the rendered DOM, so CSS variables and computeLayoutSize alone were ignored. - Physically cap the container via max-height so the rendered element is bounded to the 12-row height; extra rows scroll (overflow: auto). - Report the capped height through computeSize / computeLayoutSize / getHeight / getMinHeight so the node background matches the list. - Add enableListWheelScroll: a window capture-phase wheel hook that scrolls the hovered list instead of letting ComfyUI zoom the canvas, which fires on the document/canvas in capture and beat a container-level listener. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user