Problems using ContextMenu

Hi, thank you for your hard work, please help me with my problems.
I want to make a ContextMenu with tabs on the left, and change the class of the shapes on the right side, like this:
Snip20210826_1
but now it look like this:
Snip20210824_14

I make an icon on the top-right of the node to open a ContextMenu. The question is :
1、How can I change the shapes on the right side when I click the buttons ?
2、On the right side , I want three shapes in a row and wrap to the next row automatically, but now it align in the middle and don’t wrap at all. How can I make it align to the top and wrap automatically?
3、How can I change the selected node‘s shape when I click one shape on the right side?
The code is bellow

`
initNodeContextMenu() {
var $ = go.GraphObject.make; // for conciseness in defining templates
this.contextPanel = $(
go.Panel,
“Horizontal”,

    {
      width: 180,
      height: 170,
      background: "#1D1F25",
      itemArray: shapesArray,

      // defaultAlignment: go.Spot.Top, // thus no need to specify alignment on each element
    },
    new go.Binding("itemArray", "someProperty"),
    {
      itemTemplate: $(
        go.Panel,
        {
          mouseEnter: function (e, shape) {
            shape.background = "black";
            console.log("mouseEnter", e, shape);
          },
          mouseLeave: function (e, shape) {
            console.log("mouseLeave", e, shape);
            shape.background = "transparent";
          },
        },
        // set Panel.background to a color based on the Panel.itemIndex
        //  new go.Binding("background", "itemIndex",
        //                 // using this conversion function
        //                 function(i) { return (i%2 === 0) ? "lightgreen" : "lightyellow"; })
        //           // bound to this Panel itself, not to the Panel.data item
        //           .ofObject(),
        $(go.Shape, "RoundedRectangle", {
          width: 60,
          height: 60,
          fill: "transparent",
        }),
        $(
          go.Picture,
          {
            margin: 12,
            width: 38,
            height: 38,

            click: (e, shape) => {
              console.log("click", e, e.part.data);
              this.myDiagram.selection.each((node) => {
                if (node instanceof go.Node) {
                  // ignore any selected Links and simple Parts
                  // Examine and modify the data, not the Node directly.
                  var data = node.data;
                  console.log(333, data);
                  // Call setDataProperty to support undo/redo as well as
                  // automatically evaluating any relevant bindings.
                  this.myDiagram.model.setDataProperty(
                    data,
                    "source",
                    shape.part.data.source
                  );
                }
              });
            },
          },

          new go.Binding("source")
        )
      ),
    }
  );
  this.nodeContextMenu = $(
    go.Adornment,
    "Spot",
    $(go.Placeholder),
    $(
      go.Panel,
      "Horizontal",
      { alignment: go.Spot.Right, alignmentFocus: go.Spot.Left },

      $(
        go.Panel,
        "Vertical",

        $("Button", $(go.TextBlock, this.contextMenuStyle(), "图形"), {
          click: function (e, obj) {
            var node = obj.part.adornedPart;
            alert("Command 1 on " + node.data.text);
            node.removeAdornment("ContextMenuOver");
          },
          mouseEnter: function (e) {
            console.log(e);
          },
        }),
        $("Button", $(go.TextBlock, this.contextMenuStyle(), "办公"), {
          click: function (e, obj) {
            var node = obj.part.adornedPart;
            alert("Command 2 on " + node.data.text);
            node.removeAdornment("ContextMenuOver");
          },
        }),
        $("Button", $(go.TextBlock, this.contextMenuStyle(), "健康"), {
          click: function (e, obj) {
            var node = obj.part.adornedPart;
            alert("Command 1 on " + node.data.text);
            node.removeAdornment("ContextMenuOver");
          },
        }),
        $("Button", $(go.TextBlock, this.contextMenuStyle(), "金融"), {
          click: function (e, obj) {
            var node = obj.part.adornedPart;
            alert("Command 2 on " + node.data.text);
            node.removeAdornment("ContextMenuOver");
          },
        }),
        $("Button", $(go.TextBlock, this.contextMenuStyle(), "环保"), {
          click: function (e, obj) {
            var node = obj.part.adornedPart;
            alert("Command 1 on " + node.data.text);
            node.removeAdornment("ContextMenuOver");
          },
        }),
        $("Button", $(go.TextBlock, this.contextMenuStyle(), "3D"), {
          click: function (e, obj) {
            var node = obj.part.adornedPart;
            alert("Command 2 on " + node.data.text);
            node.removeAdornment("ContextMenuOver");
          },
        })
      ),
      this.contextPanel
    )
  );
},

`

initNodeTemplate() {
var $ = go.GraphObject.make;

  this.myDiagram.nodeTemplate = $(
    go.Node,
    "Table",
    { contextMenu: this.nodeContextMenu },
    this.nodeStyle(),
    
    $(
      go.Panel,
      "Vertical",
      $(
        go.Picture,
        { margin: 5, width: 55, height: 55 },
        new go.Binding("source")
      ),
      
      $(
        go.TextBlock,
        this.textStyle(),
        {
          margin: 8,
          maxSize: new go.Size(160, NaN),
          wrap: go.TextBlock.WrapFit,
          editable: true,
        },
        new go.Binding("text").makeTwoWay()
      )
    ),

    this.makePort("T", go.Spot.Top, go.Spot.TopSide, false, true),
    this.makePort("L", go.Spot.Left, go.Spot.LeftSide, true, true),
    this.makePort("R", go.Spot.Right, go.Spot.RightSide, true, true),
    this.makePort("B", go.Spot.Bottom, go.Spot.BottomSide, true, false)
  );
},

Here’s a complete sample that I think demonstrates what you are asking for, ignoring styling issues. You might be using Pictures rather than Shapes for the choices in each panel. Note that this code does not use "Button"s – I think having to click is not desired and I believe you don’t need the visual details of a traditional button anyway.

<!DOCTYPE html>
<html>
<head>
  <title>Flow PanelLayout and tabbed context menu</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
// load the PanelLayoutFlow extension

function init() {
  var $ = go.GraphObject.make;

  myDiagram =
    $(go.Diagram, "myDiagramDiv",
      { "undoManager.isEnabled": true });

  myDiagram.nodeTemplate =
    $(go.Node, "Vertical",
      { selectionObjectName: "SHAPE" },
      $(go.Shape,
        { name: "SHAPE", width: 40, height: 40, fill: "white", portId: "" },
        new go.Binding("fill", "color"),
        new go.Binding("figure").makeTwoWay()),
      $(go.TextBlock,
        new go.Binding("text"))
    );

  function makeTabLabel(name, selected) {
    return $(go.TextBlock, name,
        {
          margin: new go.Margin(2, 2, 0, 2),
          mouseEnter: function(e, tb) {
            var ad = tb.part;
            if (ad.selectedTextBlock === tb) return;
            if (ad.selectedTextBlock) {  // this keeps a reference to the selected TextBlock
              ad.selectedTextBlock.background = null;
              var oldpan = ad.findObject(ad.selectedTextBlock.text);
              if (oldpan) oldpan.visible = false;
            }
            ad.selectedTextBlock = tb;
            tb.background = "white";
            var newpan = ad.findObject(tb.text);
            if (newpan) newpan.visible = true;
          }
        });
  }

  function makeShape(fig) {
    return $(go.Shape, fig,
      {
        width: 30, height: 30,
        fill: "white",
        background: "transparent",  // to catch a click anywhere in bounds
        click: function(e, shape) {
          var ad = shape.part;
          e.diagram.commit(function(diag) {
            diag.model.set(ad.data, "figure", fig);
            ad.adornedPart.invalidateConnectedLinks();
          }, "modified figure");
        }
      });
  }

  function makeFlowPanel(name, figures, selected) {
    var pan = new go.Panel($(PanelLayoutFlow, { spacing: new go.Size(5, 5) }));
    pan.name = name;
    pan.column = 2;
    pan.maxSize = new go.Size(130, NaN);
    pan.visible = !!selected;
    figures.forEach(function(fig) { pan.add(makeShape(fig)); });
    return pan;
  }

  myDiagram.nodeTemplate.contextMenu =
    $(go.Adornment, "Table",
      { background: "gray", defaultAlignment: go.Spot.Top, padding: 10 },
      $(go.Panel, "Vertical",
        { column: 0, stretch: go.GraphObject.Vertical, defaultStretch: go.GraphObject.Horizontal },
        makeTabLabel("A Figures", true),
        makeTabLabel("B Figures"),
        makeTabLabel("C Figures")
      ),
      $(go.RowColumnDefinition, { column: 1, width: 10 }),
      makeFlowPanel("A Figures", ["Square", "Circle", "RoundedRectangle", "Diamond"], true),
      makeFlowPanel("B Figures", ["TriangleUp", "TriangleRight", "TriangleDown", "TriangleLeft"]),
      makeFlowPanel("C Figures", ["LineH", "LineV", "BarH", "BarV", "MinusLine", "PlusLine", "XLine"]),
    );

  myDiagram.model = new go.GraphLinksModel(
    [
      { key: 1, text: "Alpha", color: "lightblue" },
      { key: 2, text: "Beta", color: "orange", figure: "Diamond" },
      { key: 3, text: "Gamma", color: "lightgreen", figure: "Circle" },
      { key: 4, text: "Delta", color: "pink", figure: "Triangle" }
    ],
    [
      { from: 1, to: 2 },
      { from: 1, to: 3 },
      { from: 3, to: 4 },
      { from: 4, to: 1 }
    ]);
}
  </script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <p>
    The context menu shown for each node has three "tabs", each showing some number of shape figures,
    one of which the user can click to replace the node's shape's figure.
  </p>
</body>
</html>

Walter, thanks very much for your quick reply and it’s really helpful. Another thing is as you said, I use Pictures rather than Shapes. That’s because we want to use custom icons and I don’t know how to make svg file into a Shape. The icons we want are like this:

That’s my second problem, I know there’s a demo in https://gojs.net/latest/samples/icons.html, but in icons.js, all the icons are all svg strings which are very simple, not .svg files, like this:

<svg id="Capa_1" enable-background="new 0 0 510 510" height="512" viewBox="0 0 510 510" width="512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="lg1"><stop offset="0" stop-color="#f7e07d"/><stop offset="1" stop-color="#e69642"/></linearGradient><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="20" x2="436.173" xlink:href="#lg1" y1="20" y2="436.173"/><linearGradient id="lg2"><stop offset="0" stop-color="#d52c1c" stop-opacity="0"/><stop offset=".2813" stop-color="#cc2c20" stop-opacity=".281"/><stop offset=".7301" stop-color="#b52b2a" stop-opacity=".73"/><stop offset="1" stop-color="#a42b31"/></linearGradient><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="255" x2="255" xlink:href="#lg2" y1="288.383" y2="619.034"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="101.456" x2="291.624" xlink:href="#lg1" y1="295.462" y2="485.631"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="255" x2="255" xlink:href="#lg2" y1="400" y2="544.014"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="427.024" x2="339.024" xlink:href="#lg2" y1="243.399" y2="178.399"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="153.549" x2="364.948" xlink:href="#lg1" y1="-7.269" y2="204.13"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="286.542" x2="343.257" xlink:href="#lg2" y1="173.127" y2="405.302"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="382" x2="192.243" xlink:href="#lg2" y1="428" y2="238.243"/><linearGradient id="lg3"><stop offset="0" stop-color="#dc4955"/><stop offset="1" stop-color="#c4237c"/></linearGradient><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="213.139" x2="288.647" xlink:href="#lg3" y1="259.139" y2="334.647"/><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="301.73" x2="213.914" xlink:href="#lg3" y1="347.73" y2="259.914"/><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="255" x2="255" y1="154.667" y2="344.198"><stop offset="0" stop-color="#8a1958" stop-opacity="0"/><stop offset="1" stop-color="#8a1958"/></linearGradient><g><path d="m403.41 0h-296.82c-58.87 0-106.59 47.72-106.59 106.59v296.82c0 58.87 47.72 106.59 106.59 106.59h296.82c58.87 0 106.59-47.72 106.59-106.59v-296.82c0-58.87-47.72-106.59-106.59-106.59z" fill="url(#SVGID_1_)"/><path d="m0 276.384v127.026c0 58.87 47.72 106.59 106.59 106.59h296.82c58.87 0 106.59-47.72 106.59-106.59v-127.026z" fill="url(#SVGID_2_)"/><path d="m106.59 510h296.82c34.117 0 64.487-16.029 83.997-40.965l-179.006-219.665c-27.565-33.827-79.236-33.827-106.802 0l-179.006 219.665c19.51 24.936 49.88 40.965 83.997 40.965z" fill="url(#SVGID_3_)"/><path d="m0 344.63v58.78c0 58.87 47.72 106.59 106.59 106.59h296.82c58.87 0 106.59-47.72 106.59-106.59v-58.78z" fill="url(#SVGID_4_)"/><path d="m437.825 5.681-228.962 268.37 229.928 229.928c41.476-14.59 71.209-54.103 71.209-100.569v-296.82c0-46.827-30.194-86.597-72.175-100.909z" fill="url(#SVGID_5_)"/><g><path d="m403.41 0h-296.82c-35.481 0-66.907 17.339-86.281 44.002l181.29 282.998c27.565 33.827 79.236 33.827 106.802 0l181.29-282.998c-19.374-26.663-50.8-44.002-86.281-44.002z" fill="url(#SVGID_6_)"/><path d="m403.41 0h-296.82c-35.481 0-66.907 17.339-86.281 44.002l181.29 282.998c27.565 33.827 79.236 33.827 106.802 0l181.29-282.998c-19.374-26.663-50.8-44.002-86.281-44.002z" fill="url(#SVGID_7_)"/></g><path d="m502.131 443.659-190.972-190.973c-13.55-15.146-33.237-24.686-55.159-24.686-40.869 0-74 33.131-74 74 0 21.922 9.54 41.609 24.686 55.159l152.841 152.841h43.883c44.627 0 82.84-27.427 98.721-66.341z" fill="url(#SVGID_8_)"/><circle cx="255" cy="301" fill="url(#SVGID_9_)" r="74"/><circle cx="255" cy="301" fill="url(#SVGID_10_)" r="58.283"/><path d="m255.696 255.544c-30.135 0-44.696 20.386-44.696 46.722 0 29.756 18.612 44.19 38.619 44.19 7.47 0 23.424-1.899 23.424-8.104 0-1.899-1.519-5.571-4.685-5.571-2.912 0-6.458 3.039-16.334 3.039-22.412 0-28.742-15.7-28.742-33.68 0-19.247 7.218-35.96 32.541-35.96 21.525 0 31.022 12.282 31.022 34.947 0 5.571-.507 17.347-6.838 17.347-1.899 0-2.786-1.266-3.545-5.951v-21.652c0-11.522-10.635-17.22-21.398-17.22-9.116 0-18.866 4.179-18.866 9.37 0 2.406 1.899 7.217 4.431 7.217 2.28 0 5.065-3.545 12.535-3.545 6.458 0 9.37 2.659 9.37 8.104v1.646h-3.419c-14.435 0-25.703 4.052-25.703 16.08 0 10.256 8.356 14.941 15.827 14.941 6.837 0 11.902-3.799 15.954-9.243 1.013 5.571 6.078 9.623 13.675 9.623 13.675 0 20.133-13.421 20.133-29.882-.001-25.831-16.082-42.418-43.305-42.418zm7.597 51.534c0 5.319-5.445 10.383-10.13 10.383-2.659 0-4.305-1.9-4.305-5.192 0-5.191 4.178-7.976 12.156-7.976h2.279z" fill="url(#SVGID_11_)"/></g></svg>

And another svg demo in https://gojs.net/latest/samples/tiger.html, I found that var xmldoc = new DOMParser().parseFromString(tigersvg, "text/xml"); is not working and return me a Parse Error.
So Question1: I wonder if where is a simplest way to use svg files into a Shape?
Question2: How can I type a text into it like your inner Shapes?(Resize the shapes to include all the text automatically.)

Use a Picture, not a Shape: GoJS Pictures -- Northwoods Software

To put one thing inside another thing, use an “Auto” Panel: GoJS Panels -- Northwoods Software
The outer thing in your case would be a Picture.

Thanks, Walter. Now I use Picture and make text into it but I found it is hard to let Picture scale correctly(Keep aspect ratio and include all the text inside). What’s I want in your code :
Snip20210831_3
And mine is like this:
Snip20210831_4
And another problem is when I am editing, there are two layers of the text, maybe I should let the black text has a white background but I don’t know how:
Snip20210831_5
The code is bellow:

this.myDiagram.nodeTemplate = $(
        go.Node,
        "Table",
        this.nodeStyle(),
        $(
          go.Panel,
          "Auto",
          $(
            go.Picture,
             new go.Binding("source").makeTwoWay()
            
          ),
          $(
            go.TextBlock,
            this.textStyle(),
            {
              margin: 20,
              maxSize: new go.Size(160, NaN),
              wrap: go.TextBlock.WrapFit,
              textAlign: "center",
              editable: true,
              font: "14pt",
            },
            new go.Binding("text").makeTwoWay()
          )
        ),

And question3: How can I make two kinds of node, one is for ‘shape pictures’ which has text inside, and another is for ‘other svg pictures’ which has text bellow the picture?

I think all of your questions are answered in GoJS Nodes -- Northwoods Software and GoJS Panels -- Northwoods Software

Sorry Walter,
I fixed all the issues except this one:
Snip20210831_5
And I made a mindmap chart and it is also have this problem.
Snip20210903_15
Can you help with this? Thank you very much!

initNodeTemplate() {
      // a node consists of some text with a line shape underneath
      var $ = go.GraphObject.make;

      this.myDiagram.nodeTemplate = $(
        go.Node,
         "Table", 
         {
          locationSpot: go.Spot.Center,
          locationObjectName: "SHAPE",
        },
      this.nodeStyle(),
        // the main object is a Panel that surrounds a TextBlock with a rectangular Shape
        $(go.Panel, "Auto",
        
          $(go.Shape, "Rectangle",
            {  stroke: "white", strokeWidth: 1 },
            new go.Binding("fill", "key", function (key) {
              console.log(key)
            return key == "main"?"#8D5CFF":"white";
          }),
          new go.Binding("width", "key", function (key) {
              console.log(key)
            return key == "main"?"200":"";
          }),
           ),
          $(go.TextBlock, this.textStyle(),
            {
              margin: 8,
              maxSize: new go.Size(200, NaN),
              wrap: go.TextBlock.WrapFit,
              editable: true
            },
            new go.Binding("text").makeTwoWay(),
            new go.Binding("stroke", "key", function (key) {
            return key == "main"?"white":"#A6ADC0";
          }),
            )
        ),
        // four named ports, one on each side:
        this.makePort("T", go.Spot.Top, go.Spot.TopSide, false, true),
        this.makePort("L", go.Spot.Left, go.Spot.LeftSide, true, true),
        this.makePort("R", go.Spot.Right, go.Spot.RightSide, true, true),
        this.makePort("B", go.Spot.Bottom, go.Spot.BottomSide, true, false)
      );

      // selected nodes show a button for adding children
      this.myDiagram.nodeTemplate.selectionAdornmentTemplate = $(
        go.Adornment,
        "Spot",
        $(
          go.Panel,
          "Auto",
          // this Adornment has a rectangular blue Shape around the selected node
          $(go.Shape, { fill: null, stroke: "#8D5CFF", strokeWidth: 1 }),
          $(go.Placeholder, { margin: new go.Margin(0) })
        ),
        // and this Adornment has a Button to the right of the selected node
        $(
          "Button",
          {
            alignment: go.Spot.Right,
            alignmentFocus: go.Spot.Left,
            click: this.addNodeAndLink, // define click behavior for this Button in the Adornment
          },
          $(
            go.TextBlock,
            "+", // the Button content
            { font: "bold 8pt sans-serif" }
          )
        )
      );
    },

You just need to style the <textarea> that the TextEditingTool’s uses as its text editor. Something like this as part of your diagram initialization:

myDiagram.toolManager.textEditingTool.defaultTextEditor.mainElement.style['background-color'] = "lime";

Hi, Walter, how can I make defaultTextEditor the same size when I type words in it? Now the problem is when I type words and the size is smaller than before, the history text in the background will be shown, and your demo htmls also have this problem, like this:
Snip20210926_1

How can I make the defaultTextEditor the same size as background when I type words if the words are smaller length than before?