Editable TextBlocks in a context menu

Hello,

I would like the user to right-click a node, get a context menu with two text fields into which she can enter data, and then click a button that modifies the model (according to the TextBlock values) and closes the context menu.

I tried setting contextMenu to make an adornment with two TextBlocks and a button, but clicking to select the text fields closes the menu. Intercepting the click event does not seem to offer anything immediately.

Is there a pattern/example for doing this? Is it possible to use contextMenu, or do I need to do something else?

Thanks
F

Try this:

    myDiagram.nodeTemplate.contextMenu =
      $(go.Adornment, "Auto",
        $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
        $(go.Panel, "Vertical",
          { defaultStretch: go.GraphObject.Horizontal, padding: 5 },
          $(go.TextBlock, "Enter texts:", { font: "bold 11pt sans-serif" }),
          $(go.TextBlock, {
            name: "T1", isActionable: true, background: "whitesmoke", margin: 1,
            click: function(e, tb) {
              e.diagram.commandHandler.editTextBlock(tb);
            }
          }, new go.Binding("text", "text")),
          $(go.TextBlock, {
            name: "T2", isActionable: true, background: "whitesmoke", margin: 1,
            click: function(e, tb) {
              e.diagram.commandHandler.editTextBlock(tb);
            }
          }, new go.Binding("text", "text2")),
          $(go.Panel, "Horizontal",
            { stretch: go.GraphObject.None, alignment: go.Spot.Center },
            $("Button",
              $(go.TextBlock, "Accept"),
              {
                margin: 2,
                click: function(e, button) {
                  var infopanel = button.part;
                  e.diagram.startTransaction();
                  e.diagram.model.setDataProperty(infopanel.data, "text", infopanel.findObject("T1").text);
                  e.diagram.model.setDataProperty(infopanel.data, "text2", infopanel.findObject("T2").text);
                  e.diagram.remove(infopanel);
                  e.diagram.commitTransaction("changed some text");
                }
              }),
            $("Button",
              $(go.TextBlock, "Cancel"),
              {
                margin: 2,
                click: function(e, button) {
                  var infopanel = button.part;
                  e.diagram.startTransaction();
                  e.diagram.remove(infopanel);
                  e.diagram.commitTransaction("cancel info");
                }
              })
          )
        )
      );

    myDiagram.nodeTemplate.contextClick = function(e, node) {
      e.handled = true;  // don't execute standard behavior, which shows the obj.contextMenu
      var infopanel = node.contextMenu;
      infopanel.position = e.documentPoint;
      e.diagram.add(infopanel);
      infopanel.data = node.data;
    };

I did not use TwoWay Binding on the TextBlocks, just to show that the changes made in the “Accept” Button’s click event handler could be completely independent of the initial TextBlock values.

Two Tools cannot operate simultaneously, so rather than using the regular ContextMenuTool, the node’s contextClick event handler manually shows the context menu. Note how it sets InputEvent.handled to true to prevent the normal behavior of having the ContextMenuTool run to show the GraphObject.contextMenu.

The click event on the TextBlocks explicitly invoke CommandHandler.editTextBlock to start the TextEditingTool. The click events would not normally be noticed for parts in the “Tool” layer, but setting GraphObject.isActionable will allow clicks to be handled normally.

We ought to redesign things so that this is more natural to implement.

Hi Walter,

thanks very much, that works for me. Interesting solution.

There seem to be a couple of limitations:

unlike a real contextMenu, it isn’t closed when it loses focus (e.g. by the user clicking elsewhere). I considered handling this manually – but I wasn’t able to use diagram.findPartByKey to return the menu (I guess findPartByX is only meant for objects that appear in the model).

Selecting something else can cause its adornment (e.g. its border, indicating it has been focused) to draw over this context menu. I assume this is only apparent because of the above.

The diagram size does not expand to accomodate the new infoPanel, so if clicking at the edge of the screen, it is possible for most of the panel to be invisible and inaccessible.
These are not major problems for me at this time.

Yes, findPartByKey only works if the Part is in the Model. You will have to remember a reference to the context menu that is currently up so you can easily remove it later.

Or, couldn’t you just use findPartByKey to return the Node and not the menu, and then call:

Diagram.remove(node.contextMenu)

Or is there some reason that wouldn’t work?

Thanks Simon, it’s good to get the confirmation. These are not critical issues for me, but it is an interesting question, so I will comment how I understand it. Unless there’s something I’m missing, the problem is that any subsequent click event outside of Walter’s infopanel has no knowledge of the infopanel, whether it exists, and to which node it was attached. I think the two approaches you mention are this.

  1. On every outside click, iterate over all nodes and remove any contextMenus that are found. (In fact, my contextMenus are attached to itemTemplates not nodes, so it would be necessary to iterate yet more deeply.)

  2. Store a reference to the open infopanel (or to the node in question) in a global variable/in the model/in the diagram object, check this variable on every click event, and close the panel if it happens to exist.

I don’t want to implement either of these ad hoc, weighing up additional maintenance required versus the feature’s importance. I suppose GoJS already has routines to handle exactly the automatic closing of ‘authentic’ contextMenus; maybe there is a way of registering our imitation infopanel with that auto-closing service? But I don’t know it. By Walter’s comment, I assume that all of these questions would be most satisfactorily resolved with a natural implementation of editable TextBlocks in contextMenus.

maybe there is a way of registering our imitation infopanel with that auto-closing service?

ContextMenuTool.hideContextMenu is only called if a “real” context menu has is already been shown (if the tool has already been activated). You could override something else, like standardMouseClick, but you’d be doing the same work (looking for your context menu or context menus and then removing them).

If you only have one context menu available at a time, then it gets easier, just keep one reference to it and “destroy” it every time some criteria happens, such as a click on the diagram background or the opening of a new context menu.