From cf0eec8d31112a3df6184691d55e82c42b3a1f0b Mon Sep 17 00:00:00 2001 From: ausboss <102309304+ausboss@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:48:54 -0600 Subject: [PATCH] Zoom the graph over a text box that has nothing to scroll A text box on one of the pack's nodes kept the mouse wheel whenever the cursor was in it, even when it was empty, so the graph stopped zooming until you clicked away. A box now keeps the wheel only while it can still scroll that way, typed in or not, as ComfyUI's own text boxes do. Ctrl + wheel and trackpad pinch always zoom the graph; over a long text box or list they zoomed the whole page. The Callout editor no longer stops the wheel; the shared rule already lets its text scroll. --- CONTRIBUTING.md | 2 +- js/callout/index.js | 1 - js/canvas_passthrough/index.js | 4 ++-- js/shared/canvas_passthrough.mjs | 11 +++++++---- tests/canvas_passthrough.test.mjs | 12 +++++++++--- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e62411..ee6d1e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,7 +193,7 @@ example_workflows/ # example workflows (regular workflow JSON, not API JSON) drag). Messages to the user go through `showToast`, never `alert()`. - The mouse wheel over a node zooms the graph, on every part of its face. `js/canvas_passthrough/` hands the wheel on from the pack's panels, and - keeps it only for a box that can still scroll or a field being typed in, + keeps it only for a box that can still scroll, typed in or not, so a panel adds no wheel listener of its own (`tests/canvas_passthrough.test.mjs` lists the few that exist). After a change to a node face, `scripts/dev/wheel_sweep.mjs` turns the wheel at diff --git a/js/callout/index.js b/js/callout/index.js index 12588f8..1e6d4b5 100644 --- a/js/callout/index.js +++ b/js/callout/index.js @@ -649,7 +649,6 @@ function startEdit(state) { if (event.target !== area) event.preventDefault(); }); wrap.addEventListener("pointerdown", (event) => event.stopPropagation()); - wrap.addEventListener("wheel", (event) => event.stopPropagation(), { passive: true }); // Clicking another node on the canvas takes the keyboard from the text box. // That is how the link is picked, so the edit stays open then; a click on // empty canvas, or on the note itself, ends it. diff --git a/js/canvas_passthrough/index.js b/js/canvas_passthrough/index.js index 56aee17..c44e8a4 100644 --- a/js/canvas_passthrough/index.js +++ b/js/canvas_passthrough/index.js @@ -5,7 +5,7 @@ // zooming stopped over Compare, the crop previews, the video viewers and every // control on a card. One listener here hands those gestures back, the same way // ComfyUI does for its own text boxes. A panel that wants the wheel for itself -// (a scrolling list, a focused text field, an editor) still keeps it. +// (a scrolling list, a long text box) keeps it while it can still scroll. // // A file dragged in from the desktop, or a picture's card from the Assets // panel, has the same problem in the classic renderer: dropped on a node's @@ -31,7 +31,7 @@ function onWheel(event) { const root = panelRoot(target); const lgCanvas = canvas(); if (!root || !lgCanvas || event.defaultPrevented) return; - if (panelKeepsWheel(target, event, { root, host: panelHost(target), styleOf: (el) => getComputedStyle(el), activeElement: document.activeElement })) return; + if (panelKeepsWheel(target, event, { root, host: panelHost(target), styleOf: (el) => getComputedStyle(el) })) return; event.preventDefault(); lgCanvas.processMouseWheel(event); } diff --git a/js/shared/canvas_passthrough.mjs b/js/shared/canvas_passthrough.mjs index 86a6c52..2e6bf3a 100644 --- a/js/shared/canvas_passthrough.mjs +++ b/js/shared/canvas_passthrough.mjs @@ -9,7 +9,9 @@ // in js/canvas_passthrough/ wires them to the page. // The wheel belongs to the panel while it can still scroll that way (a long -// text box, a list of rows), or while a text field has the keyboard. +// text box, a list of rows). A text box with the cursor in it is no +// exception: when it has nothing left to scroll, the wheel zooms the graph, +// as it does over ComfyUI's own text boxes. export function canScrollFurther(element, delta, axis = "y") { if (!element || !delta) return false; const horizontal = axis === "x"; @@ -25,10 +27,11 @@ export function canScrollFurther(element, delta, axis = "y") { // and the panel's own root? `styleOf` is getComputedStyle, passed in so this // stays testable without a page. `host` is the frontend's wrapper around the // panel: a pointer on the wrapper itself is on nothing inside the panel. -export function panelKeepsWheel(target, event, { root, host, styleOf, activeElement }) { +// Ctrl + wheel, which is also how a trackpad pinch arrives, always zooms the +// graph; left to a scrolling box it would zoom the whole page. +export function panelKeepsWheel(target, event, { root, host, styleOf }) { + if (event.ctrlKey || event.metaKey) return false; for (let element = target; element && element !== root && element !== host; element = element.parentElement) { - const tag = String(element.tagName ?? "").toUpperCase(); - if ((tag === "TEXTAREA" || tag === "INPUT") && element === activeElement) return true; const style = styleOf(element); const overflow = style?.overflowY; if ((overflow === "auto" || overflow === "scroll") && canScrollFurther(element, event.deltaY)) return true; diff --git a/tests/canvas_passthrough.test.mjs b/tests/canvas_passthrough.test.mjs index 7cd08e8..71cbd53 100644 --- a/tests/canvas_passthrough.test.mjs +++ b/tests/canvas_passthrough.test.mjs @@ -27,12 +27,19 @@ test("the graph gets the wheel unless the panel has a use for it", () => { const list = { tagName: "DIV", parentElement: root, ...box() }; const field = { tagName: "INPUT", parentElement: root }; const styleOf = (el) => ({ overflowY: el === list ? "auto" : "visible" }); - const ctx = (extra = {}) => ({ root, styleOf, activeElement: null, ...extra }); + const ctx = (extra = {}) => ({ root, styleOf, ...extra }); assert.equal(panelKeepsWheel(stage, { deltaY: -100 }, ctx()), false, "plain picture stage"); assert.equal(panelKeepsWheel(list, { deltaY: 100 }, ctx()), true, "list with room to scroll"); assert.equal(panelKeepsWheel(list, { deltaY: -100 }, ctx()), false, "list at its top, wheel up zooms"); assert.equal(panelKeepsWheel(field, { deltaY: 100 }, ctx()), false, "idle field"); - assert.equal(panelKeepsWheel(field, { deltaY: 100 }, ctx({ activeElement: field })), true, "field being typed in"); + // A text box with the cursor in it and nothing to scroll: the graph zooms + // (issue #115, the prompt box in a workflow). + assert.equal(panelKeepsWheel(field, { deltaY: 100 }, ctx({ activeElement: field })), false, "field being typed in, nothing to scroll"); + const prompt = { tagName: "TEXTAREA", parentElement: root, ...box() }; + const scrolls = (el) => ({ overflowY: el === prompt || el === list ? "auto" : "visible" }); + assert.equal(panelKeepsWheel(prompt, { deltaY: 100 }, ctx({ styleOf: scrolls })), true, "long text still scrolls"); + assert.equal(panelKeepsWheel(prompt, { deltaY: -100 }, ctx({ styleOf: scrolls })), false, "long text at its top, wheel up zooms"); + assert.equal(panelKeepsWheel(list, { deltaY: 100, ctrlKey: true }, ctx()), false, "Ctrl + wheel and pinch always zoom the graph"); }); test("middle button and Ctrl+Shift drag go to the graph; ordinary clicks do not", () => { @@ -131,7 +138,6 @@ test("only the listed parts of the pack listen for the wheel", () => { // face of the node. const js = join(dirname(fileURLToPath(import.meta.url)), "..", "js"); const listed = [ - "callout/index.js wrap", // the text box while a note is being edited "canvas_passthrough/index.js window", // hands the wheel to the graph "image_folder/index.js root", // the copy question, a window over the page "image_folder/index.js root", // the folder list, a window over the page