This adds a function (updateTotalGroupDepth) for updating the “max” property of the slider, based on calling another new function (groupDepth) for determining the maximum nested depth of groups in the diagram.
That updateTotalGroupDepth function is called both when the diagram is initialiized (“InitialLayoutCompleted” DiagramEvent) and when the user drag-and-drops a node (in finishDrop).
That function is not called when the app changes the group structure programmatically and not in finishDrop. I don’t know if your app will do that, but if it does, you can either call it yourself after making the model changes, or else you can implement a Model Changed listener that detects group membership changes and then schedules a call to updateTotalGroupDepth. If you do that, you won’t need to add that call in finishDrop.
<!DOCTYPE html>
<html><body>
<script src="https://unpkg.com/gojs"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lora&display=swap" rel="stylesheet">
<script id="code">
function init() {
myDiagram = new go.Diagram("myDiagramDiv",
{
"InitialLayoutCompleted": e => updateTotalGroupDepth(),
// when a drag-drop occurs in the Diagram's background, make it a top-level node
mouseDrop: e => finishDrop(e, null),
layout: // Diagram has simple horizontal layout
new go.GridLayout(
{ wrappingWidth: Infinity, alignment: go.GridAlignment.Position, cellSize: new go.Size(1, 1) }),
"commandHandler.archetypeGroupData": { isGroup: true, text: "Group", horiz: false },
"undoManager.isEnabled": true
});
const colors = {
white: '#fffcf6',
blue: '#dfebe5',
darkblue: '#cae0d8',
yellow: '#fcdba2',
gray: '#59524c',
black: '#000000'
}
// The one template for Groups can be configured to either layout out its members
// horizontally or vertically, each with a different default color.
function makeLayout(horiz) { // a Binding conversion function
return new go.GridLayout(
{
wrappingColumn: horiz ? Infinity : 1,
alignment: go.GridAlignment.Position,
cellSize: new go.Size(1, 1),
spacing: horiz ? new go.Size(8, 8) : new go.Size(5, 5)
});
}
function defaultColor(horiz) { // a Binding conversion function
return horiz ? colors.white : colors.darkblue;
}
// 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();
updateTotalGroupDepth();
}
myDiagram.groupTemplate =
new go.Group("Auto",
{
ungroupable: true,
// highlight when dragging into the Group
mouseDragEnter: (e, grp, prev) => highlightGroup(e, grp, true),
mouseDragLeave: (e, grp, next) => highlightGroup(e, grp, false),
computesBoundsAfterDrag: true,
computesBoundsIncludingLocation: 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: makeLayout(false),
background: defaultColor(false) // default value if not specified in data
})
.bind("background", "horiz", defaultColor)
.bind("layout", "horiz", makeLayout)
.add(
new go.Shape("Rectangle",
{ stroke: colors.gray, strokeWidth: 1, hasShadow: true })
.bindObject("fill", "isHighlighted", h => h ? 'rgba(0,255,0,.3)' : 'transparent'),
new go.Panel("Vertical") // title above Placeholder
.add(
new go.Panel("Horizontal", // button next to TextBlock
{ stretch: go.Stretch.Horizontal })
.add(
go.GraphObject.build("SubGraphExpanderButton", { alignment: go.Spot.Right, margin: 5 }),
new go.TextBlock(
{
alignment: go.Spot.Left,
editable: true,
margin: new go.Margin(6, 10, 6, 1),
font: "bold 16px Lora, serif",
opacity: 0.95, // allow some color to show through
stroke: colors.black
})
.bind("font", "horiz", (horiz) => horiz ? "bold 20px Lora, serif" : "bold 16px Lora, serif")
.bindTwoWay("text")
), // end Horizontal Panel
new go.Placeholder({ padding: 8, margin: 4, alignment: go.Spot.TopLeft })
) // end Vertical Panel
) // end Auto Panel
myDiagram.nodeTemplate =
new go.Node("Auto",
{ // dropping on a Node is the same as dropping on its containing Group, even if it's top-level
mouseDrop: (e, node) => finishDrop(e, node.containingGroup),
isShadowed: true,
shadowBlur: 0,
shadowColor: colors.gray,
shadowOffset: new go.Point(4.5, 3.5)
})
.add(new go.Shape("RoundedRectangle", { fill: colors.yellow, stroke: null, strokeWidth: 0 }))
.add(new go.TextBlock(
{
margin: 8,
editable: true,
font: "13px Lora, serif",
})
.bindTwoWay("text"));
// initialize the Palette and its contents
myPalette =
new go.Palette("myPaletteDiv",
{
nodeTemplateMap: myDiagram.nodeTemplateMap,
groupTemplateMap: myDiagram.groupTemplateMap
});
myPalette.model = new go.GraphLinksModel([
{ text: "New Node", color: "#ACE600" },
{ isGroup: true, text: "H Group", horiz: true },
{ isGroup: true, text: "V Group", horiz: false }
]);
var slider = document.getElementById("levelSlider");
slider.addEventListener('change', reexpand);
slider.addEventListener('input', reexpand);
load();
}
function reexpand(e) {
myDiagram.commit(diag => {
var level = parseInt(document.getElementById("levelSlider").value);
diag.findTopLevelGroups().each(g => expandGroups(g, 0, level))
}, "reexpand");
}
function expandGroups(g, i, level) {
if (!(g instanceof go.Group)) return;
g.isSubGraphExpanded = i < level;
g.memberParts.each(m => expandGroups(m, i + 1, level))
}
function updateTotalGroupDepth() {
let d = 0;
myDiagram.findTopLevelGroups().each(g => d = Math.max(d, groupDepth(g)));
document.getElementById("levelSlider").max = Math.max(0, d);
}
function groupDepth(g) {
if (!(g instanceof go.Group)) return 0;
let d = 1;
g.memberParts.each(m => d = Math.max(d, groupDepth(m)+1));
return d;
}
// save a model to and load a model from JSON text, displayed below the Diagram
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="myPaletteDiv" style="width: 130px; margin-right: 2px; background-color: #dfebe5; border: solid 1px black">
</div>
<div id="myDiagramDiv" style="flex-grow: 1; height: 500px; background-color: #dfebe5; border: solid 1px black">
</div>
</div>
<p>
This sample allows the user to drag nodes, including groups, into and out of groups,
both from the Palette as well as from within the Diagram.
See the <a href="../intro/groups.html">Groups Intro page</a> for an explanation of GoJS Groups.
</p>
<p>
Highlighting to show feedback about potential addition to a group during a drag is implemented
using <a>GraphObject.mouseDragEnter</a> and <a>GraphObject.mouseDragLeave</a> event handlers.
Because <a>Group.computesBoundsAfterDrag</a> is true, the Group's <a>Placeholder</a>'s bounds are
not computed until the drop happens, so the group does not continuously expand as the user drags
a member of a group.
</p>
<p>
When a drop occurs on a Group or a regular Node, the <a>GraphObject.mouseDrop</a> event handler
adds the selection (the dragged Nodes) to the Group as new members.
The <a>Diagram.mouseDrop</a> event handler changes the dragged selected Nodes to be top-level,
rather than members of whatever Groups they had been in.
</p>
<p>
The slider controls how many nested levels of Groups are expanded. </br>
Semantic zoom level: <input id="levelSlider" type="range" min="0" max="5" />
</p>
<div id="buttons">
<button id="saveModel" onclick="save()">Save</button>
<button id="loadModel" onclick="load()">Load</button>
Diagram Model saved in JSON format:
</div>
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{"key":1, "isGroup":true, "text":"Main 1", "horiz":true},
{"key":2, "isGroup":true, "text":"Main 2", "horiz":true},
{"key":3, "isGroup":true, "text":"Group A", "group":1},
{"key":4, "isGroup":true, "text":"Group B", "group":1},
{"key":5, "isGroup":true, "text":"Group C", "group":2},
{"key":6, "isGroup":true, "text":"Group D", "group":2},
{"key":7, "isGroup":true, "text":"Group E", "group":6},
{"text":"First A", "group":3, "key":-7},
{"text":"Second A", "group":3, "key":-8},
{"text":"First B", "group":4, "key":-9},
{"text":"Second B", "group":4, "key":-10},
{"text":"Third B", "group":4, "key":-11},
{"text":"First C", "group":5, "key":-12},
{"text":"Second C", "group":5, "key":-13},
{"text":"First D", "group":6, "key":-14},
{"text":"First E", "group":7, "key":-15}
],
"linkDataArray": [ ]}
</textarea>
</div>
</body></html>