Two Different Context Menu

I’m a GOJS beginner! It’s really nice to have this opportunity to ask questions on this platform.
I have already created one custom Context Menu without any problems, but I want to show different Context Menu when users right-click on different nodes.
And when the user right-click on the context menu will add new node to another lane.
How to achieve it in this case? I will appreciate if there is an example. :)

Just define different context menus. Basic GoJS Sample defines several different ones.

1 Like

Thanks for your information! I’ll give it a try!

I solved the context menu problem! Thanks for your help!
Besides, I want to ask how to add new node (to another lane or current lane depends on which lane selected) from current node (from current lane) when the user click on context menu?

You can call Model.addNodeData with a new data object. This example does something like that in the addNodeAndLink function, which is triggered by a click function on a selection adornment: Page Flow.

1 Like

Thanks for your attention!
I saw the similar example few days ago, but I not sure about how to apply it to my case.
Here’s the problems:

  1. showContextMenu() didn’t work as i expect (I want when i right-click on nodes in Process lane “Lane4” can add new node, other lane’s node only can delete itself.)

Maybe it’s because == “Lane4” is not appropriate, because when i use !== “Lane4” and all the function shows.

  1. I want add node to corresponding lane by parse parameter addNodeAndLink(e, obj, “Lane__”)
    case “add_process”: addNodeAndLink(e, obj, “Lane4”); break;
    case “add_input”: addNodeAndLink(e, obj, “Lane3”); break;
    case “add_output”: addNodeAndLink(e, obj, “Lane5”); break;
    case “add_system”: addNodeAndLink(e, obj, “Lane2”); break;
    case “add_participants”: addNodeAndLink(e, obj, “Lane1”); break;

but i don’t know if this way works and how to add to the lane it belongs

  1. Participants Lane (lane1) and System Lane(lane 2) don’t shows the link but I have to know it belongs to which process node.

BTW the delete function have completed.

Here’s my code:

 <!DOCTYPE html>
  <title>Swim Lanes (vertical)</title>
  <style type="text/css">
    /* CSS for the traditional context menu */
    #contextMenu {
      z-index: 10002;
      position: absolute;
      left: 5px;
      border: 1px solid #444;
      background-color: #F5F5F5;
      display: none;
      box-shadow: 0 0 10px rgba( 0, 0, 0, .4 );
      font-size: 12px;
      font-family: sans-serif;
      font-weight: bold;
      #contextMenu ul {
        list-style: none;
        top: 0;
        left: 0;
        margin: 0;
        padding: 0;
      #contextMenu li a {
        position: relative;
        min-width: 60px;
        color: #444;
        display: inline-block;
        padding: 6px;
        text-decoration: none;
        cursor: pointer;
      #contextMenu li:hover {
        background: #CEDFF2;
        color: #EEE;
      #contextMenu li ul li {
        display: none;
        #contextMenu li ul li a {
          position: relative;
          min-width: 60px;
          padding: 6px;
          text-decoration: none;
          cursor: pointer;
      #contextMenu li:hover ul li {
        display: block;
        margin-left: 0px;
        margin-top: 0px;
  <script src="site/release/go.js"></script>
  <script id="code">
    // These parameters need to be set before defining the templates.
    var MINLENGTH = 200;  // this controls the minimum length of any swimlane
    var MINBREADTH = 20;  // this controls the minimum breadth of any non-collapsed swimlane
    // some shared functions
    // this may be called to force the lanes to be laid out again
    function relayoutLanes() {
      myDiagram.nodes.each(function(lane) {
        if (!(lane instanceof go.Group)) return;
        if (lane.category === "Pool") return;
        lane.layout.isValidLayout = false;  // force it to be invalid
    // this is called after nodes have been moved or lanes resized, to layout all of the Pool Groups again
    function relayoutDiagram() {
      myDiagram.findTopLevelGroups().each(function(g) { if (g.category === "Pool") g.layout.invalidateLayout(); });
    // compute the minimum size of a Pool Group needed to hold all of the Lane Groups
    function computeMinPoolSize(pool) {
      // assert(pool instanceof go.Group && pool.category === "Pool");
      var len = MINLENGTH;
      pool.memberParts.each(function(lane) {
        // pools ought to only contain lanes, not plain Nodes
        if (!(lane instanceof go.Group)) return;
        var holder = lane.placeholder;
        if (holder !== null) {
          var sz = holder.actualBounds;
          len = Math.max(len, sz.height);
      return new go.Size(NaN, len);
    // compute the minimum size for a particular Lane Group
    function computeLaneSize(lane) {
      // assert(lane instanceof go.Group && lane.category !== "Pool");
      var sz = computeMinLaneSize(lane);
      if (lane.isSubGraphExpanded) {
        var holder = lane.placeholder;
        if (holder !== null) {
          var hsz = holder.actualBounds;
          sz.width = Math.max(sz.width, hsz.width);
      // minimum breadth needs to be big enough to hold the header
      var hdr = lane.findObject("HEADER");
      if (hdr !== null) sz.width = Math.max(sz.width, hdr.actualBounds.width);
      return sz;
    // determine the minimum size of a Lane Group, even if collapsed
    function computeMinLaneSize(lane) {
      if (!lane.isSubGraphExpanded) return new go.Size(1, MINLENGTH);
      return new go.Size(MINBREADTH, MINLENGTH);
    // define a custom ResizingTool to limit how far one can shrink a lane Group
    function LaneResizingTool() {;
    go.Diagram.inherit(LaneResizingTool, go.ResizingTool);
    LaneResizingTool.prototype.isLengthening = function() {
      return (this.handle.alignment === go.Spot.Bottom);
    LaneResizingTool.prototype.computeMinPoolSize = function() {
      var lane = this.adornedObject.part;
      // assert(lane instanceof go.Group && lane.category !== "Pool");
      var msz = computeMinLaneSize(lane);  // get the absolute minimum size
      if (this.isLengthening()) {  // compute the minimum length of all lanes
        var sz = computeMinPoolSize(lane.containingGroup);
        msz.height = Math.max(msz.height, sz.height);
      } else {  // find the minimum size of this single lane
        var sz = computeLaneSize(lane);
        msz.width = Math.max(msz.width, sz.width);
        msz.height = Math.max(msz.height, sz.height);
      return msz;
    LaneResizingTool.prototype.resize = function(newr) {
      var lane = this.adornedObject.part;
      if (this.isLengthening()) {  // changing the length of all of the lanes
        lane.containingGroup.memberParts.each(function(lane) {
          if (!(lane instanceof go.Group)) return;
          var shape = lane.resizeObject;
          if (shape !== null) {  // set its desiredSize length, but leave each breadth alone
            shape.height = newr.height;
      } else {  // changing the breadth of a single lane, newr);
      relayoutDiagram();  // now that the lane has changed size, layout the pool again
    // end LaneResizingTool class
    // define a custom grid layout that makes sure the length of each lane is the same
    // and that each lane is broad enough to hold its subgraph
    function PoolLayout() {;
      this.cellSize = new go.Size(1, 1);
      this.wrappingColumn = Infinity;
      this.wrappingWidth = Infinity;
      this.isRealtime = false;  // don't continuously layout while dragging
      this.alignment = go.GridLayout.Position;
      // This sorts based on the location of each Group.
      // This is useful when Groups can be moved up and down in order to change their order.
      this.comparer = function(a, b) {
        var ax = a.location.x;
        var bx = b.location.x;
        if (isNaN(ax) || isNaN(bx)) return 0;
        if (ax < bx) return -1;
        if (ax > bx) return 1;
        return 0;
    go.Diagram.inherit(PoolLayout, go.GridLayout);
    PoolLayout.prototype.doLayout = function(coll) {
      var diagram = this.diagram;
      if (diagram === null) return;
      var pool =;
      if (pool !== null && pool.category === "Pool") {
        // make sure all of the Group Shapes are big enough
        var minsize = computeMinPoolSize(pool);
        pool.memberParts.each(function(lane) {
          if (!(lane instanceof go.Group)) return;
          if (lane.category !== "Pool") {
            var shape = lane.resizeObject;
            if (shape !== null) {  // change the desiredSize to be big enough in both directions
              var sz = computeLaneSize(lane);
              shape.width = (!isNaN(shape.width)) ? Math.max(shape.width, sz.width) : sz.width;
              shape.height = (isNaN(shape.height) ? minsize.height : Math.max(shape.height, minsize.height));
              var cell = lane.resizeCellSize;
              if (!isNaN(shape.width) && !isNaN(cell.width) && cell.width > 0) shape.width = Math.ceil(shape.width / cell.width) * cell.width;
              if (!isNaN(shape.height) && !isNaN(cell.height) && cell.height > 0) shape.height = Math.ceil(shape.height / cell.height) * cell.height;
      // now do all of the usual stuff, according to whatever properties have been set on this GridLayout, coll);
    // end PoolLayout class
    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;
      myDiagram =
        $(go.Diagram, "myDiagramDiv",

            // use a custom ResizingTool (along with a custom ResizeAdornment on each Group)
            resizingTool: new LaneResizingTool(),
            // use a simple layout that ignores links to stack the top-level Pool Groups next to each other
            layout: $(PoolLayout),
            // don't allow dropping onto the diagram's background unless they are all Groups (lanes or pools)
             "draggingTool.isEnabled": false,
            mouseDragOver: function(e) {
              if (!e.diagram.selection.all(function(n) { return n instanceof go.Group; })) {
                e.diagram.currentCursor = 'not-allowed';
            mouseDrop: function(e) {
              if (!e.diagram.selection.all(function(n) { return n instanceof go.Group; })) {
            // a clipboard copied node is pasted into the original node's group (i.e. lane).
            "commandHandler.copiesGroupKey": true,
            // automatically re-layout the swim lanes after dragging the selection
            "SelectionMoved": relayoutDiagram,  // this DiagramEvent listener is
            "SelectionCopied": relayoutDiagram, // defined above
            "animationManager.isEnabled": true,

            // enable undo & redo
            "undoManager.isEnabled": true
      // This is the actual HTML context menu:
      var cxElement = document.getElementById("contextMenu");
      // Since we have only one main element, we don't have to declare a hide method,
      // we can set mainElement and GoJS will hide it automatically
      var myContextMenu = $(go.HTMLInfo, {
        show: showContextMenu,
        mainElement: cxElement
      // this is a Part.dragComputation function for limiting where a Node may be dragged
      function stayInGroup(part, pt, gridpt) {
        // don't constrain top-level nodes
        var grp = part.containingGroup;
        if (grp === null) return pt;
        // try to stay within the background Shape of the Group
        var back = grp.resizeObject;
        if (back === null) return pt;
        // allow dragging a Node out of a Group if the Shift key is down
        if (part.diagram.lastInput.shift) return pt;
        var p1 = back.getDocumentPoint(go.Spot.TopLeft);
        var p2 = back.getDocumentPoint(go.Spot.BottomRight);
        var b = part.actualBounds;
        var loc = part.location;
        // find the padding inside the group's placeholder that is around the member parts
        var m = grp.placeholder.padding;
        // now limit the location appropriately
        var x = Math.max(p1.x + m.left, Math.min(pt.x, p2.x - m.right - b.width - 1)) + (loc.x - b.x);
        var y = Math.max(p1.y +, Math.min(pt.y, p2.y - m.bottom - b.height - 1)) + (loc.y - b.y);
        return new go.Point(x, y);

      myDiagram.nodeTemplate =
        $(go.Node, "Auto",
          { contextMenu: myContextMenu },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape, "Rectangle",
            { fill: "white", portId: "", cursor: "pointer", fromLinkable: false, toLinkable: false }),
          $(go.TextBlock, { margin: 5 },
            new go.Binding("text", "key")),
          { dragComputation: stayInGroup } // limit dragging of Nodes to stay within the containing Group, defined above
      function groupStyle() {  // common settings for both Lane and Pool Groups
        return [
            layerName: "Background",  // all pools and lanes are always behind all nodes and links
            background: "transparent",  // can grab anywhere in bounds
            movable: false, // allows users to re-order by dragging
            copyable: false,  // can't copy lanes or pools
            avoidable: false,  // don't impede AvoidsNodes routed Links
            minLocation: new go.Point(-Infinity, NaN),  // only allow horizontal movement
            maxLocation: new go.Point(Infinity, NaN)
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
      // hide links between lanes when either lane is collapsed
      function updateCrossLaneLinks(group) {
        group.findExternalLinksConnected().each(function(l) {
          l.visible = (l.fromNode.isVisible() && l.toNode.isVisible());
      // each Group is a "swimlane" with a header on the left and a resizable lane on the right
      myDiagram.groupTemplate =
        $(go.Group, "Vertical", groupStyle(),
            selectionObjectName: "SHAPE",  // selecting a lane causes the body of the lane to be highlit, not the label
            resizable: true, resizeObjectName: "SHAPE",  // the custom resizeAdornmentTemplate only permits two kinds of resizing
            layout: $(go.LayeredDigraphLayout,  // automatically lay out the lane's subgraph
                isInitial: false,  // don't even do initial layout
                isOngoing: false,  // don't invalidate layout when nodes or links are added or removed
                direction: 90,
                columnSpacing: 10,
                layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource
            computesBoundsAfterDrag: true,  // needed to prevent recomputing Group.placeholder bounds too soon
            computesBoundsIncludingLinks: false,  // to reduce occurrences of links going briefly outside the lane
            computesBoundsIncludingLocation: true,  // to support empty space at top-left corner of lane
            handlesDragDropForMembers: true,  // don't need to define handlers on member Nodes and Links
            mouseDrop: function(e, grp) {  // dropping a copy of some Nodes and Links onto this Group adds them to this Group
              if (!e.shift) return;  // cannot change groups with an unmodified drag-and-drop
              // don't allow drag-and-dropping a mix of regular Nodes and Groups
              if (!e.diagram.selection.any(function(n) { return n instanceof go.Group; })) {
                var ok = grp.addMembers(grp.diagram.selection, true);
                if (ok) {
                } else {
              } else {
            subGraphExpandedChanged: function(grp) {
              var shp = grp.resizeObject;
              if (grp.diagram.undoManager.isUndoingRedoing) return;
              if (grp.isSubGraphExpanded) {
                shp.width = grp._savedBreadth;
              } else {
                grp._savedBreadth = shp.width;
                shp.width = NaN;
          new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
          // the lane header consisting of a Shape and a TextBlock
          $(go.Panel, "Horizontal",
              name: "HEADER",
              angle: 0,  // maybe rotate the header to read sideways going up
              alignment: go.Spot.Center
            $(go.Panel, "Horizontal",  // this is hidden when the swimlane is collapsed
              new go.Binding("visible", "isSubGraphExpanded").ofObject(),
              $(go.Shape, "Diamond",
                { width: 8, height: 8, fill: "white" },
                new go.Binding("fill", "color")),
              $(go.TextBlock,  // the lane label
                { font: "bold 13pt sans-serif", editable: true, margin: new go.Margin(2, 0, 0, 0) },
                new go.Binding("text", "text").makeTwoWay())
            $("SubGraphExpanderButton", { margin: 5 })  // but this remains always visible!
          ),  // end Horizontal Panel
          $(go.Panel, "Auto",  // the lane consisting of a background Shape and a Placeholder representing the subgraph
            $(go.Shape, "Rectangle",  // this is the resized object
              { name: "SHAPE", fill: "white" },
              new go.Binding("fill", "color"),
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify)),
              { padding: 12, alignment: go.Spot.TopLeft }),
            $(go.TextBlock,  // this TextBlock is only seen when the swimlane is collapsed
                name: "LABEL",
                font: "bold 13pt sans-serif", editable: true,
                angle: 90, alignment: go.Spot.TopLeft, margin: new go.Margin(4, 0, 0, 2)
              new go.Binding("visible", "isSubGraphExpanded", function(e) { return !e; }).ofObject(),
              new go.Binding("text", "text").makeTwoWay())
          )  // end Auto Panel
        );  // end Group
      // define a custom resize adornment that has two resize handles if the group is expanded
      myDiagram.groupTemplate.resizeAdornmentTemplate =
        $(go.Adornment, "Spot",
          $(go.Shape,  // for changing the length of a lane
              alignment: go.Spot.Bottom,
              desiredSize: new go.Size(50, 7),
              fill: "lightblue", stroke: "dodgerblue",
              cursor: "row-resize"
            new go.Binding("visible", "", function(ad) {
              if (ad.adornedPart === null) return false;
              return ad.adornedPart.isSubGraphExpanded;
          $(go.Shape,  // for changing the breadth of a lane
              alignment: go.Spot.Right,
              desiredSize: new go.Size(7, 50),
              fill: "lightblue", stroke: "dodgerblue",
              cursor: "col-resize"
            new go.Binding("visible", "", function(ad) {
              if (ad.adornedPart === null) return false;
              return ad.adornedPart.isSubGraphExpanded;
        $(go.Group, "Auto", groupStyle(),
          { // use a simple layout that ignores links to stack the "lane" Groups next to each other
            layout: $(PoolLayout, { spacing: new go.Size(0, 0) })  // no space between lanes
            { fill: "white" },
            new go.Binding("fill", "color")),
          $(go.Panel, "Table",
            { defaultRowSeparatorStroke: "black" },
            $(go.Panel, "Horizontal",
              { row: 0, angle: 0 },
                { font: "bold 16pt sans-serif", editable: true, margin: new go.Margin(2, 0, 0, 0) },
                new go.Binding("text").makeTwoWay())
              { row: 1 })
      myDiagram.linkTemplate =
          { routing: go.Link.AvoidsNodes, corner: 5 },
          { relinkableFrom: true, relinkableTo: true },
          $(go.Shape, { toArrow: "Standard" })
      // define some sample graphs in some of the lanes
      myDiagram.model = new go.GraphLinksModel(
        [ // node data
          { key: "Pool1", text: "Pool", isGroup: true, category: "Pool" },
          { key: "Lane1", text: "Participants", isGroup: true, group: "Pool1", color: "lightblue", category:"Participants" },
          { key: "Lane2", text: "System", isGroup: true, group: "Pool1", color: "lightgreen", category:"System" },
          { key: "Lane3", text: "Input", isGroup: true, group: "Pool1", color: "lightyellow", category:"Input" },
          { key: "Lane4", text: "Process", isGroup: true, group: "Pool1", color: "orange", category:"Process" },
          { key: "Lane5", text: "Output", isGroup: true, group: "Pool1", color: "lightskyblue", category:"Output" },
          { key: "Start", group: "Lane4"},
          { key: "Collect", group: "Lane4"},
          { key: "Store", group: "Lane4"},
          { key: "Use", group: "Lane4" },
          { key: "Transfer", group: "Lane4" },
          { key: "Delete", group: "Lane4" },
          { key: "End", group: "Lane4" },
          { key:"Input", group:"Lane3"},

        [ // link data
          { from: "Start", to: "Collect" },
          { from: "Collect", to: "Store" },
          { from: "Store", to: "Use" },
          { from: "Use", to: "Transfer" },
          { from: "Transfer",to: "Delete" },
          { from: "Delete", to: "End" },
          { from: "Collect", to:"Input"},
      myDiagram.contextMenu = myContextMenu;
      // We don't want the div acting as a context menu to have a (browser) context menu!
      cxElement.addEventListener("contextmenu", function(e) {
        return false;
      }, false);
      function showContextMenu(obj, diagram, tool) {
        // Show only the relevant buttons given the current state.
        var cmd = diagram.commandHandler;
        document.getElementById("delete").style.display = cmd.canDeleteSelection() ? "block" : "none";
        document.getElementById("add_process").style.display=( == "Lane4" ? "block" : "none");
        document.getElementById("add_input").style.display=( =='Lane4' ? "block" : "none");
        document.getElementById("add_output").style.display=( =='Lane4' ? "block" : "none");
        document.getElementById("add_system").style.display=( =='Lane4' ? "block" : "none");
        document.getElementById("add_participants").style.display=( =='Lane4' ? "block" : "none");

        // Now show the whole context menu element = "block";
        // we don't bother overriding positionContextMenu, we just do it here:
        var mousePt = diagram.lastInput.viewPoint; = mousePt.x + "px"; = mousePt.y + "px";
      // force all lanes' layouts to be performed
    }  // end init
    // Show the diagram's model in JSON format
    function save() {
      document.getElementById("mySavedModel").value = myDiagram.model.toJson();
      myDiagram.isModified = false;
    function load() {
      myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
    function cxcommand(event, val) {
      if (val === undefined) val =;
      var diagram = myDiagram;
      switch (val) {
        case "delete": diagram.commandHandler.deleteSelection(); break;
        case "add_process": addNodeAndLink(e, obj, "Lane4"); break;
        case "add_input": addNodeAndLink(e, obj, "Lane3"); break;
        case "add_output": addNodeAndLink(e, obj, "Lane5"); break;
        case "add_system": addNodeAndLink(e, obj, "Lane2"); break;
        case "add_participants": addNodeAndLink(e, obj, "Lane1"); break;
    // A custom command, for changing the color of the selected node(s).
    function addNodeAndLink(e, obj, string){
      var adorn = obj.part;
        if (adorn === null) return;
        e.handled = true;
        var diagram = adorn.diagram;
        diagram.startTransaction("Add State");
        // get the node data for which the user clicked the button
        var fromNode = adorn.adornedPart;
        var fromData =;
        // create a new "State" data object, positioned off to the right of the adorned Node
        var toData = { text: "new" };
        var p = fromNode.location;
        toData.loc = p.x + 200 + " " + p.y;  // the "loc" property is a string, not a Point object ==> I don't know how to let the node put in the lane which it should be.
        // add the new node data to the model
        var model = diagram.model;
        // create a link data from the old node data to the new node data
        var linkdata = {};
        linkdata[model.linkFromKeyProperty] = model.getKeyForNodeData(fromData); //==>Participants(Lane 1) and System(Lane 2) can't show the line but I want to know it belongs to which process.
        linkdata[model.linkToKeyProperty] = model.getKeyForNodeData(toData);
        // and add the link data to the model
        // select the new Node
        var newnode = diagram.findNodeForData(toData);;
        diagram.commitTransaction("Add State");
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px;"></div>
  <div id="contextMenu">

          <li id="add_process" onclick="cxcommand(event)"><a href="#" target="_self">Add Process</a></li>
          <li id="add_input" onclick="cxcommand(event)"><a href="#" target="_self">Add Input</a></li>
          <li id="add_output" onclick="cxcommand(event)"><a href="#" target="_self">Add Output</a></li>
          <li id="add_system" onclick="cxcommand(event)"><a href="#" target="_self">Add System</a></li>
          <li id="add_participants" onclick="cxcommand(event)"><a href="#" target="_self">Add Participant</a></li>
          <li id="delete" onclick="cxcommand(event)"><a href="#" target="_self">Delete</a></li>

  <button onclick="relayoutLanes()">Layout</button>
  <button id="SaveButton" onclick="save()">Save</button>
  <button onclick="load()">Load</button>
  Diagram Model saved in JSON format:
  <br />
  <textarea id="mySavedModel" style="width:100%;height:300px"></textarea>

I solved the 1. problem as stated above,
I found out that I should use obj.part.containingGroup.key == “Lane4” ? “block” : “none”