This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 60b2c34f43 Issue #8286 : Attach Hop Web canvas overlay to the RAP 
widget, not a size guess (#8290)
60b2c34f43 is described below

commit 60b2c34f43db5c00edf941ccd8cf1e7a0600c175
Author: Matt Casters <[email protected]>
AuthorDate: Tue Sep 8 22:34:05 2026 +0200

    Issue #8286 : Attach Hop Web canvas overlay to the RAP widget, not a size 
guess (#8290)
    
    RAP does not put widget ids on DOM elements unless enableUITests is on,
    so document.getElementById(canvasId) was usually null. The overlay then
    guessed the first canvas larger than 500x500, which left a blank graph
    in a small viewport and drew the graph inside a dialog when one was
    open. Resolve the widget through RAP's object registry and wait for its
    nested canvas instead. The same lookup is used for mouse-wheel zoom.
    
    Fixes #8286
---
 .../org/apache/hop/ui/hopgui/canvas-svg.js         | 113 ++++++++++++++-----
 .../org/apache/hop/ui/hopgui/canvas-zoom.js        | 124 +++++++++++++++------
 .../hop/ui/hopgui/CanvasOverlayAttachTest.java     |  64 +++++++++++
 3 files changed, 238 insertions(+), 63 deletions(-)

diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js
index 537ca8be2a..d245a2af71 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js
@@ -44,32 +44,62 @@
         };
     }
 
-    function findCanvasForWidget(canvasId) {
-        if (canvasId) {
-            var widgetElement = document.getElementById(canvasId);
-            if (widgetElement) {
-                if (widgetElement.tagName === "CANVAS") {
-                    return widgetElement;
+    /**
+     * RAP does not put widget ids on DOM elements unless enableUITests is on
+     * (Widget._renderHtmlIds), so document.getElementById(canvasId) is 
usually null.
+     * Guessing "the first canvas larger than 500x500" then attaches the 
overlay to a
+     * dialog, or to nothing if the graph is smaller than that. Look the 
widget up in
+     * RAP's registry and take the canvas its GC created inside it.
+     */
+    function getWidgetDomElement(widgetId) {
+        if (!widgetId) {
+            return null;
+        }
+        try {
+            if (typeof rap !== "undefined" && typeof rap.getObject === 
"function") {
+                var proxy = rap.getObject(widgetId);
+                if (proxy && proxy.$el) {
+                    var queried = proxy.$el.get ? proxy.$el.get(0) : 
(proxy.$el[0] || proxy.$el);
+                    if (queried && queried.tagName) {
+                        return queried;
+                    }
                 }
-                var nestedCanvas = widgetElement.querySelector("canvas");
-                if (nestedCanvas) {
-                    return nestedCanvas;
+            }
+            if (typeof rwt !== "undefined" && rwt.remote && 
rwt.remote.ObjectRegistry) {
+                var nativeWidget = 
rwt.remote.ObjectRegistry.getObject(widgetId);
+                if (nativeWidget) {
+                    if (typeof nativeWidget.getElement === "function") {
+                        var element = nativeWidget.getElement();
+                        if (element) {
+                            return element;
+                        }
+                    }
+                    if (typeof nativeWidget._getTargetNode === "function") {
+                        var target = nativeWidget._getTargetNode();
+                        if (target) {
+                            return target;
+                        }
+                    }
+                    if (nativeWidget._element) {
+                        return nativeWidget._element;
+                    }
                 }
             }
+        } catch (ignored) {
+            // RAP has not registered this widget on the client yet.
         }
-        return findVisibleGraphCanvas();
+        return document.getElementById(widgetId);
     }
 
-    function findVisibleGraphCanvas() {
-        var allCanvases = document.querySelectorAll("canvas");
-        for (var i = 0; i < allCanvases.length; i++) {
-            var c = allCanvases[i];
-            var rect = c.getBoundingClientRect();
-            if (rect.width > 500 && rect.height > 500 && c.offsetParent !== 
null) {
-                return c;
-            }
+    function findCanvasForWidget(canvasId) {
+        var widgetElement = getWidgetDomElement(canvasId);
+        if (!widgetElement) {
+            return null;
         }
-        return null;
+        if (widgetElement.tagName === "CANVAS") {
+            return widgetElement;
+        }
+        return widgetElement.querySelector("canvas");
     }
 
     function buildServiceHandlerUrl(serviceId) {
@@ -280,15 +310,18 @@
     }
 
     hop.CanvasSvgRenderer = function (properties) {
+        properties = properties || {};
         this._canvas = null;
         this._overlay = null;
-        this._sessionUuid = null;
-        this._canvasId = null;
+        this._sessionUuid = properties.sessionUuid || null;
+        this._canvasId = properties.canvasId || null;
+        this._serviceHandlerUrl = properties.serviceHandlerUrl || null;
+        this._findTimer = null;
+        this._destroyed = false;
         this._revision = 0;
         this._areas = [];
         this._props = {};
         this._remoteObject = null;
-        this._serviceHandlerUrl = null;
         this._pollTimer = null;
         this._pollCount = 0;
         this._emptyRetries = 0;
@@ -346,6 +379,11 @@
 
     hop.CanvasSvgRenderer.prototype = {
         destroy: function () {
+            this._destroyed = true;
+            if (this._findTimer) {
+                clearTimeout(this._findTimer);
+                this._findTimer = null;
+            }
             if (this._pollTimer) {
                 clearInterval(this._pollTimer);
                 this._pollTimer = null;
@@ -384,18 +422,36 @@
         _findAndAttachCanvas: function () {
             var self = this;
             var attempts = 0;
-            var maxAttempts = 20;
+            if (this._findTimer) {
+                clearTimeout(this._findTimer);
+                this._findTimer = null;
+            }
 
             var tryFind = function () {
-                attempts++;
+                if (self._destroyed) {
+                    return;
+                }
                 var canvas = findCanvasForWidget(self._canvasId);
                 if (!canvas) {
-                    if (attempts < maxAttempts) {
-                        setTimeout(tryFind, 100);
+                    attempts++;
+                    // Nested <canvas> is created lazily on the first GC. Wait 
longer when the
+                    // RAP widget is already on the client; otherwise give up 
so a later
+                    // attachListener can start a fresh search.
+                    var widgetPresent = !!getWidgetDomElement(self._canvasId);
+                    var maxAttempts = widgetPresent ? 300 : 100;
+                    if (self._canvasId && attempts < maxAttempts) {
+                        self._findTimer = setTimeout(tryFind, 100);
                     }
                     return;
                 }
+                self._findTimer = null;
                 if (self._canvas === canvas) {
+                    var parent = canvas.parentElement;
+                    if (parent && self._overlay && self._overlay.parentNode 
!== parent) {
+                        self._attachToCanvas(canvas);
+                    } else if (self._overlay) {
+                        self._syncOverlayLayout(canvas);
+                    }
                     return;
                 }
                 self._attachToCanvas(canvas);
@@ -516,6 +572,11 @@
             if (!this._pollTimer) {
                 this._pollTimer = setInterval(function () {
                     self._pollCount++;
+                    if (self._canvas && !self._canvas.parentNode) {
+                        self._canvas = null;
+                        self._findAndAttachCanvas();
+                        return;
+                    }
                     if (self._canvas) {
                         self._syncOverlayLayout(self._canvas);
                     }
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js
index f03e1b3df7..8c964e3d2f 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js
@@ -24,20 +24,84 @@
         window.hop = {};
     }
 
+    /**
+     * RAP does not put widget ids on DOM elements unless enableUITests is on, 
so
+     * document.getElementById(canvasId) is usually null. Do not guess by 
size: that
+     * binds wheel zoom to a dialog canvas or to nothing when the graph is 
small.
+     */
+    function getWidgetDomElement(widgetId) {
+        if (!widgetId) {
+            return null;
+        }
+        try {
+            if (typeof rap !== "undefined" && typeof rap.getObject === 
"function") {
+                var proxy = rap.getObject(widgetId);
+                if (proxy && proxy.$el) {
+                    var queried = proxy.$el.get ? proxy.$el.get(0) : 
(proxy.$el[0] || proxy.$el);
+                    if (queried && queried.tagName) {
+                        return queried;
+                    }
+                }
+            }
+            if (typeof rwt !== "undefined" && rwt.remote && 
rwt.remote.ObjectRegistry) {
+                var nativeWidget = 
rwt.remote.ObjectRegistry.getObject(widgetId);
+                if (nativeWidget) {
+                    if (typeof nativeWidget.getElement === "function") {
+                        var element = nativeWidget.getElement();
+                        if (element) {
+                            return element;
+                        }
+                    }
+                    if (typeof nativeWidget._getTargetNode === "function") {
+                        var target = nativeWidget._getTargetNode();
+                        if (target) {
+                            return target;
+                        }
+                    }
+                    if (nativeWidget._element) {
+                        return nativeWidget._element;
+                    }
+                }
+            }
+        } catch (ignored) {
+            // RAP has not registered this widget on the client yet.
+        }
+        return document.getElementById(widgetId);
+    }
+
+    function findCanvasForWidget(canvasId) {
+        var widgetElement = getWidgetDomElement(canvasId);
+        if (!widgetElement) {
+            return null;
+        }
+        if (widgetElement.tagName === "CANVAS") {
+            return widgetElement;
+        }
+        return widgetElement.querySelector("canvas");
+    }
+
     // Define the CanvasZoom constructor BEFORE registering the type handler
     hop.CanvasZoom = function(properties) {
+        properties = properties || {};
         this._canvas = null;
-        this._canvasId = properties.canvas; // This is the Canvas widget ID 
(Composite), not the actual canvas element
+        this._canvasId = properties.canvas; // RAP Canvas widget id, not the 
HTML <canvas>
         this._remoteObject = null;
         this._wheelHandler = null;
         this._sizeCheckInterval = null;
-        
+        this._findTimer = null;
+        this._destroyed = false;
+
         // DON'T attach in constructor - wait for explicit attachListener call 
from Java
         // This ensures the canvas is fully created and the remote object is 
ready
     };
 
     hop.CanvasZoom.prototype = {
         destroy: function() {
+            this._destroyed = true;
+            if (this._findTimer) {
+                clearTimeout(this._findTimer);
+                this._findTimer = null;
+            }
             if (this._canvas && this._wheelHandler) {
                 this._canvas.removeEventListener('wheel', this._wheelHandler);
             }
@@ -90,46 +154,27 @@
         _findAndAttachCanvas: function() {
             var self = this;
             var attempts = 0;
-            var maxAttempts = 20;
+            if (this._findTimer) {
+                clearTimeout(this._findTimer);
+                this._findTimer = null;
+            }
 
-            var findCanvasElement = function() {
-                // Prefer the RAP widget id from Java (same approach as 
canvas-svg.js).
-                if (self._canvasId) {
-                    var widgetElement = 
document.getElementById(self._canvasId);
-                    if (widgetElement) {
-                        if (widgetElement.tagName === "CANVAS") {
-                            return widgetElement;
-                        }
-                        var nested = widgetElement.querySelector("canvas");
-                        if (nested) {
-                            return nested;
-                        }
-                    }
-                }
-                // Fallback: first large visible graph canvas
-                var allCanvases = document.querySelectorAll("canvas");
-                for (var i = 0; i < allCanvases.length; i++) {
-                    var c = allCanvases[i];
-                    var rect = c.getBoundingClientRect();
-                    if (rect.width > 500 && rect.height > 500 && 
c.offsetParent !== null) {
-                        return c;
-                    }
-                }
-                return null;
-            };
-            
             var tryFindCanvas = function() {
-                attempts++;
-                var canvas = findCanvasElement();
-                
+                if (self._destroyed) {
+                    return;
+                }
+                var canvas = findCanvasForWidget(self._canvasId);
+
                 if (!canvas) {
-                    if (attempts < maxAttempts) {
-                        setTimeout(tryFindCanvas, 100);
-                        return;
-                    } else {
-                        return;
+                    attempts++;
+                    var widgetPresent = !!getWidgetDomElement(self._canvasId);
+                    var maxAttempts = widgetPresent ? 300 : 100;
+                    if (self._canvasId && attempts < maxAttempts) {
+                        self._findTimer = setTimeout(tryFindCanvas, 100);
                     }
+                    return;
                 }
+                self._findTimer = null;
                 
                 // Same canvas element: still re-ensure the wheel listener 
(RAP may replace nodes).
                 if (self._canvas === canvas && self._wheelHandler) {
@@ -152,6 +197,11 @@
                 // This catches transforms applied by RAP at any time, 
including initial load at low zoom
                 if (!self._sizeCheckInterval) {
                     self._sizeCheckInterval = setInterval(function() {
+                        if (self._canvas && !self._canvas.parentNode) {
+                            self._canvas = null;
+                            self._findAndAttachCanvas();
+                            return;
+                        }
                         self._applyCanvasSizeFix();
                     }, 200); // Check every 200ms
                 }
diff --git 
a/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java 
b/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java
new file mode 100644
index 0000000000..8055092f92
--- /dev/null
+++ b/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Guards the Hop Web canvas overlay against issue #8286.
+ *
+ * <p>RAP does not put widget ids on DOM elements unless enableUITests is on, 
so looking up {@code
+ * document.getElementById(canvasId)} and then guessing "the first canvas 
larger than 500x500" left
+ * a blank graph in a small viewport and drew the graph inside a dialog when 
one was open.
+ */
+class CanvasOverlayAttachTest {
+
+  @Test
+  void svgOverlayResolvesTheRapWidgetNotALargeCanvas() throws IOException {
+    String js = readResource("org/apache/hop/ui/hopgui/canvas-svg.js");
+
+    assertFalse(js.contains("findVisibleGraphCanvas"), js);
+    assertFalse(js.contains("rect.width > 500"), js);
+    assertTrue(js.contains("ObjectRegistry"), js);
+    assertTrue(js.contains("this._canvasId = properties.canvasId"), js);
+  }
+
+  @Test
+  void zoomResolvesTheRapWidgetNotALargeCanvas() throws IOException {
+    String js = readResource("org/apache/hop/ui/hopgui/canvas-zoom.js");
+
+    assertFalse(js.contains("rect.width > 500"), js);
+    assertTrue(js.contains("ObjectRegistry"), js);
+    assertTrue(js.contains("this._canvasId = properties.canvas"), js);
+  }
+
+  private static String readResource(String name) throws IOException {
+    InputStream in = 
CanvasOverlayAttachTest.class.getClassLoader().getResourceAsStream(name);
+    assertNotNull(in, name);
+    try (in) {
+      return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+    }
+  }
+}

Reply via email to