From 844bf29de1018c65e2f8708d72afb98308f6156a Mon Sep 17 00:00:00 2001
From: Nick O'Leary <nick.oleary@gmail.com>
Date: Thu, 24 Jun 2021 17:40:26 +0100
Subject: [PATCH 1/2] Add RED.view.annotations api

---
 Gruntfile.js                                  |   1 +
 .../editor-client/src/js/ui/common/popover.js |   4 -
 .../src/js/ui/view-annotations.js             | 148 ++++++++++++++++++
 .../@node-red/editor-client/src/js/ui/view.js | 101 ++++++------
 .../editor-client/src/sass/variables.scss     |   5 +
 5 files changed, 200 insertions(+), 59 deletions(-)
 create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js

diff --git a/Gruntfile.js b/Gruntfile.js
index 5d9b8ba5c..df9939a8c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -170,6 +170,7 @@ module.exports = function(grunt) {
                     "packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js",
                     "packages/node_modules/@node-red/editor-client/src/js/ui/statusBar.js",
                     "packages/node_modules/@node-red/editor-client/src/js/ui/view.js",
+                    "packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js",
                     "packages/node_modules/@node-red/editor-client/src/js/ui/view-navigator.js",
                     "packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js",
                     "packages/node_modules/@node-red/editor-client/src/js/ui/sidebar.js",
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js
index 7049c3164..8d7553069 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js
@@ -308,11 +308,9 @@ RED.popover = (function() {
                             // DOWN
                             if (currentItem.length > 0) {
                                 if (currentItem.index() === menuOptions.length-1) {
-                                    console.log("WARP TO TOP")
                                     // Wrap to top of list
                                     list.children().first().children().first().focus();
                                 } else {
-                                    console.log("GO DOWN ONE")
                                     currentItem.next().children().first().focus();
                                 }
                             } else {
@@ -323,11 +321,9 @@ RED.popover = (function() {
                             // UP
                             if (currentItem.length > 0) {
                                 if (currentItem.index() === 0) {
-                                    console.log("WARP TO BOTTOM")
                                     // Wrap to bottom of list
                                     list.children().last().children().first().focus();
                                 } else {
-                                    console.log("GO UP ONE")
                                     currentItem.prev().children().first().focus();
                                 }
                             } else {
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
new file mode 100644
index 000000000..e3a6a1114
--- /dev/null
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
@@ -0,0 +1,148 @@
+RED.view.annotations = (function() {
+
+    var annotations = {};
+
+    function init() {
+        RED.hooks.add("viewRedrawNode.annotations", function(evt) {
+            try {
+                if (evt.node.__pendingAnnotation__) {
+                    addAnnotation(evt.node.__pendingAnnotation__,evt);
+                    delete evt.node.__pendingAnnotation__;
+                }
+                var badgeDX = 0;
+                var controlDX = 0;
+                for (var i=0,l=evt.el.__annotations__.length;i<l;i++) {
+                    var annotation = evt.el.__annotations__[i];
+                    if (annotations.hasOwnProperty(annotation.id)) {
+                        var opts = annotations[annotation.id];
+                        var showAnnotation = true;
+                        var isBadge = opts.type === 'badge';
+                        if (opts.show !== undefined) {
+                            if (typeof opts.show === "string") {
+                                showAnnotation = !!evt.node[opts.show]
+                            } else if (typeof opts.show === "function"){
+                                showAnnotation = opts.show(evt.node)
+                            } else {
+                                showAnnotation = !!opts.show;
+                            }
+                            annotation.element.classList.toggle("hide", !showAnnotation);
+                        }
+                        if (isBadge) {
+                            if (showAnnotation) {
+                                var rect = annotation.element.getBoundingClientRect();
+                                badgeDX += rect.width;
+                                annotation.element.setAttribute("transform", "translate("+(evt.node.w-3-badgeDX)+", -8)");
+                                badgeDX += 4;
+                            }
+                        } else {
+                            if (showAnnotation) {
+                                var rect = annotation.element.getBoundingClientRect();
+                                annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)");
+                                controlDX += rect.width + 4;
+                            }
+                        }
+                    } else {
+                        annotation.element.parentNode.removeChild(annotation.element);
+                        evt.el.__annotations__.splice(i,1);
+                        i--;
+                        l--;
+                    }
+                }
+        }catch(err) {
+            console.log(err)
+        }
+        });
+    }
+
+
+    /**
+     * Register a new node annotation
+     * @param {string} id - unique identifier
+     * @param {type} opts - annotations options
+     *
+     * opts: {
+     *   type: "badge"|"annotation"
+     *   class: "",
+     *   element: function(node),
+     *   show: string|function(node),
+     *   filter: function(node) -> boolean
+     * }
+     */
+    function register(id, opts) {
+        annotations[id] = opts
+        RED.hooks.add("viewAddNode.annotation-"+id, function(evt) {
+            if (opts.filter && !opts.filter(evt.node)) {
+                return;
+            }
+            addAnnotation(id,evt);
+        });
+
+        var nodes = RED.view.getActiveNodes();
+        nodes.forEach(function(n) {
+            n.__pendingAnnotation__ = id;
+        })
+        RED.view.redraw();
+
+    }
+
+    function addAnnotation(id,evt) {
+        var opts = annotations[id];
+        evt.el.__annotations__ = evt.el.__annotations__ || [];
+        var annotationGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
+        annotationGroup.setAttribute("class",opts.class || "");
+        evt.el.__annotations__.push({
+            id:id,
+            element: annotationGroup
+        });
+        var annotation = opts.element(evt.node);
+        if (opts.tooltip) {
+            annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation,evt.node,opts.tooltip));
+            annotation.addEventListener("mouseleave", annotationMouseLeave);
+        }
+        annotationGroup.appendChild(annotation);
+        evt.el.appendChild(annotationGroup);
+    }
+
+
+    function unregister(id) {
+        delete annotations[id]
+        RED.hooks.remove("*.annotation-"+id);
+        RED.view.redraw();
+    }
+
+    var badgeHoverTimeout;
+    var badgeHover;
+    function getAnnotationMouseEnter(annotation,node,tooltip) {
+        return function() {
+            var text = typeof tooltip === "function"?tooltip(node):tooltip;
+            if (text) {
+                clearTimeout(badgeHoverTimeout);
+                badgeHoverTimeout = setTimeout(function() {
+                    var pos = RED.view.getElementPosition(annotation);
+                    var rect = annotation.getBoundingClientRect();
+                    badgeHoverTimeout = null;
+                    badgeHover = RED.view.showTooltip(
+                        (pos[0]+rect.width/2),
+                        (pos[1]),
+                        text,
+                        "top"
+                    );
+                },500);
+            }
+        }
+    }
+    function annotationMouseLeave() {
+        clearTimeout(badgeHoverTimeout);
+        if (badgeHover) {
+            badgeHover.remove();
+            badgeHover = null;
+        }
+    }
+
+    return {
+        init: init,
+        register:register,
+        unregister:unregister
+    }
+
+})();
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 a30af74b4..dc8041d8d 100755
--- 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
@@ -546,10 +546,44 @@ RED.view = (function() {
             }
         });
 
+        RED.view.annotations.init();
         RED.view.navigator.init();
         RED.view.tools.init();
+
+
+        RED.view.annotations.register("red-ui-flow-node-changed",{
+            type: "badge",
+            class: "red-ui-flow-node-changed",
+            element: function() {
+                var changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle");
+                changeBadge.setAttribute("cx",5);
+                changeBadge.setAttribute("cy",5);
+                changeBadge.setAttribute("r",5);
+                return changeBadge;
+            },
+            show: function(n) { return n.changed||n.moved }
+        })
+
+        RED.view.annotations.register("red-ui-flow-node-error",{
+            type: "badge",
+            class: "red-ui-flow-node-error",
+            element: function(d) {
+                var errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path");
+                errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z");
+                return errorBadge
+            },
+            tooltip: function(d) {
+                if (d.validationErrors && d.validationErrors.length > 0) {
+                    return RED._("editor.errors.invalidProperties")+"\n  - "+d.validationErrors.join("\n    - ")
+                }
+            },
+            show: function(n) { return !n.valid }
+        })
+
     }
 
+
+
     function updateGrid() {
         var gridTicks = [];
         for (var i=0;i<space_width;i+=+gridSize) {
@@ -3597,31 +3631,6 @@ RED.view = (function() {
         }
     }
 
-    function errorBadgeMouseEnter(e) {
-        var d = this.__data__;
-        if (d.validationErrors && d.validationErrors.length > 0) {
-            clearTimeout(portLabelHoverTimeout);
-            var node = this;
-            portLabelHoverTimeout = setTimeout(function() {
-                var pos = getElementPosition(node);
-                portLabelHoverTimeout = null;
-                portLabelHover = showTooltip(
-                    (pos[0]),
-                    (pos[1]),
-                    RED._("editor.errors.invalidProperties")+"\n  - "+d.validationErrors.join("\n    - "),
-                    "top"
-                );
-            },500);
-        }
-    }
-    function errorBadgeMouseLeave() {
-        clearTimeout(portLabelHoverTimeout);
-        if (portLabelHover) {
-            portLabelHover.remove();
-            portLabelHover = null;
-        }
-    }
-
     function redrawStatus(d,nodeEl) {
         if (d.z !== RED.workspaces.active()) {
             return;
@@ -3938,31 +3947,11 @@ RED.view = (function() {
 
                 nodeContents.appendChild(statusEl);
 
-
-                var changeBadgeG = document.createElementNS("http://www.w3.org/2000/svg","g");
-                changeBadgeG.setAttribute("class","red-ui-flow-node-changed hide");
-                changeBadgeG.setAttribute("transform","translate(20, -2)");
-                node[0][0].__changeBadge__ = changeBadgeG;
-                var changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle");
-                changeBadge.setAttribute("r",5);
-                changeBadgeG.appendChild(changeBadge);
-                nodeContents.appendChild(changeBadgeG);
-
-
-                var errorBadgeG = document.createElementNS("http://www.w3.org/2000/svg","g");
-                errorBadgeG.setAttribute("class","red-ui-flow-node-error hide");
-                errorBadgeG.setAttribute("transform","translate(0, -2)");
-                node[0][0].__errorBadge__ = errorBadgeG;
-                var errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path");
-                errorBadge.setAttribute("d","M -5,4 l 10,0 -5,-8 z");
-                errorBadgeG.appendChild(errorBadge);
-                errorBadge.__data__ = d;
-                errorBadge.addEventListener("mouseenter", errorBadgeMouseEnter);
-                errorBadge.addEventListener("mouseleave", errorBadgeMouseLeave);
-                nodeContents.appendChild(errorBadgeG);
-
                 node[0][0].appendChild(nodeContents);
+
+                RED.hooks.trigger("viewAddNode",{node:d,el:this})
             });
+
             node.each(function(d,i) {
                 if (d.dirty) {
                     var self = this;
@@ -4202,10 +4191,10 @@ RED.view = (function() {
                                                                  );
                             faIcon.attr("y",(d.h+13)/2);
                         }
-                        this.__changeBadge__.setAttribute("transform", "translate("+(d.w-10)+", -2)");
-                        this.__changeBadge__.classList.toggle("hide", !(d.changed||d.moved));
-                        this.__errorBadge__.setAttribute("transform", "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)");
-                        this.__errorBadge__.classList.toggle("hide", d.valid);
+                        // this.__changeBadge__.setAttribute("transform", "translate("+(d.w-10)+", -2)");
+                        // this.__changeBadge__.classList.toggle("hide", !(d.changed||d.moved));
+                        // this.__errorBadge__.setAttribute("transform", "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)");
+                        // this.__errorBadge__.classList.toggle("hide", d.valid);
 
                         thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) {
                             var port = d3.select(this);
@@ -4254,8 +4243,6 @@ RED.view = (function() {
                         // });
                     }
 
-                    RED.hooks.trigger("viewAddNode",{node:d,el:this})
-
                     if (d.dirtyStatus) {
                         redrawStatus(d,this);
                     }
@@ -4270,6 +4257,8 @@ RED.view = (function() {
                         }
                     }
                 }
+
+                RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
             });
             var link = linkLayer.selectAll(".red-ui-flow-link").data(
                 activeLinks,
@@ -5300,6 +5289,8 @@ RED.view = (function() {
         },
         redrawStatus: redrawStatus,
         showQuickAddDialog:showQuickAddDialog,
-        calculateNodeDimensions: calculateNodeDimensions
+        calculateNodeDimensions: calculateNodeDimensions,
+        getElementPosition:getElementPosition,
+        showTooltip:showTooltip
     };
 })();
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss
index 63a4b1d14..72b573a2e 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss
@@ -81,6 +81,11 @@
     --red-ui-node-status-changed-border: #{$node-status-changed-border};
     --red-ui-node-status-changed-background: #{$node-status-changed-background};
 
+
+
+    --red-ui-node-border: #{$node-border};
+    --red-ui-node-port-background:#{$node-port-background};
+
     --red-ui-node-label-color: #{$node-label-color};
     --red-ui-node-selected-color: #{$node-selected-color};
     --red-ui-port-selected-color: #{$port-selected-color};

From 3255e11cfcf472df693d58cd2de996ef50c57cd0 Mon Sep 17 00:00:00 2001
From: Nick O'Leary <nick.oleary@gmail.com>
Date: Thu, 24 Jun 2021 17:59:32 +0100
Subject: [PATCH 2/2] Limit annotations to badge type

---
 .../@node-red/editor-client/src/js/ui/view-annotations.js    | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
index e3a6a1114..1df8c4bd1 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
@@ -61,7 +61,7 @@ RED.view.annotations = (function() {
      * @param {type} opts - annotations options
      *
      * opts: {
-     *   type: "badge"|"annotation"
+     *   type: "badge"
      *   class: "",
      *   element: function(node),
      *   show: string|function(node),
@@ -69,6 +69,9 @@ RED.view.annotations = (function() {
      * }
      */
     function register(id, opts) {
+        if (opts.type !== 'badge') {
+            throw new Error("Unsupported annotation type: "+opts.type);
+        }
         annotations[id] = opts
         RED.hooks.add("viewAddNode.annotation-"+id, function(evt) {
             if (opts.filter && !opts.filter(evt.node)) {