Okay. But, do we have any sample examples to navigate inside the node/panel/graph object using a11y??
In the diagram below. How can we navigate/access using a11y to Click me! button which is a GraphObject.
Okay. But, do we have any sample examples to navigate inside the node/panel/graph object using a11y??
In the diagram below. How can we navigate/access using a11y to Click me! button which is a GraphObject.
Good morning. Not at this moment, but as it so happens, I was working on such a sample yesterday. It will be a while before it’s ready for me to share with you.
OK, here you go, a custom CommandHandler that supports showing a “focus” Adornment and handles arrow keys, Enter, Escape, and Space in order to navigate around most any graph of Nodes, Links, Groups, and simple Parts, and also inside any Part to focus on any GraphObject that has a GraphObject.contextMenu, a GraphObject.click event handler, or a GraphObject.toolTip.
I think it satisfies your navigation requirements. It doesn’t do anything else about WAI-ARIA compliance, which is even more application-specific than navigation.
It’s too big to put in a single post, so I’ve split up the single HTML file. A newer version of the code is also available in Typescript.
<!DOCTYPE html>
<html>
<head>
<title>General Keyboard Navigation CommandHandler</title>
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<p>
The custom <b>KeyboardCommandHandler</b> adds the notion of focus for individual GraphObjects,
rather than HTML focus for the whole Diagram/Div.
It shows where focus is in the diagram and handles the four arrow keys,
<code>Enter</code>, <code>Escape</code>, and <code>Space</code>.
All but <code>Space</code> may change what has focus.
Only <code>control-Enter</code> or <code>Space</code> may change the state of the diagram
when they invoke the click event handlers (or show context menus or tooltips).
Focus is independent of selection, so normal operations using mouse or finger do not affect and
are unaffected by focus.
</p>
<p>
Basically, <code>Enter</code> to start using keyboard navigation, use the arrow keys to move around,
hit <code>Enter</code> to enter Groups and <code>Escape</code> to leave Groups.
While using arrow keys, hold down <code>shift</code> to navigate amongst Nodes via Link connections,
or hold down <code>control</code> to navigate the same way but focusing on Links between the Nodes.
At any Part, <code>control-Enter</code> to invoke a context menu or click handler or tooltip,
or <code>shift-Enter</code> to navigate the GraphObjects within the visual tree of that Part, stopping
at those objects with context menu, click event handler (buttons), or tooltip.
</p>
<p>
The arrow keys change which GraphObject has the focus relative to the current focus.
For Nodes, navigates to the closest Node in that direction;
with <code>shift</code> navigates to connected Nodes; with <code>control</code> navigates to connected Links.
For Links, navigates to either connected Node.
For simple Parts, navigates to the closest non-Link non-Adornment in that direction.
</p>
<p>
<code>Enter</code> when nothing has focus changes focus to the first selected Part, or else to a top-level Node.
On an expanded Group it changes focus to the first Part that is a member of the Group.
On a Part, <code>shift-Enter</code> changes focus to the first GraphObject that has a context menu or a click event handler.
On any GraphObject or for Diagram <code>control-Enter</code> does something on that focused Part or GraphObject or Diagram,
either showing a context menu or calling a click event handler.
</p>
<p>
<code>Escape</code> changes focus from a GraphObject to its containing Part,
or for a Part changes focus to its containing Group, or for a top-level Part removes focus from the diagram.
</p>
<p>
<code>Space</code> (also with <code>control</code> or <code>shift</code>) selects or deselects the focused Part.
If the focused GraphObject is not a Part, it acts to show its context menu or calls its click event handler
(like <code>control-Enter</code>).
</p>
<p>
When nothing has focus, toggle turning off or on the keyboard behavior by <code>control-shift-Enter</code>.
That will let the arrow keys scroll.
The <code>PageUp</code>, <code>PageDown</code>, <code>Home</code>, <code>End</code>, and all other standard keyboard commands
are unaffected by this custom CommandHandler.
</p>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
// This extension of CommandHandler shows an Adornment highlighting the current GraphObject that has "focus".
// It handles arrow keys, Enter, Escape, and Space keys to allow the user to change which GraphObject has focus.
// An instance of it should be installed as the Diagram.commandHandler.
// If support for context menus is desired, also install a KeyboardContextMenuTool,
// replacing the Diagram's standard ContextMenuTool.
//
//?? Future possible extensions:
// Invoking tools (besides ContextMenuTool and ToolManager)
// Unable to focus on a completely disconnected Link (unless first selected)
class KeyboardCommandHandler extends go.CommandHandler {
constructor(init) {
super();
this._isNavigationEnabled = true;
this._focusBox = new go.Adornment("Auto", { layerName: "Tool" }) // so that it's in front of all regular Adornments
.add(
new go.Panel("Spot")
.add(
new go.Shape({ name: "SHAPE", fill: null, stroke: "lime", strokeWidth: 2 })
.bindObject("stroke", "adornedPart", part => part.isSelected ? "darkcyan" : "lime"),
new go.Shape({ stretch: go.Stretch.Fill, fill: null, stroke: "magenta", strokeWidth: 2, strokeDashArray: [4, 4] })
),
new go.Placeholder()
);
// internal, for now
this._sourceNode = null;
this._sourceFrom = true;
if (init) Object.assign(this, init);
}
// Whether the arrow/Enter/Escape/Space keys are used for doing navigation; default is true.
get isNavigationEnabled() { return this._isNavigationEnabled; }
set isNavigationEnabled(enabled) {
this._isNavigationEnabled = enabled;
if (!enabled) this.focus = null;
}
// The Adornment used to show the current GraphObject that has focus.
// Defaults to two Shapes surrounding the focus object: a lime (or darkcyan if Part.isSelected) border
// and a dotted magenta border.
get focusBox() { return this._focusBox; }
set focusBox(adornment) {
if (adornment instanceof go.Adornment) {
this._focusBox = adornment;
}
}
// The GraphObject that has focus for this KeyboardCommandHandler; may be null.
get focus() {
const obj = this._focusBox.adornedObject;
if (obj !== null && obj.diagram === null) {
this.focus = null;
return null;
}
return obj;
}
set focus(obj) {
const old = this._focusBox.adornedObject; // not .focus!
if (obj !== old) {
if (obj === null) {
this._setSource(null);
if (this._focusBox.adornedPart !== null) {
this._focusBox.adornedPart.removeAdornment("Focus");
this._focusBox.adornedObject = null;
}
return;
}
const part = obj.part;
if (part === null) return;
const pb = part.actualBounds;
const ob = obj.getDocumentBounds();
if (!ob.isReal()) return;
if (this._focusBox.adornedPart !== null) {
this._focusBox.adornedPart.removeAdornment("Focus");
}
const shp = this._focusBox.findObject("SHAPE");
if (shp !== null) {
shp.desiredSize = ob.size.inflate(shp.strokeWidth, shp.strokeWidth);
}
this._focusBox.adornedObject = obj;
part.addAdornment("Focus", this._focusBox);
this.onFocusChanged(old, obj);
}
}
// This called immediately after focus changes.
// The default behavior is to scroll to the new focus object if it's out of the viewport
// It would be easy to maintain a history by overriding this method.
onFocusChanged(oldobj, newobj) { // either OLDOBJ or NEWOBJ might be null
if (newobj) {
if (!this.diagram.viewportBounds.containsRect(newobj.part.actualBounds)) {
this.diagram.commandHandler.scrollToPart(newobj.part);
}
}
}
/*override*/
doKeyDown() {
const diagram = this.diagram;
if (diagram === null) return;
const e = diagram.lastInput;
const k = e.commandKey;
// toggling isNavigationEnabled must be done before all keyboard commands
if ((e.control || e.meta) && e.shift && k === 'Enter' && this.focus === null) {
this.isNavigationEnabled = !this.isNavigationEnabled;
}
if (!this.isNavigationEnabled) {
super.doKeyDown();
return;
}
const obj = this.focus;
if (k === 'ArrowUp' || k === 'ArrowDown' || k === 'ArrowLeft' || k === 'ArrowRight') {
this._arrowObject(obj);
} else if (k === 'Enter') {
this._enterObject(obj);
} else if (k === 'Escape') {
this._escapeObject(obj);
} else if (k === 'Space') {
this._selectObject(obj);
} else {
super.doKeyDown();
}
}
// Handle arrow keys, probably changing the focus relative to the current focus object.
// Unmodified arrow keys choose the next object based on its location in the desired direction.
// Shift arrow keys follow links, not caring about where the other nodes are.
// Control arrow keys do the same, but also focus at a link before getting to a node.
// When the focus objects are simple Parts or GraphObjects within a Part,
// the shift and control modifiers don't matter because there are no link relationships between the objects.
_arrowObject(obj) {
const diagram = this.diagram;
const e = diagram.lastInput;
const k = e.commandKey;
if (obj === null) {
const root = this._findTopLevelChoices(diagram.isTreePathToChildren).first();
if (root !== null) this.focus = root;
} else if (obj instanceof go.Link) {
const link = obj;
if (k === 'ArrowUp') {
let n = diagram.isTreePathToChildren ? link.fromNode : link.toNode;
while (n !== null && !n.isVisible()) n = n.containingGroup;
if (n !== null) this.focus = n;
} else if (k === 'ArrowDown') {
let n = diagram.isTreePathToChildren ? link.toNode : link.fromNode;
while (n !== null && !n.isVisible()) n = n.containingGroup;
if (n !== null) this.focus = n;
} else if (k === 'ArrowLeft') {
if (this._sourceNode !== null) {
const uplevellinks = this._findLinkChoices(this._sourceNode, this._sourceFrom);
const idx = uplevellinks.indexOf(link);
if (idx > 0) this.focus = uplevellinks.elt(idx-1);
}
} else if (k === 'ArrowRight') {
if (this._sourceNode !== null) {
const uplevellinks = this._findLinkChoices(this._sourceNode, this._sourceFrom);
const idx = uplevellinks.indexOf(link);
if (idx >= 0 && idx < uplevellinks.count-1) this.focus = uplevellinks.elt(idx+1);
}
}
} else if (obj instanceof go.Node) {
const node = obj;
if (k === 'ArrowUp') {
if (e.control || e.meta) { // to a link
const link = this._findLinkChoices(node, !diagram.isTreePathToChildren).first();
if (link) {
this._setSource(node, !diagram.isTreePathToChildren);
this.focus = link;
}
} else if (e.shift) { // follow a link to a node
const parent = this._findNodeChoices(node, !diagram.isTreePathToChildren).first();
if (parent) {
this._setSource(node, !diagram.isTreePathToChildren);
this.focus = parent;
}
} else {
this._setSource(null);
this.focus = this._findNearestTowards(270);
}
} else if (k === 'ArrowDown') {
if (e.control || e.meta) { // to a link
const link = this._findLinkChoices(node, diagram.isTreePathToChildren).first();
if (link) {
this._setSource(node, diagram.isTreePathToChildren);
this.focus = link;
}
} else if (e.shift) { // follow a link to a node
const child = this._findNodeChoices(node, diagram.isTreePathToChildren).first();
if (child) {
this._setSource(node, diagram.isTreePathToChildren);
this.focus = child;
}
} else {
this._setSource(null);
this.focus = this._findNearestTowards(90);
}
} else if (k === 'ArrowLeft') {
if (e.shift || e.control || e.meta) { // follow links to nodes
if (this._sourceNode !== null) {
const uplevelnodes = this._findNodeChoices(this._sourceNode, this._sourceFrom);
const idx = uplevelnodes.indexOf(node);
if (idx > 0) this.focus = uplevelnodes.elt(idx-1);
}
} else {
this._setSource(null);
this.focus = this._findNearestTowards(180);
}
} else if (k === 'ArrowRight') {
if (e.shift || e.control || e.meta) { // follow links to nodes
if (this._sourceNode !== null) {
const uplevelnodes = this._findNodeChoices(this._sourceNode, this._sourceFrom);
const idx = uplevelnodes.indexOf(node);
if (idx >= 0 && idx < uplevelnodes.count-1) this.focus = uplevelnodes.elt(idx+1);
}
} else {
this._setSource(null);
this.focus = this._findNearestTowards(0);
}
}
} else if (obj instanceof go.Part) { // handle Adornments and simple Parts
this._setSource(null);
const part = obj;
if (k === 'ArrowUp') { // with or without shift or control modifier
this.focus = this._findNearestTowards(270);
} else if (k === 'ArrowDown') {
this.focus = this._findNearestTowards(90);
} else if (k === 'ArrowLeft') {
this.focus = this._findNearestTowards(180);
} else if (k === 'ArrowRight') {
this.focus = this._findNearestTowards(0);
}
} else { // some GraphObject within a Part
const part = obj.part;
// if (e.shift) { // respecting "tab"/insertion order
// if (k === 'ArrowLeft' || k === 'ArrowUp') {
// const buttons = this._findButtons(part);
// const idx = buttons.indexOf(obj);
// if (idx > 0) this.focus = buttons.elt(idx-1);
// } else if (k === 'ArrowRight' || k === 'ArrowDown') {
// const buttons = this._findButtons(part);
// const idx = buttons.indexOf(obj);
// if (idx >= 0 && idx < buttons.count-1) this.focus = buttons.elt(idx+1);
// }
// } else {
if (k === 'ArrowUp') { // with or without shift or control modifier
this.focus = this._findNearestTowards(270);
} else if (k === 'ArrowDown') {
this.focus = this._findNearestTowards(90);
} else if (k === 'ArrowLeft') {
this.focus = this._findNearestTowards(180);
} else if (k === 'ArrowRight') {
this.focus = this._findNearestTowards(0);
}
// }
}
} // end of arrowObject()
// internal method of keeping track of the "parent" node
_setSource(node, from) {
this._sourceNode = node;
if (from === true || from === false) this._sourceFrom = from;
}
// Enter the focused GraphObject, or if there is no focused object, focus on
// either the first selected Part or some top-level Part that might be a root node.
// When there's the control modifier, either:
// - open its contextMenu and focus on its first button, or
// - call its click event handler, or
// - show its toolTip
// If it's a Part, with the shift modifier, if it has buttons, focus on its first button.
_enterObject(obj) {
const diagram = this.diagram;
const e = diagram.lastInput;
if (obj === null) {
if ((e.control || e.meta) && !e.shift) { // "open" the diagram
this._openObject(diagram); // but not when toggling isNavigationEnabled true
} else if (diagram.selection.count > 0) {
this.focus = diagram.selection.first();
} else {
const root = this._findTopLevelChoices(diagram.isTreePathToChildren).first();
if (root !== null) this.focus = root;
}
} else if (obj instanceof go.Part) {
if (e.shift) { // enter into a Part if it has manipulable GraphObjects
const buttons = this._findButtons(obj);
if (buttons.count > 0) {
this.focus = buttons.first();
}
} else if (e.control || e.meta) { // "open" the Part
this._openObject(obj);
} else if (obj instanceof go.Group && obj.isSubGraphExpanded) {
const mem = obj.memberParts.first();
if (mem !== null && mem.isVisible()) this.focus = mem;
} else if (obj instanceof go.Link && obj.isLabeledLink) {
const lab = obj.labelNodes.first();
if (lab !== null && lab.isVisible()) this.focus = lab;
}
} else if (e.control || e.meta) { // "open" the GraphObject
this._openObject(obj);
}
}
// Return a List of top-level Nodes with a minimum number of Links connected.
// The direction of the link connected withe the node is determined by INTO.
_findTopLevelChoices(into) {
const results = new go.List();
let num = 0;
while (results.count === 0) {
const it = this.diagram.nodes;
while (it.next()) {
const node = it.value;
if (node.isTopLevel && node.isVisible() &&
(into ? node.findNodesInto() : node.findNodeOutOf()).count === num) {
results.add(node);
}
}
num++;
}
return results;
}
// Return a List of GraphObjects in "tab" order (i.e. insertion order)
// that either have GraphObject.contextMenu or GraphObject.click event handler.
_findButtons(part) {
return this._walkPanels(part, new go.List());
}
_walkPanels(obj, results) {
if (!(obj instanceof go.Part) && // don't include the Part itself
obj.isVisibleObject() &&
(obj.contextMenu || obj.click || obj.toolTip)) {
results.add(obj);
}
if (obj instanceof go.Panel) {
obj.elements.each(elt => this._walkPanels(elt, results));
}
return results;
}
// Do something for the focused GraphObject or Diagram.
// If there's a contextMenu, show it and focus to its first element with a click handler.
// If there's a click handler, call it with an artifical InputEvent.
// If there's a toolTip, show it.
_openObject(obj) {
const diagram = this.diagram;
if (obj === null) obj = diagram;
if (obj.contextMenu) {
diagram.commandHandler.showContextMenu(obj);
const cm = diagram.toolManager.contextMenuTool.currentContextMenu;
if (cm instanceof go.Adornment) {
const buttons = this._findButtons(cm);
if (buttons.count > 0) {
this.focus = buttons.first();
}
} else {
//?? notify about showing some HTML
}
} else if (obj.click) {
const ie = new go.InputEvent();
ie.diagram = diagram;
ie.documentPoint = obj instanceof go.GraphObject ? obj.getDocumentPoint(go.Spot.Center) : diagram.viewportBounds.center;
ie.viewPoint = diagram.transformDocToView(ie.documentPoint);
ie.buttons = 1;
ie.clickCount = 1;
ie.down = true;
ie.up = true;
ie.targetObject = obj;
diagram.lastInput = ie;
obj.click(ie, obj);
} else if (obj.toolTip) {
const ie = new go.InputEvent();
ie.diagram = diagram;
ie.documentPoint = obj.getDocumentPoint(go.Spot.Center);
ie.viewPoint = diagram.transformDocToView(ie.documentPoint);
ie.targetObject = ie;
diagram.lastInput = ie;
diagram.toolManager.showToolTip(obj.toolTip, obj);
}
}
// Change the current focus to the containing one, or for top-level Parts, remove focus altogether.
_escapeObject(obj) {
const diagram = this.diagram;
if (diagram.selection.count > 0) { // clear the selection first and stop any tool
super.doKeyDown();
} else { // and only then change or remove the focus
if (this.focus instanceof go.Part) {
const part = this.focus;
if (part instanceof go.Node && part.labeledLink !== null) {
this.focus = part.labeledLink;
} else if (part.containingGroup !== null) {
this.focus = part.containingGroup;
} else {
this.focus = null;
}
} else if (this.focus instanceof go.GraphObject) {
this.focus = this.focus.part;
}
}
}
// Handle when the user presses the Space key in order to select or deselect the Part in focus.
// For focused GraphObjects within Parts, such as buttons, the Space key clicks them.
_selectObject(obj) {
if (obj === null) return;
const diagram = this.diagram;
const e = diagram.lastInput;
if (obj instanceof go.Part) { // do normal selection behavior
let part = obj.part;
if (e.control || e.meta) { // toggle selection
diagram.raiseDiagramEvent('ChangingSelection', diagram.selection);
while (part !== null && !part.canSelect()) part = part.containingGroup;
if (part !== null) part.isSelected = !part.isSelected;
diagram.raiseDiagramEvent('ChangedSelection', diagram.selection);
} else if (e.shift) { // add to selection
if (!part.isSelected) {
diagram.raiseDiagramEvent('ChangingSelection', diagram.selection);
while (part !== null && !part.canSelect()) part = part.containingGroup;
if (part !== null) part.isSelected = true;
diagram.raiseDiagramEvent('ChangedSelection', diagram.selection);
}
} else {
if (!part.isSelected) {
while (part !== null && !part.canSelect()) part = part.containingGroup;
if (part !== null) diagram.select(part);
}
}
} else {
this._openObject(obj);
}
}
// Return a List of Nodes for which to consider navigating to from a given NODE.
// The OUTOF parameter controls whether those Nodes are connected via Links going out of the Node or into the Node.
_findNodeChoices(node, outof) {
const results = new go.Set();
if (node !== null) {
if (node instanceof go.Group) {
const it = node.findExternalLinksConnected();
while (it.next()) {
const l = it.value;
if (!l.isVisible()) continue;
let n = outof ? l.toNode : l.fromNode;
let o = outof ? l.fromNode : l.toNode;
if (n === null || n === node || n === o || n.isMemberOf(node)) continue;
while (!n.isVisible()) n = n.containingGroup;
if (n !== null && n.isVisible()) {
results.add(n);
}
}
} else {
const it = outof ? node.findNodesOutOf() : node.findNodesInto();
while (it.next()) {
let n = it.value;
while (!n.isVisible()) n = n.containingGroup;
if (n !== null && n !== node && n.isVisible()) {
results.add(n);
}
}
}
}
return results.toList();
}
// Return a List of Links for which to consider navigating to from a given NODE.
// The OUTOF parameter controls whether those are Links going out of the Node or into the Node.
_findLinkChoices(node, outof) {
const results = new go.Set();
if (node instanceof go.Group) {
const it = node.findExternalLinksConnected();
while (it.next()) {
const l = it.value;
if (!l.isVisible()) continue;
if (outof ? (l.fromNode === node || l.fromNode.isMemberOf(node)) :
(l.toNode === node || l.toNode.isMemberOf(node))) {
results.add(l);
}
}
} else {
const it = outof ? node.findLinksOutOf() : node.findLinksInto();
while (it.next()) {
const link = it.value;
if (link.isVisible()) {
results.add(link);
}
}
}
return results.toList();
}
_findNearestTowards(dir) {
if (dir < 0) dir += 360;
else if (dir >= 360) dir -= 360;
const obj = this.focus;
if (!obj) return null;
const center = obj.getDocumentBounds().center;
let closestDist = Infinity;
let closestObj = obj;
const checkNearer = (elt, bnds) => {
// quick ruling out of some GraphObjects
if (dir === 0 && (bnds.right < center.x || (bnds.x - center.x)**2 > closestDist)) return;
else if (dir === 90 && (bnds.bottom < center.y || (bnds.y - center.y)**2 > closestDist)) return;
else if (dir === 180 && (bnds.x > center.x || (center.x - bnds.right)**2 > closestDist)) return;
else if (dir === 270 && (bnds.y > center.y || (center.y - bnds.bottom)**2 > closestDist)) return;
// check that the ELT is within 45 degrees of the given DIR
const ang = center.direction(bnds.centerX, bnds.centerY);
const diff = Math.min(Math.abs(dir - ang), Math.min(Math.abs(dir + 360 - ang), Math.abs(dir - 360 - ang)));
if (diff <= 45) {
let dist = center.distanceSquared(bnds.centerX, bnds.centerY);
// increase distance the further the angle
if (dir === 0 || dir === 180) {
dist += Math.abs(center.y - bnds.centerY)**2;
} else {
dist += Math.abs(center.x - bnds.centerX)**2;
}
if (dist < closestDist) {
closestDist = dist;
closestObj = elt;
}
}
};
if (obj instanceof go.Part) {
const grp = obj.containingGroup;
if (grp !== null) {
const it = grp.memberParts;
while (it.next()) {
const elt = it.value;
if (elt === obj || elt instanceof go.Link) continue;
checkNearer(elt, elt.actualBounds);
}
} else {
let it = obj.diagram.nodes;
while (it.next()) {
const elt = it.value;
if (elt === obj || !elt.isTopLevel) continue;
checkNearer(elt, elt.actualBounds);
}
it = obj.diagram.parts;
while (it.next()) {
const elt = it.value;
if (elt === obj || !elt.isTopLevel) continue;
checkNearer(elt, elt.actualBounds);
}
}
} else {
const part = obj.part;
const it = this._findButtons(part).iterator;
const bnds = new go.Rect();
while (it.next()) {
const elt = it.value;
if (elt === obj) continue;
elt.getDocumentBounds(bnds);
checkNearer(elt, bnds);
}
}
return closestObj;
}
} // end KeyboardCommandHandler
The rest of the HTML page, the sample app code:
const myDiagram =
new go.Diagram("myDiagramDiv", {
commandHandler: new KeyboardCommandHandler(),
contextMenuTool: new KeyboardContextMenuTool(),
click: e => console.log("background click at", e.documentPoint.toString()),
layout: new go.LayeredDigraphLayout({ direction: 90, setsPortSpots: false }),
"undoManager.isEnabled": true
});
myDiagram.nodeTemplate =
new go.Node("Auto", {
fromSpot: go.Spot.BottomSide, toSpot: go.Spot.TopSide,
//click: (e, node) => console.log("clicked node", node.data.text),
contextMenu:
go.GraphObject.build("ContextMenu")
.add(
go.GraphObject.build("ContextMenuButton", {
click: (e, button) => console.log("context menu 1 on", button.part.adornedPart.data.text)
})
.add(new go.TextBlock("command 1")),
go.GraphObject.build("ContextMenuButton", {
click: (e, button) => console.log("context menu 2 on", button.part.adornedPart.data.text)
})
.add(new go.TextBlock("command 2"))
)
})
.add(
new go.Shape({ fill: "white" })
.bind("fill", "color"),
new go.Panel("Table")
.add(
new go.Panel("Horizontal", { columnSpan: 9, margin: new go.Margin(6, 4, 2, 4) })
.add(
new go.TextBlock({ margin: new go.Margin(0, 4, 0, 0) })
.bind("text"),
go.GraphObject.build("TreeExpanderButton")
),
go.GraphObject.build("Button", {
row: 1, column: 0, margin: 2,
click: (e, but) => {
console.log("deleted", but.part.data.text);
e.diagram.select(but.part);
e.diagram.commandHandler.deleteSelection();
}
})
.bind("visible", "text", t => t.indexOf("a") >= 0)
.add(new go.TextBlock("Del")),
go.GraphObject.build("Button", { row: 1, column: 1, margin: 2, click: (e, but) => console.log("clicked 2 on", but.part.data.text) })
.bind("visible", "text", t => t.indexOf("e") >= 0)
.add(new go.TextBlock("b 2")),
go.GraphObject.build("Button", { row: 1, column: 2, margin: 2, click: (e, but) => console.log("clicked 3 on", but.part.data.text) })
.bind("visible", "text", t => t.indexOf("t") >= 0)
.add(new go.TextBlock("b 3")),
)
);
function makePort(id, text, spot) {
return new go.Shape({
portId: id, fromLinkable: true, toLinkable: true,
fromSpot: go.Spot.Bottom, toSpot: go.Spot.Top,
width: 8, height: 8, margin: new go.Margin(0, 1),
fill: "gray", strokeWidth: 0,
toolTip: go.GraphObject.build("ToolTip")
.add( new go.TextBlock(text) )
});
}
myDiagram.nodeTemplateMap.add("Ports",
new go.Node("Spot")
.add(
new go.Panel("Auto")
.add(
new go.Shape({ fill: "white" })
.bind("fill", "color"),
new go.TextBlock({ margin: 8 })
.bind("text")
),
new go.Panel("Horizontal", { alignment: go.Spot.Top })
.add(
makePort("i1", "first input"),
makePort("i2", "second input"),
makePort("i3", "third input")
),
new go.Panel("Horizontal", { alignment: go.Spot.Bottom })
.add(
makePort("o1", "first output"),
makePort("o2", "second output")
)
));
myDiagram.nodeTemplateMap.add("Simple",
new go.Node("Auto")
.add(
new go.Shape("Capsule", { fill: "white" })
.bind("fill", "color"),
new go.TextBlock()
.bind("text")
));
myDiagram.groupTemplate =
new go.Group("Vertical", {
layout: new go.LayeredDigraphLayout({ direction: 90 }),
//click: (e, node) => console.log("clicked group", node.data.text),
contextMenu:
go.GraphObject.build("ContextMenu")
.add(
go.GraphObject.build("ContextMenuButton", {
click: (e, button) => console.log("group menu on", button.part.adornedPart.data.text)
})
.add(new go.TextBlock("only command"))
)
})
.add(
new go.Panel("Horizontal")
.add(
go.GraphObject.build("SubGraphExpanderButton"),
new go.TextBlock({ font: "bold 12pt sans-serif" })
.bind("text")
),
new go.Panel("Auto")
.add(
new go.Shape({ fill: "whitesmoke", stroke: "gray", strokeWidth: 2 }),
new go.Placeholder({ padding: 14 })
)
);
myDiagram.linkTemplate =
new go.Link({ corner: 10,
relinkableFrom: true, relinkableTo: true,
reshapable: true, resegmentable: true
})
.add(
new go.Shape(),
new go.Shape({ toArrow: "OpenTriangle" }),
go.GraphObject.build("Button", {
click: (e, button) => console.log("clicked link button", button.part.data.from, button.part.data.to)
})
.add(new go.Shape("Diamond", { width: 8, height: 8, fill: "yellow" }))
);
myDiagram.linkTemplateMap.add("Simple",
new go.Link().add(new go.Shape())
);
myDiagram.model = new go.GraphLinksModel(
{
linkFromPortIdProperty: "fpid",
linkToPortIdProperty: "tpid",
nodeDataArray: [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink", group: -1 },
{ key: 5, text: "Epsilon", color: "yellow", category: "Ports", group: -1 },
{ key: -1, isGroup: true, text: "Group-1" },
{ key: 6, text: "Simple 1", color: "lavender", category: "Simple", group: -1 },
{ key: 7, text: "Simple 1", color: "lavender", category: "Simple", group: -1 },
{ key: 11, text: "Zeta", color: "lightblue" },
{ key: 12, text: "Eta", color: "orange" },
{ key: 13, text: "Theta", color: "lightgreen" },
{ key: 14, text: "Iota", color: "pink", group: -2 },
{ key: 15, text: "Kappa", color: "yellow", category: "Ports", group: -2 },
{ key: -2, isGroup: true, text: "Group-2" },
{ key: 21, text: "A 1", category: "Ports" },
{ key: 22, text: "A 2", category: "Ports" },
{ key: 23, text: "B 1", category: "Ports" },
{ key: 24, text: "B 2", category: "Ports" },
{ key: 25, text: "C 1", category: "Ports" },
{ key: 26, text: "C 2", category: "Ports" },
{ key: 31, text: "E 1", color: "yellow", category: "Ports" },
{ key: 32, text: "E 2", color: "yellow", category: "Ports" },
{ key: 33, text: "E 3", color: "yellow", category: "Ports" },
{ key: 34, text: "E 4", color: "yellow", category: "Ports" },
{ key: 35, text: "E 5", color: "yellow", category: "Ports" },
{ key: 36, text: "E 6", color: "yellow", category: "Ports" },
{ key: 37, text: "E 7", color: "yellow", category: "Ports" },
],
linkDataArray: [
{ from: 2, to: 2 },
{ from: 2, to: 2 },
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 3, to: 4 },
{ from: 1, to: 5 },
{ from: 4, to: 1 },
{ from: 6, to: 7, category: "Simple" },
{ from: 11, to: 12 },
{ from: 11, to: 13 },
{ from: 13, to: 14 },
{ from: 13, to: -2 },
{ from: 21, fpid: "o1", to: 23, tpid: "i1" },
{ from: 21, fpid: "o2", to: 24, tpid: "i1" },
{ from: 22, fpid: "o1", to: 23, tpid: "i2" },
{ from: 22, fpid: "o2", to: 24, tpid: "i3" },
{ from: 23, fpid: "o1", to: 25, tpid: "i1" },
{ from: 23, fpid: "o2", to: 26, tpid: "i3" },
{ from: 24, fpid: "o1", to: 25, tpid: "i2" },
{ from: 24, fpid: "o2", to: 26, tpid: "i1" },
{ from: 31, to: 34 },
{ from: 32, to: 34 },
{ from: 33, to: 34 },
{ from: 34, to: 35 },
{ from: 34, to: 36 },
{ from: 34, to: 37 },
]
});
</script>
</body>
</html>
Hi @walter I think this example suits our use case. I will try to use this code & will make changes further. But can we directly copy & use this code for business?
We do have an enterprise license for GoJS. So, does copying this kind of code snippet directly in our business use cases have copyright issues?
Good
No copyright problems – all of our samples are included for your use under the license agreement.