Making group top left stay fixed when adding node

I am using the regrouping example to explore this. I would like to be able to place items where I like so I have changed the diagram to use Layout instead of GridLayout. When I drop a node into a group the group repositions to wrap the node where I drop it. I would like to change that behaviour so the group expands to right and downwards to accommodate the node. This would mean the top left of the group stays where it was and the node would be repositioned within the placeholder as happens when a second node is dropped into the group. Do you have any suggestions on how to achieve this?


<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoJS expt</title>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/1.8.9/go-debug.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          allowDrop: true, // from Palette
          // what to do when a drag-drop occurs in the Diagram's background
          mouseDrop: function(e) { finishDrop(e, null); },
          layout:  // Diagram has simple horizontal layout
            $(go.Layout),
          initialContentAlignment: go.Spot.Center,
          "commandHandler.archetypeGroupData": { isGroup: true, category: "OfNodes" },
          "undoManager.isEnabled": true
        });
    // There are two templates for Groups, "OfGroups" and "OfNodes".
    // this function is used to highlight a Group that the selection may be dropped into
    function highlightGroup(e, grp, show) {
      if (!grp) return;
      e.handled = true;
      if (show) {
        // cannot depend on the grp.diagram.selection in the case of external drag-and-drops;
        // instead depend on the DraggingTool.draggedParts or .copiedParts
        var tool = grp.diagram.toolManager.draggingTool;
        var map = tool.draggedParts || tool.copiedParts;  // this is a Map
        // now we can check to see if the Group will accept membership of the dragged Parts
        if (grp.canAddMembers(map.toKeySet())) {
          grp.isHighlighted = true;
          return;
        }
      }
      grp.isHighlighted = false;
    }
    // Upon a drop onto a Group, we try to add the selection as members of the Group.
    // Upon a drop onto the background, or onto a top-level Node, make selection top-level.
    // If this is OK, we're done; otherwise we cancel the operation to rollback everything.
    function finishDrop(e, grp) {
      var ok = (grp !== null
                ? grp.addMembers(grp.diagram.selection, true)
                : e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true));
      if (!ok) e.diagram.currentTool.doCancel();
    }
    myDiagram.groupTemplateMap.add("OfGroups",
      $(go.Group, "Auto",
        {
          background: "transparent",
          // highlight when dragging into the Group
          mouseDragEnter: function(e, grp, prev) { highlightGroup(e, grp, true); },
          mouseDragLeave: function(e, grp, next) { highlightGroup(e, grp, false); },
          computesBoundsAfterDrag: true,
          // when the selection is dropped into a Group, add the selected Parts into that Group;
          // if it fails, cancel the tool, rolling back any changes
          mouseDrop: finishDrop,
          handlesDragDropForMembers: true,  // don't need to define handlers on member Nodes and Links
          // Groups containing Groups lay out their members horizontally
          layout:
            $(go.GridLayout,
              { wrappingWidth: Infinity, alignment: go.GridLayout.Position,
                  cellSize: new go.Size(1, 1), spacing: new go.Size(4, 4) })
        },
        new go.Binding("background", "isHighlighted", function(h) { return h ? "rgba(255,0,0,0.2)" : "transparent"; }).ofObject(),
        $(go.Shape, "Rectangle",
          { fill: null, stroke: "#FFDD33", strokeWidth: 2 }),
        $(go.Panel, "Vertical",  // title above Placeholder
          $(go.Panel, "Horizontal",  // button next to TextBlock
            { stretch: go.GraphObject.Horizontal, background: "#FFDD33" },
            $("SubGraphExpanderButton",
              { alignment: go.Spot.Right, margin: 5 }),
            $(go.TextBlock,
              {
                alignment: go.Spot.Left,
                editable: true,
                margin: 5,
                font: "bold 18px sans-serif",
                opacity: 0.75,
                stroke: "#404040"
              },
              new go.Binding("text", "text").makeTwoWay())
          ),  // end Horizontal Panel
          $(go.Placeholder,
            { padding: 5, alignment: go.Spot.TopLeft })
        )  // end Vertical Panel
      ));  // end Group and call to add to template Map

    myDiagram.groupTemplateMap.add("OfNodes",
      $(go.Group, "Auto",
        {
          background: "transparent",
          ungroupable: true,
          // highlight when dragging into the Group
          mouseDragEnter: function(e, grp, prev) { highlightGroup(e, grp, true); },
          mouseDragLeave: function(e, grp, next) { highlightGroup(e, grp, false); },
          computesBoundsAfterDrag: true,
          // when the selection is dropped into a Group, add the selected Parts into that Group;
          // if it fails, cancel the tool, rolling back any changes
          mouseDrop: finishDrop,
          handlesDragDropForMembers: true,  // don't need to define handlers on member Nodes and Links
          // Groups containing Nodes lay out their members vertically
          layout:
            $(go.GridLayout,
              { wrappingColumn: 1, alignment: go.GridLayout.Position,
                  cellSize: new go.Size(1, 1), spacing: new go.Size(4, 4) })
        },
        new go.Binding("background", "isHighlighted", function(h) { return h ? "rgba(255,0,0,0.2)" : "transparent"; }).ofObject(),
        $(go.Shape, "Rectangle",
          { fill: null, stroke: "#33D3E5", strokeWidth: 2 }),
        $(go.Panel, "Vertical",  // title above Placeholder
          $(go.Panel, "Horizontal",  // button next to TextBlock
            { stretch: go.GraphObject.Horizontal, background: "#33D3E5" },
            $("SubGraphExpanderButton",
              { alignment: go.Spot.Right, margin: 5 }),
            $(go.TextBlock,
              {
                alignment: go.Spot.Left,
                editable: true,
                margin: 5,
                font: "bold 16px sans-serif",
                opacity: 0.75,
                stroke: "#404040"
              },
              new go.Binding("text", "text").makeTwoWay())
          ),  // end Horizontal Panel
          $(go.Placeholder,
            { padding: 5, alignment: go.Spot.TopLeft })
        )  // end Vertical Panel
      ));  // end Group and call to add to template Map
    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { // dropping on a Node is the same as dropping on its containing Group, even if it's top-level
          mouseDrop: function(e, nod) { finishDrop(e, nod.containingGroup); }
        },
        $(go.Shape, "Rectangle",
          { fill: "#ACE600", stroke: null },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          {
            margin: 5,
            editable: true,
            font: "bold 13px sans-serif",
            opacity: 0.75,
            stroke: "#404040"
          },
          new go.Binding("text", "text").makeTwoWay())
      );
    // initialize the Palette and its contents
    myPalette =
      $(go.Palette, "myPaletteDiv",
        {
          nodeTemplateMap: myDiagram.nodeTemplateMap,
          groupTemplateMap: myDiagram.groupTemplateMap,
          layout: $(go.GridLayout, { wrappingColumn: 1, alignment: go.GridLayout.Position })
        });
    myPalette.model = new go.GraphLinksModel([
      { text: "lightgreen", color: "#ACE600" },
      { text: "yellow", color: "#FFDD33" },
      { text: "lightblue", color: "#33D3E5" }
    ]);
    load();
  }
  function load() {
    myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
  }
</script>
</head>
<body onload="init()">
<h1>Based on Regrouping Demo but using Layout instead of GridLayout</h1>
<div id="sample">
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div id="myPaletteDiv" style="width: 100px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 500px; border: solid 1px black"></div>
  </div>
  <p>
    Dragging A into the group makes the group reposition higher up to wrap where the node is dropped. 
  </p>
  <textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "text":"Main 1", "isGroup":true, "category":"OfGroups"},
{"text":"A", "key":-7}
 ],
  "linkDataArray": [  ]}
  </textarea>
</div>
</body>
</html>

I notice you left the Group.layout for both Group templates – is that what you want?

I think you want to set this property on Groups:

    computesBoundsIncludingLocation: true,

Note that when you add a member node the group might still move because its containing group’s layout moves that group. If you want to disable that behavior you can either remove the Group.layout or set its Layout.isOngoing to false or set the node’s Part.layoutConditions appropriately: GoJS Layouts -- Northwoods Software.

Thanks Walter. I’ve tried that and adding “computesBoundsIncludingLocation: true,” seemed to get closer to what I’m after.

However, the group does still wrap around where I drop the item and grows upwards. I’m wondering if there’s a way to make the group expand down and right to accommodate the item. In other words fixing the top left position of the group and making it grow down and right to fit in the item.

I’ve streamlined the sample code I’m using to illustrate this. I would like to keep the layout on the group to automatically layout new items. Any suggestions on what I can try next?

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoJS expt</title>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/1.8.9/go-debug.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          allowDrop: true,
          mouseDrop: function(e) { finishDrop(e, null); },
          layout:
            $(go.Layout),
          initialContentAlignment: go.Spot.Center
        });

    function highlightGroup(e, grp, show) {
      if (!grp) return;
      e.handled = true;
      if (show) {
        var tool = grp.diagram.toolManager.draggingTool;
        var map = tool.draggedParts || tool.copiedParts;
        if (grp.canAddMembers(map.toKeySet())) {
          grp.isHighlighted = true;
          return;
        }
      }
      grp.isHighlighted = false;
    }

    function finishDrop(e, grp) {
      var ok = (grp !== null
                ? grp.addMembers(grp.diagram.selection, true)
                : e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true));
      if (!ok) e.diagram.currentTool.doCancel();
    }

    myDiagram.groupTemplateMap.add("OfGroups",
      $(go.Group, "Auto",
        {
          background: "transparent",
          mouseDragEnter: function(e, grp, prev) { highlightGroup(e, grp, true); },
          mouseDragLeave: function(e, grp, next) { highlightGroup(e, grp, false); },
          computesBoundsAfterDrag: true,
          computesBoundsIncludingLocation: true,
          mouseDrop: finishDrop,
          handlesDragDropForMembers: true,
          layout:
            $(go.GridLayout,
              { wrappingWidth: Infinity, alignment: go.GridLayout.Position,
                  cellSize: new go.Size(1, 1), spacing: new go.Size(4, 4) })
        },
        new go.Binding("background", "isHighlighted", function(h) { return h ? "rgba(255,0,0,0.2)" : "transparent"; }).ofObject(),
        $(go.Shape, "Rectangle",
          { fill: null, stroke: "#FFDD33", strokeWidth: 2 }),
        $(go.Panel, "Vertical",
          $(go.Panel, "Horizontal",
            { stretch: go.GraphObject.Horizontal, background: "#FFDD33" },
            $(go.TextBlock,
              {
                alignment: go.Spot.Left,
                editable: true,
                margin: 5
              },
              new go.Binding("text", "text").makeTwoWay())
          ),
          $(go.Placeholder,
            { padding: 5, alignment: go.Spot.TopLeft })
        )
      ));

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        {
          mouseDrop: function(e, nod) { finishDrop(e, nod.containingGroup); }
        },
        $(go.Shape, "Rectangle",
          { fill: "#ACE600", stroke: null },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          {
            margin: 5,
            editable: true
          },
          new go.Binding("text", "text").makeTwoWay())
      );

    myPalette =
      $(go.Palette, "myPaletteDiv",
        {
          nodeTemplateMap: myDiagram.nodeTemplateMap,
          groupTemplateMap: myDiagram.groupTemplateMap,
          layout: $(go.GridLayout, { wrappingColumn: 1, alignment: go.GridLayout.Position })
        });
    myPalette.model = new go.GraphLinksModel([
      { text: "lightgreen", color: "#ACE600" },
      { text: "yellow", color: "#FFDD33" },
      { text: "lightblue", color: "#33D3E5" }
    ]);
    load();
  }
  const treeData = {class:'go.GraphLinksModel',nodeDataArray:[{key:1, text:'Main 1', isGroup:true, category:'OfGroups'}, {text:'A', key:-1}],linkDataArray: [  ] }
  function load() {
    myDiagram.model = go.Model.fromJson(treeData);
  }
</script>
</head>
<body onload="init()">
<div id="sample">
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div id="myPaletteDiv" style="width: 100px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 500px; border: solid 1px black"></div>
  </div>
  <p>
    Dragging A into the group makes the group reposition higher up to wrap where the node is dropped. 
  </p>
</div>
</body>
</html>

Does this do what you want?

    function finishDrop(e, grp) {
      if (grp !== null) {
        var gloc = grp.location.copy();
        var ok = grp.addMembers(e.diagram.selection, true);
        if (!ok) {
          e.diagram.currentTool.doCancel();
        } else {
          var newbnds = e.diagram.computePartsBounds(e.diagram.selection).addMargin(grp.placeholder.padding);
          e.diagram.moveParts(e.diagram.selection, gloc.subtract(newbnds.position), false);
        }
      } else {
        var ok = e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true);
        if (!ok) e.diagram.currentTool.doCancel();
      }
    }

That does indeed. Many thanks.

When I drop a group into another group then the receiving group moves up. I’m struggling to understand what the finishDrop changes calculation does and what to do to handle a group to keep the original fixed at top left.

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoJS expt 4 - squarify and drop onto group</title>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/1.8.9/go-debug.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          allowDrop: true,
          mouseDrop: function(e) { finishDrop(e, null); },
          layout:
            $(go.Layout),
          initialContentAlignment: go.Spot.Center
        });

    function SquareLayout () {
      go.GridLayout.call(this)
      this.isViewportSized = false
    }

    go.Diagram.inherit(SquareLayout, go.GridLayout)

    SquareLayout.prototype.doLayout = function (coll) {
      var ecoll = this.collectParts(coll)
      // estimate minimum space needed
      var tot = 0
      var spac = this.spacing
      ecoll.each(function (p) {
        if (p instanceof go.Link) return
        tot += (p.actualBounds.width + spac.width) * (p.actualBounds.height + spac.height)
      })
      if (tot === 0) return
      this.wrappingWidth = Math.sqrt(tot)

      var diff = 30.0
      var increasing = false
      let count = 0
      while (diff >= 1.0) {
        if (count++ >= 1000) break
        go.GridLayout.prototype.doLayout.call(this, ecoll)
        var bnds = this.diagram.computePartsBounds(ecoll)
        if (bnds.height < 1) break
        var ratio = bnds.width / bnds.height
        if (ratio < 0.99) {
          if (!increasing) { increasing = true; diff *= 0.8 }
          this.wrappingWidth += diff
        } else if (ratio > 1.01) {
          if (increasing) { increasing = false; diff *= 0.8 }
          if (this.wrappingWidth - diff < 1) break
          this.wrappingWidth -= diff
        } else {
          break
        }
      }

      this.isValidLayout = true // in case it's been invalidated by setting wrappingWidth
    }

    function highlightGroup(e, grp, show) {
      if (!grp) return;
      e.handled = true;
      if (show) {
        var tool = grp.diagram.toolManager.draggingTool;
        var map = tool.draggedParts || tool.copiedParts;
        if (grp.canAddMembers(map.toKeySet())) {
          grp.isHighlighted = true;
          return;
        }
      }
      grp.isHighlighted = false;
    }

    function finishDrop(e, grp) {
      if (grp !== null) {
        var gloc = grp.location.copy();
        var ok = grp.addMembers(e.diagram.selection, true);
        if (!ok) {
          e.diagram.currentTool.doCancel();
        } else {
          var newbnds = e.diagram.computePartsBounds(e.diagram.selection).addMargin(grp.placeholder.padding);
          e.diagram.moveParts(e.diagram.selection, gloc.subtract(newbnds.position), false);
        }
      } else {
        const moveContainingGroup = false; // e.diagram.selection.containingGroup
        var ok = e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true);
        if (moveContainingGroup) {
          var newbnds = e.diagram.computePartsBounds(e.diagram.selection.containingGroup).addMargin(grp.placeholder.padding);
          e.diagram.moveParts(e.diagram.selection.containingGroup, gloc.subtract(newbnds.position), false);
        }
        if (!ok) e.diagram.currentTool.doCancel();
      }
    }

    myDiagram.groupTemplateMap.add("OfGroups",
      $(go.Group, "Auto",
        {
          background: "transparent",
          mouseDragEnter: function(e, grp, prev) { highlightGroup(e, grp, true); },
          mouseDragLeave: function(e, grp, next) { highlightGroup(e, grp, false); },
          computesBoundsAfterDrag: true,
          computesBoundsIncludingLocation: true,
          mouseDrop: finishDrop,
          handlesDragDropForMembers: true,
          layout:
            $(SquareLayout,
              {
                cellSize: new go.Size(1, 1),
                spacing: new go.Size(4, 4)
              }
            )
        },
        new go.Binding("background", "isHighlighted", function(h) { return h ? "rgba(255,0,0,0.2)" : "transparent"; }).ofObject(),
        $(go.Shape, "Rectangle",
          { fill: null, stroke: "#FFDD33", strokeWidth: 2 }),
        $(go.Panel, "Vertical",
          $(go.Panel, "Horizontal",
            { stretch: go.GraphObject.Horizontal, background: "#FFDD33" },
            $(go.TextBlock,
              {
                alignment: go.Spot.Left,
                editable: true,
                margin: 5
              },
              new go.Binding("text", "text").makeTwoWay())
          ),
          $(go.Placeholder,
            { padding: 5, alignment: go.Spot.TopLeft })
        )
      ));

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        {
          mouseDrop: function(e, nod) { finishDrop(e, nod.containingGroup); }
        },
        $(go.Shape, "Rectangle",
          { fill: "#ACE600", stroke: null },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          {
            margin: 5,
            editable: true
          },
          new go.Binding("text", "text").makeTwoWay())
      );

    myPalette =
      $(go.Palette, "myPaletteDiv",
        {
          nodeTemplateMap: myDiagram.nodeTemplateMap,
          groupTemplateMap: myDiagram.groupTemplateMap,
          layout: $(go.GridLayout, { wrappingColumn: 1, alignment: go.GridLayout.Position })
        });
    myPalette.model = new go.GraphLinksModel([
      { text: "lightgreen", color: "#ACE600" },
      { text: "yellow", color: "#FFDD33" },
      { text: "lightblue", color: "#33D3E5" }
    ]);
    load();
  }
  const treeData = {class:'go.GraphLinksModel',nodeDataArray:[
    {key:1, text:'Main 1', isGroup:true, category:'OfGroups'},
    {key:2, text:'Main 2', isGroup:true, category:'OfGroups'},
    {text:'A', key:-1},
    {text:'Alpha', key:-2, group:1},
    {text:'Beta', key:-2, group:1},
    {text:'Gamma', key:-2, group:1},
    {text:'Delta', key:-2, group:2}
  ],linkDataArray: [  ] }
  function load() {
    myDiagram.model = go.Model.fromJson(treeData);
  }
</script>
</head>
<body onload="init()">
<div id="sample">
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div id="myPaletteDiv" style="width: 100px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 500px; border: solid 1px black"></div>
  </div>
  <p>
    Dragging group Main 2 into group Main 1 makes the Main 1 move up.
  </p>
</div>
</body>
</html>

Could you please be more specific about what happens and what you want?

When I drop the Main 2 group onto Main 1 then the Main 1 group moves up. I would like it to keep the same top left position as it had before the drop. Dropping a node into Main 1 does not affect the top left position of Main 1 due to the adjustment you suggested is added to finishDrop. I’m trying to establish how to achieve the same thing when a group is dropped instead of a node.

By how much is the group moved? If you step through the code, can you identify exactly where the calculation is wrong and why? If your calculation needs to take additional considerations into account, then you can do so.