Is there a way to display a nested context menu?

I tried the code below for a nested context menu, but it didn’t work.

Code:

this.myDiagram.contextMenu =
      $("ContextMenu",
        $("ContextMenuButton",
          {
            visible: true,
            "ButtonBorder.fill": "#E9E9E9",
            "_buttonFillOver": "lightgray",
            click: downloadSVG
          },
          $(go.TextBlock, "Download SVG"),
        ),
        $("ContextMenuButton",
          {
            visible: true,
            "ButtonBorder.fill": "#E9E9E9",
            "_buttonFillOver": "lightgray",
            click: resetDiagram
          },
          $(go.TextBlock, "Reset Diagram"),
        ),
        $("ContextMenuButton",
          {
            visible: true,
            "ButtonBorder.fill": "#E9E9E9",
            "_buttonFillOver": "lightgray",
            click: (e:any, obj:any) => {
              const submenu = $( "ContextMenu",
                $("ContextMenuButton",
                  {
                    visible: true,
                    "ButtonBorder.fill": "#E9E9E9",
                    "_buttonFillOver": "lightgray",
                    click: abc
                  },
                  $(go.TextBlock, "abc"),
                ),
                $("ContextMenuButton",
                  {
                    visible: true,
                    "ButtonBorder.fill": "#E9E9E9",
                    "_buttonFillOver": "lightgray",
                    click: def
                  },
                  $(go.TextBlock, "def"),
                ),
                $("ContextMenuButton",
                  {
                    visible: true,
                    "ButtonBorder.fill": "#E9E9E9",
                    "_buttonFillOver": "lightgray",
                    click: ghi
                  },
                  $(go.TextBlock, "ghi"),
                ),
              );
              obj.part.diagram.showContextMenu(submenu, obj.part);
            }
          },
          $(go.TextBlock, "Diagram Data"),
        ),
      );

There is no Diagram.showContextMenu method. There is one on CommandHandler, but the argument needs to be the GraphObject (or Diagram) that has a contextMenu property that is shown, not a completely new Adornment not associated with any GraphObject (presumably a Node in your case).

Furthermore, if you put the context menu on the “ContextMenuButton”'s GraphObject.contextMenu property and passed that button to CommandHandler.showContextMenu, it would automatically be removed because the top-level context menu would be removed.

Normally programmers use HTML context menus when wanting to show submenus or other more complicated appearances or behaviors. See the example in the Custom Context Menu sample: Custom HTML Context Menu for Nodes and Diagram | GoJS Diagramming Library – hover over the “Color” context menu button and see the submenu it shows.

Hmmm, try this code, which seems to work for a nested context menu that is an Adornment, but doesn’t let the user try different submenus without hiding the top-level context menu.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">

class CustomContextMenuTool extends go.ContextMenuTool {
  _keepShowing = false;
  _stillShowing = [];

  maybeStopTool(currobj) {  // override undocumented method
    const showing = this._keepShowing;
    this._keepShowing = false;
    if (currobj && showing) {
      this._stillShowing.push(currobj.part);
      return;
    }
    super.maybeStopTool(currobj);
  }

  hideContextMenu() {
    if (this._keepShowing) return;
    this._stillShowing.forEach(ad => this.diagram.remove(ad));
    this._stillShowing = [];
    super.hideContextMenu();
  }
}

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      contextMenuTool: new CustomContextMenuTool(),
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

myDiagram.nodeTemplate.contextMenu =
  go.GraphObject.build("ContextMenu")
    .add(
      go.GraphObject.build("ContextMenuButton", {
          click: (e, button) => console.log("simple command on " + button.part.adornedPart.data.text)
        })
        .add(new go.TextBlock("simple command")),
      go.GraphObject.build("ContextMenuButton", {
          contextMenu:
            go.GraphObject.build("ContextMenu")
              .add(
                go.GraphObject.build("ContextMenuButton", {
                    click: (e, button) => console.log("submenu command on " + button.part.adornedPart.data.text)
                  })
                  .add(new go.TextBlock("submenu command")),
                go.GraphObject.build("ContextMenuButton", {
                    click: (e, button) => console.log("submenu command 2 on " + button.part.adornedPart.data.text)
                  })
                  .add(new go.TextBlock("submenu command 2")),
              ),
          click: (e, button) => {
            // depend on override of ContextMenuTool.maybeStopTool not to stop the tool
            e.diagram.currentTool._keepShowing = true;
            e.diagram.commandHandler.showContextMenu(button);
          }
        })
        .add(new go.TextBlock("shows submenu")),
      go.GraphObject.build("ContextMenuButton", {
          contextMenu:
            go.GraphObject.build("ContextMenu")
              .add(
                go.GraphObject.build("ContextMenuButton", {
                    click: (e, button) => console.log("subTWO 1st command on " + button.part.adornedPart.data.text)
                  })
                  .add(new go.TextBlock("subTWO 1st command")),
                go.GraphObject.build("ContextMenuButton", {
                    click: (e, button) => console.log("subTWO 2nd command on " + button.part.adornedPart.data.text)
                  })
                  .add(new go.TextBlock("subTWO 2nd command")),
              ),
          click: (e, button) => {
            // depend on override of ContextMenuTool.maybeStopTool not to stop the tool
            e.diagram.currentTool._keepShowing = true;
            e.diagram.commandHandler.showContextMenu(button);
          }
        })
        .add(new go.TextBlock("submenu TWO")),
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue" },
  { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen" },
  { key: 4, text: "Delta", color: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 2 },
  { from: 3, to: 4 },
  { from: 4, to: 1 }
]);
  </script>
</body>
</html>