Giving you two edited versions using the sample from the gojs samples. one with your fix and another i worked on top of yours.
Please let me know if you think my fix might have any issues.
Recursive Function →
const changeLayer = (g,newlay) => {
if(g.containingGroup){
changeLayer(g.containingGroup,newlay)
g.containingGroup.memberParts.each(function (m) {
if(g.key !==m.key) {
if(m instanceof go.Group){
m.findSubGraphParts().each((siblingPart)=>{
siblingPart.layerName = newlay;
})
}
m.layerName = newlay;
}
});
g.layerName = newlay;
}
else{
g.layerName = newlay;
}
}
Group →
selectionChanged: g => {
var newlay = g.isSelected ? "Foreground" : "";
changeLayer(g,newlay);
g.findSubGraphParts().each((memberPart) => {
memberPart.layerName = newlay;
});
},
Node →
selectionChanged: g => {
var newlay = g.isSelected ? "Foreground" : "";
if(g.containingGroup){
changeLayer(g,newlay);
} else{
g.layerName = newlay;
}
}
Full Sample with your fix →
<!DOCTYPE html>
<html lang="en">
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/release/go.js"></script>
<p>
This is a minimalist HTML and JavaScript skeleton of the GoJS Sample
<a href="https://gojs.net/latest/samples/regrouping.html">regrouping.html</a>. It was automatically generated from a
button on the sample page,
and does not contain the full HTML. It is intended as a starting point to adapt for your own usage.
For many samples, you may need to inspect the
<a href="https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/regrouping.html">full source on Github</a>
and copy other files or scripts.
</p>
<div id="allSampleContent" class="p-4 w-full">
<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: new go.GridLayout({ // Diagram has simple horizontal layout
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();
const slider = document.getElementById('levelSlider');
const oldMax = parseInt(slider.max);
updateTotalGroupDepth();
const newMax = parseInt(slider.max);
// keep the slider value accurate to the current depth
slider.value = parseInt(slider.value) + newMax - oldMax;
}
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
selectionChanged: g => {
var newlay = g.isSelected ? "Foreground" : "";
g.layerName = newlay;
g.findSubGraphParts().each(m => { m.layerName = newlay; });
}
})
.bind('background', 'horiz', defaultColor)
.bind('layout', 'horiz', makeLayout)
.add(
new go.Shape('Rectangle', { stroke: colors.gray, strokeWidth: 1 })
.bindObject('fill', 'isHighlighted', h => h ? 'rgba(0,255,0,.3)' : 'transparent'),
new go.Panel('Vertical') // title above Placeholder
.add(
new go.Panel('Horizontal', { stretch: go.Stretch.Horizontal }) // button next to TextBlock
.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),
selectionChanged: g => {
var newlay = g.isSelected ? "Foreground" : "";
g.layerName = newlay;
}
})
.add(
new go.Shape('RoundedRectangle', { fill: colors.yellow, stroke: null, strokeWidth: 0 }),
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>
</div>
</body>
</html>
Full Sample with my fix →
<!DOCTYPE html>
<html lang="en">
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/release/go.js"></script>
<p>
This is a minimalist HTML and JavaScript skeleton of the GoJS Sample
<a href="https://gojs.net/latest/samples/regrouping.html">regrouping.html</a>. It was automatically generated from a button on the sample page,
and does not contain the full HTML. It is intended as a starting point to adapt for your own usage.
For many samples, you may need to inspect the
<a href="https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/regrouping.html">full source on Github</a>
and copy other files or scripts.
</p>
<div id="allSampleContent" class="p-4 w-full">
<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: new go.GridLayout({ // Diagram has simple horizontal layout
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();
const slider = document.getElementById('levelSlider');
const oldMax = parseInt(slider.max);
updateTotalGroupDepth();
const newMax = parseInt(slider.max);
// keep the slider value accurate to the current depth
slider.value = parseInt(slider.value) + newMax - oldMax;
}
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),
selectionChanged: g => {
var newlay = g.isSelected ? "Foreground" : "";
changeLayer(g,newlay);
g.findSubGraphParts().each((memberPart) => {
memberPart.layerName = newlay;
});
},
// 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 })
.bindObject('fill', 'isHighlighted', h => h ? 'rgba(0,255,0,.3)' : 'transparent'),
new go.Panel('Vertical') // title above Placeholder
.add(
new go.Panel('Horizontal', { stretch: go.Stretch.Horizontal }) // button next to TextBlock
.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
const changeLayer = (g,newlay) => {
if(g.containingGroup){
changeLayer(g.containingGroup,newlay)
g.containingGroup.memberParts.each(function (m) {
if(g.key !==m.key) {
if(m instanceof go.Group){
m.findSubGraphParts().each((siblingPart)=>{
siblingPart.layerName = newlay;
})
}
m.layerName = newlay;
}
});
g.layerName = newlay;
}
else{
g.layerName = newlay;
}
}
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),
selectionChanged: g => {
var newlay = g.isSelected ? "Foreground" : "";
if(g.containingGroup){
changeLayer(g,newlay);
} else{
g.layerName = newlay;
}
// if (g.containingGroup) {
// g.containingGroup.layerName = newlay;
// g.containingGroup.findSubGraphParts().each(function(m) {
// m.layerName = newlay;
// });
// }
},
})
.add(
new go.Shape('RoundedRectangle', { fill: colors.yellow, stroke: null, strokeWidth: 0 }),
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>
</div>
</body>
</html>
Thanks.