Create a link when the adornment (shown only on hover) is visible

I’ve created an adornment for a node, which shows only on mouse hover (followed the documentation). However, I’ve lost the ability to create a new link from that node to another node. If I remove the adornment, then it works as expected.

How do I create a link from one node to another with the adornment visible?

Note: My adornment needs or has no actionable items.

It would have been useful if you had shown a small screenshot of your Adornment along with how it is implemented.

My guess is that the area in the Adornment covering the port(s) are opaque to mouse events because you are using a “transparent” brush instead of a null brush, perhaps as the background or Shape.fill in your Adornment.

Here’s what my adornment looks like:

It’s a simple TextBlock.

And you’re right, if I set the background to null instead of transparent, then I can at least create links from just above the adornment and the sides. However, when I make it null, the adornment misbehaves - sometimes it continues to show even after mouse hover. With transparent as the background, it works very well.

However, even with null as the background, I’m unable to create a link from right above the adornment.

Here’s how I’m creating my adornment:

var nodeHoverAdornment =
            $$(go.Adornment, "Spot",
                {
                    background: "transparent",
                    mouseLeave: function (e, obj) {
                        var ad = obj.part;
                        ad.adornedPart.removeAdornment("mouseHover");
                    }
                },
                $$(go.Placeholder,
                    {
                        background: "transparent",  // to allow this Placeholder to be "seen" by mouse events
                        isActionable: false,  // needed because this is in a temporary Layer
                        click: function (e, obj) {
                            var node = obj.part.adornedPart;
                            node.diagram.select(node);
                            obj.part.click();
                        }
                    }),
                $$("TextBlock",
                    {
                        alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Bottom,
                        text:"my adornment"
                    },
                    {
                        mouseHover: function (e, obj) {
                            console.log("hover test");
                        }
                    })
            );

So you took that from Buttons that show on Hover, didn’t you? OK, let’s modify it to behave more like what I think you want:

    // this is shown by the mouseHover event handler
    var nodeHoverAdornment =
      $(go.Adornment, "Spot",
        { pickable: false },
        $(go.Placeholder),
        $("Button",
          { alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Bottom },
          { click: function(e, obj) { alert("Hi!"); } },
          $(go.TextBlock, "Hi!"))
      );

    // define a simple Node template
    myDiagram.nodeTemplate =
      $(go.Node, "Auto",  // the Shape will go around the TextBlock
        $(go.Shape, "RoundedRectangle", { strokeWidth: 0, portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer" },
          // Shape.fill is bound to Node.data.color
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          { margin: 8 },  // some room around the text
          // TextBlock.text is bound to Node.data.key
          new go.Binding("text", "key")),
        { // show the Adornment when a mouseHover event occurs
          mouseHover: function(e, obj) {
            var node = obj.part;
            nodeHoverAdornment.adornedObject = node;
            node.addAdornment("mouseHover", nodeHoverAdornment);
          },
          // hide the Adornment when the mouse leaves the area of the node,
          // because the hover adornment is covers part of the node
          mouseLeave: function(e, node) {
            if (!node.actualBounds.containsPoint(e.documentPoint)) node.removeAdornment("mouseHover");
          }
        }
      );

This uses one of the buttons from the original sample instead of a simple TextBlock as what you are using, but that doesn’t matter for demonstrating the behavior. Basically the Adornment is quite straight-forward.

The only tricky point is that there is a smarter GraphObject.mouseLeave event handler on the node. You don’t want to blindly call Part.removeAdornment when the mouse leaves the node, because it leaves the node when entering the Adornment! So I’ve added a check to make sure the mouse isn’t still within the bounds of the node.

Thanks for that example, now I can create links from around the adornment, however, I’m still unable to create a link by clicking directly above the adornment. It moves the node instead.

I’d like to create a link by clicking on the adornment as well.

On a side note, I had to add the same code in the mouseLeave listener of the node template to the adornment too - otherwise, if the mouse exited the node directly from the adornment, the adornment would still be visible (as the mouseLeave listener is not called on the node when the mouse enters the adornment) .

OK, I have moved the setting of GraphObject.pickable to false to be on the whole Adornment. Try it on yours.

But then the Adornment won’t get any mouseLeave event. Yet you might be able to simplify the Node.mouseLeave event handler not to bother checking the InputEvent.documentPoint being within the Node.actualBounds.

That did it! Making the entire thing pickable as false has made it linkable over the adornment too.

However, there’s this very peculiar thing that I’ve noticed: if the mouse exits the node at the normal speed, then the adornment is dismissed as expected; but if the mouse exits the node very very slowly, then the adornment is not dismissed. I assume that this is because the mouseLeave is not triggered if the movement is too slow.

My solution to the above problem would be to check if the mouse is about to leave the node (say, 20 pixels from each side), and then dismiss the adornment early on itself.

Is there a better solution to this?

I just tried it, and I’m unable to reproduce the behavior you experienced, and I have not put in any hack checking a distance of 20 from the sides of the node.

I’m just using the code that I posted above.

Oh okay. Let me try stripping my code down to just that and seeing where I’m going wrong.

Is it possible for the adornment to get the mouseHover, mouseLeave etc events? I’d like to change the colour of the adornment on hover.

Edit: It should still pass all the events back to the node, so that links can be created. So maybe a proxy - just call the adornments listeners, and pass it back to the node.

Edit 2:
I solved it by updating my node’s mouseHover to:

mouseHover: function (e, obj) {
    var node = obj.part;
    nodeHoverAdornment.adornedObject = node;
    node.addAdornment("mouseHover", nodeHoverAdornment);
    // Since the adornment's mouseHover will not be called (pickable=false),
    // try to see if the mouse is inside the adornment's shapes here,
    // and if it is, call the shape's mouseHover function.
    nodeHoverAdornment.elements.iterator.each(function (elem) {
        var bounds = elem.actualBounds;
        var nBounds = obj.actualBounds;
        var isInside = new go.Rect(
            bounds.x + nBounds.x,
            bounds.y + nBounds.y,
            bounds.width,
            bounds.height
        ).containsPoint(e.documentPoint);
        if (isInside) {
            if (elem.mouseHover) {
                elem.mouseHover(e, obj);
            }
        }
    });
}

That’s not going to work reliably, because the coordinate system for the elements of a Panel are in the Panel’s coordinate system, not document coordinates.

Isn’t it the same usage as the below?

if (!node.actualBounds.containsPoint(e.documentPoint)) node.removeAdornment("mouseHover");

I’ve implemented it, and after a little bit of testing, it seems to work fine.

Is there a better way to do this? I agree that this is hacking it a little too much.

I don’t know what to say. Panels really do have different coordinate systems than their containers.

I actually changed my implementation to using a button, from some inspiration here: Selection Adornment Buttons