fix(ui): cap lora widget height and enable wheel scroll in Node 2.0 mode (#959)

- Add 'Node 2.0: Maximum visible LoRA entries' setting (default 12)
- Apply max-height to loras container in Vue mode to prevent unbounded growth
- Add enableListWheelScroll: window capture-phase wheel hook so scroll
  inside the widget scrolls the list instead of zooming the canvas
This commit is contained in:
Will Miao
2026-06-08 16:19:08 +08:00
parent 4d239008a6
commit e3c812367e
3 changed files with 104 additions and 2 deletions

View File

@@ -11,10 +11,10 @@ import {
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";
import { getStrengthStepPreference, getLoraWidgetMaxVisibleLoras } from "./settings.js";
export function addLorasWidget(node, name, opts, callback) {
ensureLmStyles();
@@ -29,6 +29,20 @@ export function addLorasWidget(node, name, opts, callback) {
// Set initial height using CSS variables approach
const defaultHeight = 200;
// In Vue/node-2.0 mode, cap the widget height so it shows at most N entries.
// This prevents content from driving the node size beyond the cap.
// canvas/legacy mode is unaffected.
if (typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode) {
const maxLoras = getLoraWidgetMaxVisibleLoras();
const gap = 5; // flex gap from .lm-loras-container CSS
const maxH = CONTAINER_PADDING + HEADER_HEIGHT + maxLoras * LORA_ENTRY_HEIGHT + maxLoras * gap;
container.style.maxHeight = `${maxH}px`;
container.style.setProperty('--comfy-widget-max-height', `${maxH}px`);
// Window capture-phase hook: scroll the widget instead of zooming the canvas
// when the wheel is over a scrollable loras list.
enableListWheelScroll(container);
}
// Check if this is a randomizer node (lock button instead of drag handle)
const isRandomizerNode = opts?.isRandomizerNode === true;

View File

@@ -39,6 +39,9 @@ const NEW_TAB_ZOOM_LEVEL = 0.8;
const STRENGTH_STEP_SETTING_ID = "loramanager.strength_step";
const STRENGTH_STEP_DEFAULT = 0.05;
const LORA_WIDGET_MAX_VISIBLE_SETTING_ID = "loramanager.lora_widget_max_visible_loras";
const LORA_WIDGET_MAX_VISIBLE_DEFAULT = 12;
// ============================================================================
// Helper Functions
// ============================================================================
@@ -360,6 +363,32 @@ const getStrengthStepPreference = (() => {
};
})();
const getLoraWidgetMaxVisibleLoras = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default max visible loras.");
settingsUnavailableLogged = true;
}
return LORA_WIDGET_MAX_VISIBLE_DEFAULT;
}
try {
const value = settingManager.get(LORA_WIDGET_MAX_VISIBLE_SETTING_ID);
return value ?? LORA_WIDGET_MAX_VISIBLE_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read max visible loras setting, using default.", error);
settingsUnavailableLogged = true;
}
return LORA_WIDGET_MAX_VISIBLE_DEFAULT;
}
};
})();
// ============================================================================
// Register Extension with All Settings
// ============================================================================
@@ -463,6 +492,19 @@ app.registerExtension({
tooltip: "Step size for adjusting LoRA strength via arrow buttons or keyboard (default: 0.05)",
category: ["LoRA Manager", "LoRA Widget", "Strength Step"],
},
{
id: LORA_WIDGET_MAX_VISIBLE_SETTING_ID,
name: "Node 2.0: Maximum visible LoRA entries",
type: "slider",
attrs: {
min: 3,
max: 50,
step: 1,
},
defaultValue: LORA_WIDGET_MAX_VISIBLE_DEFAULT,
tooltip: "When using Node 2.0 rendering, limit the loras widget height to show at most this many entries (default: 12). Excess entries are accessible via scrollbar.",
category: ["LoRA Manager", "LoRA Widget", "Max Visible"],
},
],
async setup() {
await loadWorkflowOptions();
@@ -549,4 +591,5 @@ export {
getUsageStatisticsPreference,
getNewTabTemplatePreference,
getStrengthStepPreference,
getLoraWidgetMaxVisibleLoras,
};

View File

@@ -784,6 +784,51 @@ export function forwardWheelToCanvas(container, options = {}) {
}, { passive: false });
}
// Marks scrollable containers whose wheel scrolling must win over canvas zoom.
const LM_WHEEL_CLASS = 'lm-wheel-scrollable';
let lmWheelHookInstalled = false;
/**
* Keep vertical wheel scrolling inside a scrollable widget container, even in
* Nodes 2.0 / Vue mode where ComfyUI's wheel→zoom handler runs on the document
* in the capture phase (outer than any container-level listener).
* Installs a single capture-phase hook on `window` (the outermost dispatch
* point). When the wheel is over a marked, scrollable element, we manually
* scroll it and fully consume the event so canvas zoom never sees it.
*/
export function enableListWheelScroll(container) {
if (!container) return;
container.classList.add(LM_WHEEL_CLASS);
if (lmWheelHookInstalled) return;
lmWheelHookInstalled = true;
window.addEventListener('wheel', (event) => {
// Let pinch/zoom and horizontal gestures pass through.
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(`.${LM_WHEEL_CLASS}`);
if (!el) return;
const canScrollY = el.scrollHeight > el.clientHeight + 1;
if (!canScrollY) return;
// Translate deltaMode to approximate pixels.
const unit = event.deltaMode === 1 ? 16
: event.deltaMode === 2 ? el.clientHeight
: 1;
el.scrollTop += event.deltaY * unit;
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) {