From cdde99b9ab6de892f7e077669a78d30c62273244 Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Tue, 30 Sep 2025 13:11:02 +0200 Subject: [PATCH] Add scroll spacer to fix scrollable area at minimum zoom When at minimum zoom with "cover" behavior, the SVG canvas may be smaller than the viewport in one dimension. This causes the browser's scrollWidth/scrollHeight to be limited by the SVG size rather than the full canvas extent. Added an invisible spacer div that matches the scaled canvas dimensions, ensuring the scrollable area always reflects the actual canvas size. This allows proper scrolling to reach all canvas edges without going beyond canvas boundaries. --- .../@node-red/editor-client/src/js/ui/view.js | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 3e5a8e040..5b9843ae1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -336,6 +336,24 @@ RED.view = (function() { function init() { chart = $("#red-ui-workspace-chart"); + + // Add invisible spacer div to ensure scrollable area matches canvas dimensions + // At minimum zoom with "cover" behavior, SVG may be smaller than viewport in one dimension + // This spacer forces the browser to calculate scrollWidth/Height based on full canvas size + // Browser's maxScroll = scrollWidth - viewport will then correctly show canvas edges + var scrollSpacer = $('
') + .css({ + position: 'absolute', + top: 0, + left: 0, + width: space_width + 'px', + height: space_height + 'px', + pointerEvents: 'none', + visibility: 'hidden' + }) + .attr('id', 'red-ui-workspace-scroll-spacer') + .appendTo(chart); + chart.on('contextmenu', function(evt) { if (RED.view.DEBUG) { console.warn("contextmenu", { mouse_mode, event: d3.event }); @@ -2810,14 +2828,14 @@ RED.view = (function() { // Calculate the minimum zoom to ensure canvas always fills the viewport (no empty space) var viewportWidth = chart.width(); var viewportHeight = chart.height(); - + // Canvas is 8000x8000, calculate zoom to cover viewport var zoomToFitWidth = viewportWidth / space_width; var zoomToFitHeight = viewportHeight / space_height; - + // Use the LARGER zoom to ensure canvas covers entire viewport (no empty space visible) var calculatedMinZoom = Math.max(zoomToFitWidth, zoomToFitHeight); - + // Return the larger of the calculated min or the configured min // This ensures canvas always fills the viewport return Math.max(calculatedMinZoom, RED.view.zoomConstants.MIN_ZOOM); @@ -4969,6 +4987,15 @@ RED.view = (function() { eventLayer.attr("transform","scale("+scaleFactor+")"); outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor); + // Update scroll spacer to match scaled canvas size + // This ensures scrollable area = canvas area + // Browser calculates maxScroll = scrollWidth - viewport, which correctly + // allows scrolling to see the far edges of canvas without going beyond + $('#red-ui-workspace-scroll-spacer').css({ + width: (space_width * scaleFactor) + 'px', + height: (space_height * scaleFactor) + 'px' + }); + // Don't bother redrawing nodes if we're drawing links if (showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) {