Here you go:
<!DOCTYPE html>
<html>
<head>
<title>Limited Group Resizing</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
// This is a custom ResizingTool that works on Groups and limits resizing so
// that the group does not become smaller than needed to cover its members and
// so that the group does not overlap any non-member nodes.
// This assumes that Groups do not have a Placeholder and are not rotated.
function LimitedGroupResizingTool() {
go.ResizingTool.call(this);
this._oppositePoint = new go.Point(); // in document coordinates
this._lastOK = new go.Rect(); // in document coordinates
}
go.Diagram.inherit(LimitedGroupResizingTool, go.ResizingTool);
LimitedGroupResizingTool.prototype.computeMinSize = function() {
var group = this.adornedObject.part;
if (group instanceof go.Group) {
this._oppositePoint = this.adornedObject.getDocumentPoint(this.handle.alignment.opposite());
// remember a "safe" bounds
this._lastOK = this.adornedObject.getDocumentBounds();
// this ignores ResizingTool.minSize and the adornedObject.minSize;
// you can change this code to respect those properties if you want to
var membnds = group.diagram.computePartsBounds(group.memberParts);
membnds.addMargin(GroupMargin);
membnds.unionPoint(this._oppositePoint);
return membnds.size;
} else {
return go.ResizingTool.prototype.computeMinSize.call(this);
}
}
LimitedGroupResizingTool.prototype._checkIntersections = function(r, group) {
// this is slightly more efficient than findPartsIn
var results = this.diagram.findObjectsIn(r,
// ignore all Links and temporary Parts such as Adornments
function(obj) {
var p = obj.part;
if (p.layer.isTemporary) return null;
if (p instanceof go.Link) return null;
return p;
},
// don't include the Group itself nor any of its member Nodes
function(node) { return node !== group && !node.isMemberOf(group) },
true);
return results.count > 0;
}
LimitedGroupResizingTool.prototype.computeResize = function(newPoint, spot, min, max, cell, reshape) {
var newRect = go.ResizingTool.prototype.computeResize.call(this, newPoint, spot, min, max, cell, reshape);
var group = this.adornedObject.part;
if (group instanceof go.Group) {
// assume not rotated; use document coordinates
var tl = this.adornedObject.getDocumentPoint(newRect.position);
var br = this.adornedObject.getDocumentPoint(new go.Point(newRect.right, newRect.bottom));
if (this._checkIntersections(new go.Rect(tl, br), group)) {
return this._lastOK; // too big! use last safe bounds
} else {
this._lastOK = newRect.copy(); // remember for future use
}
}
return newRect;
}
LimitedGroupResizingTool.prototype.resize = function(newr) {
var group = this.adornedObject.part;
if (group instanceof go.Group) {
var obj = this.adornedObject;
obj.desiredSize = newr.size;
group.ensureBounds();
var pt = this.adornedObject.getDocumentPoint(this.handle.alignment.opposite());
group.location = group.location.copy().subtract(pt).add(this._oppositePoint);
} else {
go.ResizingTool.prototype.resize.call(this, newr);
}
}
// end of LimitedGroupResizingTool
var GroupMargin = new go.Margin(5);
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{ // replace the standard ResizingTool
resizingTool: new LimitedGroupResizingTool(),
"undoManager.isEnabled": true
});
// 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;
// no placeholder -- just assume some Margin
var m = GroupMargin;
// 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 + m.top, 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,
{ dragComputation: stayInGroup },
new go.Binding("location").makeTwoWay(),
$(go.TextBlock, new go.Binding("text", "key"))
);
myDiagram.groupTemplate =
$(go.Group, "Vertical",
{
resizable: true, resizeObjectName: "SHAPE",
selectionObjectName: "SHAPE",
locationObjectName: "SHAPE"
},
new go.Binding("location").makeTwoWay(),
$(go.TextBlock, { font: "bold 11pt sans-serif" },
new go.Binding("text", "key")),
$(go.Shape, { name: "SHAPE", fill: "whitesmoke" },
new go.Binding("desiredSize").makeTwoWay())
);
myDiagram.model = new go.GraphLinksModel([
{ key: "Alpha", group: "Epsilon", location: new go.Point(20, 70) },
{ key: "Beta", group: "Epsilon", location: new go.Point(40, 120) },
{ key: "Gamma", location: new go.Point(50, 200) },
{ key: "Delta", location: new go.Point(150, 100) },
{ key: "Epsilon", isGroup: true, location: new go.Point(10, 60) }
],[
{ from: "Alpha", to: "Beta" },
{ from: "Beta", to: "Gamma" },
{ from: "Alpha", to: "Delta" }
]);
}
</script>
</head>
<body onload="init()">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
This sample uses a Group template that does not use a Placeholder, but allows the user
to resize the group. Resizing of a group will not allow the group to resize smaller than its members,
nor to grow larger to cover any non-member nodes.
Moving member nodes is limited to stay within the group.
</body>
</html>