create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
620 lines (561 loc) • 28.8 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="Keeping a TreeModel in synch with a GraphLinksModel in a Regrouping editor." />
<meta itemprop="description" content="Keeping a TreeModel in synch with a GraphLinksModel in a Regrouping editor." />
<meta property="og:description" content="Keeping a TreeModel in synch with a GraphLinksModel in a Regrouping editor." />
<meta name="twitter:description" content="Keeping a TreeModel in synch with a GraphLinksModel in a Regrouping editor." />
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta itemprop="name" content="Regrouping Editor and Simultaneous Tree View" />
<meta property="og:title" content="Regrouping Editor and Simultaneous Tree View" />
<meta name="twitter:title" content="Regrouping Editor and Simultaneous Tree View" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/regrouping.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/regrouping.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/regrouping.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/regroupingTreeView.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/regroupingTreeView.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Regrouping Editor and Simultaneous Tree View | GoJS Diagramming Library
</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
<div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
<a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
href="../">
GoJS
</a>
<div class="relative">
<button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
<svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="topnavList" class="hidden md:block">
<div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
<a href="../learn/">Learn</a>
<a href="../samples/">Samples</a>
<a href="../intro/">Intro</a>
<a href="../api/">API</a>
<a href="../download.html">Download</a>
<a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
<a id="tc" href="https://nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
<a id="tb" href="https://nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
</div>
</div>
</div>
</div>
</nav>
<script>
window.addEventListener("DOMContentLoaded", function () {
// topnav
var topButton = document.getElementById("topnavButton");
var topnavList = document.getElementById("topnavList");
if (topButton && topnavList) {
topButton.addEventListener("click", function (e) {
topnavList
.classList
.toggle("hidden");
e.stopPropagation();
});
document.addEventListener("click", function (e) {
// if the clicked element isn't the list, close the list
if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
topButton.click();
}
});
// set active <a> element
var url = window
.location
.href
.toLowerCase();
var aTags = topnavList.getElementsByTagName('a');
for (var i = 0; i < aTags.length; i++) {
var lowerhref = aTags[i]
.href
.toLowerCase();
if (lowerhref.endsWith('.html'))
lowerhref = lowerhref.slice(0, -5);
if (url.startsWith(lowerhref)) {
aTags[i]
.classList
.add('active');
break;
}
}
}
});
</script>
<div class="flex flex-col prose">
<div class="w-full max-w-screen-xl mx-auto">
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>
<div id="allSampleContent" class="p-4 w-full">
<script id="code">
function init() {
myDiagram = new go.Diagram('myDiagramDiv', {
// what to do when a drag-drop occurs in the Diagram's background
mouseDrop: e => finishDrop(e, null),
// Diagram has simple horizontal layout
layout: new go.GridLayout({
wrappingWidth: Infinity,
alignment: go.GridAlignment.Position,
cellSize: new go.Size(1, 1)
}),
'commandHandler.archetypeGroupData': { isGroup: true, category: 'OfNodes' },
'undoManager.isEnabled': true,
// when a node is selected in the main Diagram, select the corresponding tree node
ChangedSelection: e => {
if (myChangingSelection) return;
myChangingSelection = true;
var diagnodes = new go.Set();
myDiagram.selection.each(n => diagnodes.add(myTreeView.findNodeForData(n.data)));
myTreeView.clearSelection();
myTreeView.selectCollection(diagnodes);
myChangingSelection = false;
}
});
var myChangingSelection = false; // to protect against recursive selection changes
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener('Modified', e => {
const button = document.getElementById('saveModel');
if (button) button.disabled = !myDiagram.isModified;
const idx = document.title.indexOf('*');
if (myDiagram.isModified) {
if (idx < 0) document.title += '*';
} else {
if (idx >= 0) document.title = document.title.slice(0, idx);
}
});
// 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();
}
// common styling for Groups
function groupStyle(grp) {
grp.background = 'transparent';
grp.ungroupable = true; // allow the user to remove the Group while keeping its members
// when selected, move the whole Group into the "Foreground" Layer
grp.selectionChanged = g => {
const newlay = g.isSelected ? "Foreground" : "";
g.layerName = newlay;
g.findSubGraphParts().each(m => m.layerName = newlay);
};
// highlight when dragging into the Group
grp.mouseDragEnter = (e, grp, prev) => highlightGroup(e, grp, true);
grp.mouseDragLeave = (e, grp, next) => highlightGroup(e, grp, false);
grp.computesBoundsAfterDrag = true;
grp.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
grp.mouseDrop = finishDrop;
grp.handlesDragDropForMembers = true; // don't need to define handlers on member Nodes and Links
grp.bindObject('background', 'isHighlighted', h => h ? 'rgba(255,0,0,0.2)' : 'transparent')
}
myDiagram.groupTemplateMap.add('OfGroups',
new go.Group("Auto", {
// Groups containing Groups lay out their members horizontally
layout: new go.GridLayout({
wrappingWidth: Infinity,
alignment: go.GridAlignment.Position,
cellSize: new go.Size(1, 1),
spacing: new go.Size(4, 4)
})
})
.apply(groupStyle)
.add(
new go.Shape('Rectangle', { fill: null, stroke: '#E69900', strokeWidth: 2 }),
new go.Panel('Vertical') // title above Placeholder
.add(
new go.Panel('Horizontal', { // button next to TextBlock
stretch: go.Stretch.Horizontal,
background: '#FFDD33',
margin: 1
})
.add(
go.GraphObject.build('SubGraphExpanderButton', { alignment: go.Spot.Right, margin: 5 }),
new go.TextBlock({
alignment: go.Spot.Left,
editable: true,
margin: 5,
font: 'bold 18px sans-serif',
stroke: '#9A6600'
})
.bindTwoWay('text')
), // end Horizontal Panel
new go.Placeholder({ padding: 5, alignment: go.Spot.TopLeft })
) // end Vertical Panel
)
); // end Group and call to add to template Map
myDiagram.groupTemplateMap.add('OfNodes',
new go.Group("Auto", {
// Groups containing Nodes lay out their members vertically
layout: new go.GridLayout({
wrappingColumn: 1,
alignment: go.GridAlignment.Position,
cellSize: new go.Size(1, 1),
spacing: new go.Size(4, 4)
})
})
.apply(groupStyle)
.add(
new go.Shape('Rectangle', { fill: null, stroke: '#0099CC', strokeWidth: 2 }),
new go.Panel('Vertical') // title above Placeholder
.add(
new go.Panel('Horizontal', { // button next to TextBlock
stretch: go.Stretch.Horizontal,
background: '#33D3E5',
margin: 1
})
.add(
go.GraphObject.build('SubGraphExpanderButton', { alignment: go.Spot.Right, margin: 5 }),
new go.TextBlock({
alignment: go.Spot.Left,
editable: true,
margin: 5,
font: 'bold 16px sans-serif',
stroke: '#006080'
})
.bindTwoWay('text')
), // end Horizontal Panel
new go.Placeholder({ padding: 5, alignment: go.Spot.TopLeft })
) // end Vertical Panel
)
); // end Group and call to add to template Map
// Nodes have a trivial definition
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, nod) => finishDrop(e, nod.containingGroup)
})
.add(
new go.Shape('Rectangle', {
fill: '#ACE600',
stroke: '#558000',
strokeWidth: 2
})
.bind('fill', 'color'),
new go.TextBlock({
margin: 5,
editable: true,
font: 'bold 13px sans-serif',
stroke: '#446700'
})
.bindTwoWay('text')
);
var myChangingModel = false; // to protect against recursive model changes
myDiagram.addModelChangedListener(e => {
if (e.model.skipsUndoManager) return;
if (myChangingModel) return;
myChangingModel = true;
// don't need to start/commit a transaction because the UndoManager is shared with myTreeView
if (e.modelChange === 'nodeGroupKey' || e.modelChange === 'nodeParentKey') {
// handle structural change: group memberships
var treenode = myTreeView.findNodeForData(e.object);
if (treenode !== null) treenode.updateRelationshipsFromData();
} else if (e.change === go.ChangeType.Property) {
var treenode = myTreeView.findNodeForData(e.object);
if (treenode !== null) treenode.updateTargetBindings();
} else if (e.change === go.ChangeType.Insert && e.propertyName === 'nodeDataArray') {
// pretend the new data isn't already in the nodeDataArray for myTreeView
myTreeView.model.nodeDataArray.splice(e.newParam, 1);
// now add to the myTreeView model using the normal mechanisms
myTreeView.model.addNodeData(e.newValue);
} else if (e.change === go.ChangeType.Remove && e.propertyName === 'nodeDataArray') {
// remove the corresponding node from myTreeView
var treenode = myTreeView.findNodeForData(e.oldValue);
if (treenode !== null) myTreeView.remove(treenode);
}
myChangingModel = false;
});
// setup the tree view; will be initialized with data by the load() function
myTreeView = new go.Diagram('myTreeView', {
initialContentAlignment: go.Spot.TopLeft,
allowMove: false, // don't let users mess up the tree
allowCopy: true, // but you might want this to be false
'commandHandler.copiesTree': true,
'commandHandler.copiesParentKey': true,
allowDelete: true, // but you might want this to be false
'commandHandler.deletesTree': true,
allowHorizontalScroll: false,
layout: new go.TreeLayout({
alignment: go.TreeAlignment.Start,
angle: 0,
compaction: go.TreeCompaction.None,
layerSpacing: 16,
layerSpacingParentOverlap: 1,
nodeIndentPastParent: 1.0,
nodeSpacing: 0,
setsPortSpot: false,
setsChildPortSpot: false,
arrangementSpacing: new go.Size(0, 0)
}),
// when a node is selected in the tree, select the corresponding node in the main diagram
ChangedSelection: e => {
if (myChangingSelection) return;
myChangingSelection = true;
var diagnodes = new go.Set();
myTreeView.selection.each(n => diagnodes.add(myDiagram.findNodeForData(n.data)));
myDiagram.clearSelection();
myDiagram.selectCollection(diagnodes);
myChangingSelection = false;
}
});
myTreeView.nodeTemplate =
new go.Node({ selectionAdorned: false })
.add(
// no Adornment: instead change panel background color by binding to Node.isSelected
go.GraphObject.build('TreeExpanderButton', {
width: 14,
'ButtonBorder.fill': 'white',
'ButtonBorder.stroke': null,
_buttonFillOver: 'rgba(0,128,255,0.25)',
_buttonStrokeOver: null
}),
new go.Panel('Horizontal', { position: new go.Point(16, 0) })
.bindObject('background', 'isSelected', s => s ? 'lightblue' : 'white')
.add(
// Icon is not needed?
//new go.Picture(
// {
// width: 14, height: 14,
// margin: new go.Margin(0, 4, 0, 0),
// imageStretch: go.ImageStretch.Uniform,
// source: "images/50x40.png"
// }),
new go.TextBlock({ editable: true })
.bindTwoWay('text')
) // end Horizontal Panel
); // end Node
// without lines
myTreeView.linkTemplate = new go.Link();
// cannot share the model itself, but can share all of the node data from the main Diagram,
// pretending the "group" relationship is the "tree parent" relationship
myTreeView.model = new go.TreeModel({ nodeParentKeyProperty: 'group' });
myTreeView.addModelChangedListener(e => {
if (e.model.skipsUndoManager) return;
if (myChangingModel) return;
myChangingModel = true;
// don't need to start/commit a transaction because the UndoManager is shared with myDiagram
if (e.modelChange === 'nodeGroupKey' || e.modelChange === 'nodeParentKey') {
// handle structural change: tree parent/children
var node = myDiagram.findNodeForData(e.object);
if (node !== null) node.updateRelationshipsFromData();
} else if (e.change === go.ChangeType.Property) {
// propagate simple data property changes back to the main Diagram
var node = myDiagram.findNodeForData(e.object);
if (node !== null) node.updateTargetBindings();
} else if (e.change === go.ChangeType.Insert && e.propertyName === 'nodeDataArray') {
// pretend the new data isn't already in the nodeDataArray for the main Diagram model
myDiagram.model.nodeDataArray.splice(e.newParam, 1);
// now add to the myDiagram model using the normal mechanisms
myDiagram.model.addNodeData(e.newValue);
} else if (e.change === go.ChangeType.Remove && e.propertyName === 'nodeDataArray') {
// remove the corresponding node from the main Diagram
var node = myDiagram.findNodeForData(e.oldValue);
if (node !== null) myDiagram.remove(node);
}
myChangingModel = false;
});
load();
}
// 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);
// share all of the data with the tree view
myTreeView.model.nodeDataArray = myDiagram.model.nodeDataArray;
// share the UndoManager too!
myTreeView.model.undoManager = myDiagram.model.undoManager;
}
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="myTreeView" style="width: 150px; 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>
This sample demonstrates the synchronization of two different models, necessitated by their being different types:
<a>TreeModel</a> for the tree view and <a>GraphLinksModel</a> for the general diagram on the right. Normally in such situations one would have a single
model with two diagrams showing the shared model. However in this case there are two separate models but the model data, including the
<a>Model.nodeDataArray</a>, are shared. That means the "group" property is used in the normal fashion in the GraphLinksModel but is used as the "parent"
reference in the TreeModel.
</p>
<p>
That introduces some complications when there are changes to the data, since they need to be reflected in other other model even though the data properties
have already been changed! This is accomplished by having a Model Changed listener on each model that explicitly updates the other model.
</p>
<div id="buttons">
<button id="saveModel" onclick="save()">Save</button>
<button id="loadModel" onclick="load()">Load</button>
</div>
<textarea id="mySavedModel" style="width: 100%; height: 300px">
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{"key":1, "text":"Main 1", "isGroup":true, "category":"OfGroups"},
{"key":2, "text":"Main 2", "isGroup":true, "category":"OfGroups"},
{"key":3, "text":"Group A", "isGroup":true, "category":"OfNodes", "group":1},
{"key":4, "text":"Group B", "isGroup":true, "category":"OfNodes", "group":1},
{"key":5, "text":"Group C", "isGroup":true, "category":"OfNodes", "group":2},
{"key":6, "text":"Group D", "isGroup":true, "category":"OfNodes", "group":2},
{"key":7, "text":"Group E", "isGroup":true, "category":"OfNodes", "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>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
<div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
<hr/>
<h3 class="text-xl">GoJS Features in this sample</h3>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Collections</h4>
<p>
<b>GoJS</b> provides its own collection classes: <a href="../api/symbols/List.html" target="api">List</a>, <a href="../api/symbols/Set.html" target="api">Set</a>, and <a href="../api/symbols/Map.html" target="api">Map</a>.
You can iterate over a collection by using an <a href="../api/symbols/Iterator.html" target="api">Iterator</a>.
More information can be found in the <a href="../intro/collections.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#collections">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Grid Layouts</h4>
<p>
This predefined layout is used for placing Nodes in a grid-like arrangement.
Nodes can be ordered, spaced apart, and wrapped as needed. This Layout ignores any Links connecting the nodes being laid out.
More information can be found in the <a href="../intro/layouts.html#GridLayout">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#gridlayout">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Tree Layout</h4>
<p>
This predefined layout is used for placing Nodes of a tree-structured graph in layers (rows or columns).
For discussion and examples of the most commonly used properties of the <a href="../api/symbols/TreeLayout.html">TreeLayout</a>,
see the <a href="../intro/trees.html">Trees</a> page in the Introduction.
More information can be found in the <a href="../intro/layouts.html#TreeLayout">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#treelayout">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Groups</h4>
<p>
The <a href="../api/symbols/Group.html" target="api">Group</a> class is used to treat a collection of <a href="../api/symbols/Node.html" target="api">Node</a>s and <a href="../api/symbols/Link.html" target="api">Link</a>s as if they were a single <a href="../api/symbols/Node.html" target="api">Node</a>.
Those nodes and links are members of the group; together they constitute a subgraph.
</p>
<p>
A subgraph is <em>not</em> another <a href="../api/symbols/Diagram.html" target="api">Diagram</a>, so there is no separate HTML Div element for the subgraph of a group.
All of the <a href="../api/symbols/Part.html" target="api">Part</a>s that are members of a <a href="../api/symbols/Group.html" target="api">Group</a> belong to the same Diagram as the Group.
There can be links between member nodes and nodes outside of the group as well as links between the group itself and other nodes.
There can even be links between member nodes and the containing group itself.
</p>
<p>
More information can be found in the <a href="../intro/groups.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#groups">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Buttons</h4>
<p>
GoJS defines several <a href="../api/symbols/Panel.html" target="api">Panel</a>s for common uses.
These include "Button", "TreeExpanderButton", "SubGraphExpanderButton", "PanelExpanderButton", "ContextMenuButton", and "CheckBoxButton".
"ContextMenuButton"s are typically used inside of "ContextMenu" Panels;
"CheckBoxButton"s are used in the implementation of "CheckBox" Panels.
</p>
<p>
These predefined panels can be used as if they were <a href="../api/symbols/Panel.html" target="api">Panel</a>-derived classes in calls to <a href="../api/symbols/GraphObject.html#build" target="api">GraphObject.build</a>.
They are implemented as simple visual trees of <a href="../api/symbols/GraphObject.html" target="api">GraphObject</a>s in <a href="../api/symbols/Panel.html" target="api">Panel</a>s,
with pre-set properties and event handlers.
</p>
<p>
More information can be found in the <a href="../intro/buttons.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#buttons">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Commands</h4>
<p>
A <a href="../api/symbols/CommandHandler.html" target="api">CommandHandler</a> handles all default keyboard input events in a Diagram.
There are many predefined methods on <a>CommandHandler</a> that implement common commands to operate on the Diagram or the current <a>Diagram.selection></a>.
</p>
<p>
You can override <a>CommandHandler.doKeyDown</a> to handle additional keyboard shortcuts or to change which commands are invoked via the keyboard.
<p>
Your code can invoke a command by calling the appropriate method on the <a>Diagram.commandHandler</a>.
Each command method has a corresponding <b>can...</b> predicate that your code can use to enable or disable any buttons that invoke the command.
Your code can customize the behavior of a command by overriding the method on <a>CommandHandler</a>,
or by setting properties on the <a>CommandHandler</a> or <a>Diagram</a> or <a>Part</a>s --
see <a href="../intro/permissions.html">GoJS Permissions</a>.
</p>
<p>
There are several CommandHandler extensions that provide additional functionality.
<p>
More information can be found in the <a href="../intro/commands.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#commands">Related samples</a>
</p>
<hr>
</div>
</div>
</body>
<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>