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>