Custom Node Status Popup

Hi everyone. I've used this forum over the years and received tons of help. Just wanted to give back and share a Status Popup that I add to my custom nodes. Hopefully it will be useful to someone.

The motivation was to get a little more status information on the screen than what's provided by the built-in status text.

It looks like this:

Capture

You click on the node to open the popup. And click again to close. It can be programmed to stay open or auto-close after a timeout. The popup text is sent from the backend JS.

To add it to your custom node:

1: Add this file to your node's "resources" directory.

Located at:

.node-red/node_modules/YourNodeDirectory/resources/AddPopupSupport.js

(Create the directory if necessary.)

AddPopupSupport.js

//
// Popup for a custom node
//
// To use:
//
//  1. Add this file to the resources directory:  .node-red/node_modules/<custom node>/resources/
//
//  2. Make the HTML file look like this:
//
//      --------------------------------------------------
//      Add this to import the popup code
//      --------------------------------------------------
//      <script src="/resources/<custom node>/AddPopupSupport.js"></script>
//
//      <script type="text/javascript">
//
//          --------------------------------------------------
//          Make this function auto run
//          --------------------------------------------------
//          (function() {
//              RED.nodes.registerType('<custom node>',{ // this is the label in the pallette
//                  ...
//              });
//          })();
//
//          --------------------------------------------------
//          Create the popup
//          --------------------------------------------------
//          AddPopupSupport('<custom node>', {width:160, height:80, autoCloseTime: 10000});
//          // width, height, autoCloseTime are optional
//
//      </script>
//
// 3. Send Message from the JS file with this:
//      RED.comms.publish('<custom node>/popupstatustext', {
//          nodeID: node.id,
//          popupHTML: "Custom message for<br>"+ node.id + "<br>" + Date(), // put custom status message here
//      });
//
//
function AddPopupSupport(nodeType, options) {

    const width = (options?.width) ? options.width : 160;
    const height = (options?.height) ? options.height : 80;
    const autoCloseTime = (options?.autoCloseTime) ? options.autoCloseTime : -1;

    // subscribe to backend messages for this nodetype
    if (!window[nodeType+'Subscribed']) {
        window[nodeType+'Subscribed'] = true;
        RED.comms.subscribe(nodeType+'/popupstatustext', function (topic, msg) {
            const popupDiv = document.getElementById("popup.div." + msg.nodeID); // access the div
            popupDiv.innerHTML = msg.popupHTML;
        });    
   }

    // Create a popup to display diagnostics
    RED.events.on('runtime-state', function () {
        RED.nodes.eachNode( (node)=>{

            if ((node.type === nodeType) & (!document.getElementById('popup.div.'+node.id))) {

                //------------------------------------------------------------------------------------------------
                // Create the Popup elements
                //------------------------------------------------------------------------------------------------
                const svgElement = document.getElementById(node.id);

                // create svg group
                let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
                group.setAttribute("transform", "translate(0, 50)");
                group.style.display = 'none';

                // Create a svg rect element for the background
                let rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
                rect.setAttribute("id", "rect");
                rect.setAttribute("x", "0");
                rect.setAttribute("y", "0");
                rect.setAttribute("width", width);
                rect.setAttribute("height", height);
                rect.setAttribute("fill", "#f1f1f1");
                rect.setAttribute("stroke", "#d4d4d4");
                rect.setAttribute("stroke-width", "1");
                rect.setAttribute("rx", "6");  // Border radius

                // Create a foreignObject element
                let foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
                const sourceText = svgElement.querySelector('.red-ui-flow-node-status-label');
                for (const attr of sourceText.attributes) { foreignObject.setAttribute(attr.name, attr.value) };
                foreignObject.setAttribute("x", "6");
                foreignObject.setAttribute("y", "5");
                foreignObject.setAttribute("width", width - 12);
                foreignObject.setAttribute("height", height - 10);

                // Create div for our HTML to live
                let divElement = document.createElement("div");
                divElement.setAttribute("id", "popup.div." + node.id);
                divElement.style.width = width;
                divElement.style.height = height;
                divElement.innerHTML = '<p>' + node.id + '</p>'

                // put it all together
                foreignObject.appendChild(divElement);
                group.appendChild(rect);
                group.appendChild(foreignObject);
                svgElement.appendChild(group);

                //------------------------------------------------------------------------------------------------
                // Event Handlers
                //------------------------------------------------------------------------------------------------
                let dblClicked = false;
                let mousedownLongTime = false;
                let autoCloseTimeout = null;

                svgElement.addEventListener('click', function() {
                    setTimeout(function() { 
                        if (!dblClicked && !mousedownLongTime) { 
                            if (group.style.display === 'block') {
                                if (autoCloseTimeout) { clearTimeout(autoCloseTimeout) };
                                group.style.display = 'none';
                            } else {
                                svgElement.parentNode.appendChild(svgElement);  // move to end (top)
                                group.style.display = 'block';
                                if (autoCloseTime > 0) {
                                    autoCloseTimeout = setTimeout(() => { group.style.display = 'none'; }, autoCloseTime);
                                }
                            }
                        } 
                    }, 250);
                });

                svgElement.addEventListener('dblclick', function(event) {
                    dblClicked = true;
                    setTimeout(() => { dblClicked = false; }, 300);
                    event.stopPropagation();
                });

                let mousedownTimer = null;
                svgElement.addEventListener('mousedown', function(event) {
                    mousedownTimer = setTimeout(() => { mousedownLongTime = true; }, 150);
                }, true);

                svgElement.addEventListener('mouseup', function(event) {
                    if (mousedownTimer) { clearTimeout(mousedownTimer) };
                    setTimeout(() => { mousedownLongTime = false; }, 300);
                }, true);

            }

        })

    });

}

2: SETUP YOUR HTML SCRIPT

Setup your custom node HTML script to import the popup like this:

HTML File

<script src="/resources/<YourNodeName>/AddPopupSupport.js"></script>

<script type="text/javascript">

    //---------------------------------------
    // make this an autorun function
    //---------------------------------------
    (function() {
        RED.nodes.registerType('<YourNodeName>',{ 

        // your typical custom code goes here

        });
    })();

    //---------------------------------------
    //These are the only new lines to add
    //---------------------------------------
    AddPopupSupport('<YourNodeName>', {width:160, height:80, autoCloseTime:10000});
    // width, height and autoCloseTime are optional

</script>

// the rest of your HTML file goes here

3: SEND STATUS TEXT FROM BACKEND

Similar to how you send status using node.status
use this to send HTML formatted status messages to the popup from the backend JS:

JS File

            RED.comms.publish('<YourNodeName>/popupstatustext', {
                nodeID: node.id,
                popupHTML: "This is a test<br>For "+ node.id,
            });

It seems to work for all my use cases.

I'm probably breaking a lot of accepted practices with this and there's probably better solutions out there but I haven't found it.

I'm just a novice and took some shortcuts with formatting and such too. I know this can be done cleaner but hey it works. :slight_smile:

Have fun.

edit: Updated the code with some bug fixes that were pointed out.

3 Likes

Hi,
Thanks for sharing this wonder :heart_eyes:
FYI, the PR #3675 with this functionality also exists but needs to be reworked.
PS: yours is visually more beautiful :kissing_heart:

Great to see someone having a go at this and it looks pretty good.

You could, if you wanted, do some optimisation since the Editor not only has jQuery but also D3 available. Using D3 should allow you to simplify the code somewhat I think.

This opens a new design space for me.

I sent this popupHTML to a popup and it opened Big Buck Bunny from youtube:

            RED.comms.publish('<nodetype>/popupstatustext', {
                nodeID: node.id,
                popupHTML: `
                    <iframe
                    width="560"
                    height="315"
                    src="https://www.youtube.com/embed/aqz-KE-bpKQ"
                    frameborder="0"
                    allowfullscreen
                    ></iframe>
              `
            });

The code would need some work to get it sized properly but totally doable.

I can see doing something similar for a fancy INJECT node where you could define a popup with an input box or option buttons to click. The comms would have to be setup in reverse though.

Or a hover popup to see Global or Context variable values.

Or open a detailed Users Manual, etc...

Fun stuff.

2 Likes

Hi @Mike7833

There have been some threads on the forum along similar topics recently.

We're keen to find a more formalised way to allow nodes to "extend" their appearance, whilst trying to keep some amount of consistency and style guidelines. We don't want to open the flood gates of nodes taking over the editor.

Right now, all of the nodes that do these sorts of things are using undocumented parts of the editor code and make assumptions about how the UI works. These are things that could change (and this, break these nodes) in any release as we make internal improvements.

So this is just my obligatory word of caution, and hope that we can focus some attention on what the core could do to more formally support these types of things, as well as establish some guidelines for how they should be used to avoid getting a muddle of a result.

2 Likes

Hello @knolleary

It would be nice if the graphic elements of a popup were already enshrined similar to the existing status graphic elements. Or maybe have an enshrined function to create the elements.

The backend would need a function similar to node.status(...) which the editor would then need to handle and update the popup(s). Two-way communication would be nice so designers could have their popup send data to the backend. If the popup was an HTML element then have an enshrined javascript function they could call or something similar.

It sounds very similar to the exist node.status but with more parameters and two-way comms.

Anyway, I don't really know the implications or complexity of adding something like this. May be easy to conceptualize but hard to implement.

2 Likes

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.