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);
+ }
+ }
+}