create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
961 lines (896 loc) • 41.5 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="A simple block diagram editor that includes context menus for changing shapes and colors." />
<meta itemprop="description" content="A simple block diagram editor that includes context menus for changing shapes and colors." />
<meta property="og:description" content="A simple block diagram editor that includes context menus for changing shapes and colors." />
<meta name="twitter:description" content="A simple block diagram editor that includes context menus for changing shapes and colors." />
<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="Block Diagram Editor, Drawing Nodes and Links with Different Shapes and Colors" />
<meta property="og:title" content="Block Diagram Editor, Drawing Nodes and Links with Different Shapes and Colors" />
<meta name="twitter:title" content="Block Diagram Editor, Drawing Nodes and Links with Different Shapes and Colors" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/blockeditor.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/blockeditor.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/blockeditor.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/blockEditor.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/blockEditor.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Block Diagram Editor, Drawing Nodes and Links with Different Shapes and Colors | 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 src="../extensions/Figures.js"></script>
<script src="../extensions/DrawCommandHandler.js"></script>
<script id="code">
function init() {
// Colors are predefined to allow easy manipulation of themes
const colors = {
red: '#ff3333',
blue: '#3358ff',
green: '#25ad23',
magenta: '#d533ff',
purple: '#7d33ff',
orange: '#ff6233',
brown: '#8e571e',
white: '#ffffff',
black: '#000000',
beige: '#fffcd5',
extralightblue: '#d5ebff',
extralightred: '#f2dfe0',
lightblue: '#a5d2fa',
lightgray: '#cccccc',
lightgreen: '#b3e6b3',
lightred: '#fcbbbd'
};
myDiagram = new go.Diagram('myDiagramDiv', {
padding: 20, // extra space when scrolled all the way
grid:
new go.Panel('Grid') // a simple 10x10 grid
.add(
new go.Shape('LineH', { stroke: 'lightgray', strokeWidth: 0.5 }),
new go.Shape('LineV', { stroke: 'lightgray', strokeWidth: 0.5 })
),
'draggingTool.isGridSnapEnabled': true,
handlesDragDropForTopLevelParts: true,
mouseDrop: e => {
// when the selection is dropped in the diagram's background,
// make sure the selected Parts no longer belong to any Group
var ok = e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true);
if (!ok) e.diagram.currentTool.doCancel();
},
commandHandler: new DrawCommandHandler(), // support offset copy-and-paste
// create a new node by double-clicking in background
'clickCreatingTool.archetypeNodeData': { text: 'NEW NODE' },
PartCreated: e => {
var node = e.subject; // the newly inserted Node -- now need to snap its location to the grid
node.location =
node.location.copy()
.snapToGridPoint(e.diagram.grid.gridOrigin, e.diagram.grid.gridCellSize);
setTimeout(() => {
// and have the user start editing its text
e.diagram.commandHandler.editTextBlock();
}, 20);
},
'commandHandler.archetypeGroupData': { isGroup: true, text: 'NEW GROUP' },
SelectionGrouped: e => {
var group = e.subject;
setTimeout(() => {
// and have the user start editing its text
e.diagram.commandHandler.editTextBlock();
});
},
LinkRelinked: e => {
// re-spread the connections of other links connected with both old and new nodes
var oldnode = e.parameter.part;
oldnode.invalidateConnectedLinks();
var link = e.subject;
if (e.diagram.toolManager.linkingTool.isForwards) {
link.toNode.invalidateConnectedLinks();
} else {
link.fromNode.invalidateConnectedLinks();
}
},
'undoManager.isEnabled': true
});
// Node template
myDiagram.nodeTemplate =
new go.Node('Auto', {
locationSpot: go.Spot.Center,
locationObjectName: 'SHAPE',
desiredSize: new go.Size(120, 60),
minSize: new go.Size(40, 40),
resizable: true,
resizeCellSize: new go.Size(20, 20)
})
// these Bindings are TwoWay because the DraggingTool and ResizingTool modify the target properties
.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
.bindTwoWay('desiredSize', 'size', go.Size.parse, go.Size.stringify)
.add(
new go.Shape({
// the border
name: 'SHAPE',
fill: colors.white,
cursor: 'pointer',
portId: '',
fromLinkable: true,
toLinkable: true,
fromLinkableDuplicates: true,
toLinkableDuplicates: true,
fromSpot: go.Spot.AllSides,
toSpot: go.Spot.AllSides
})
.bind('figure')
.bind('fill')
.bind('stroke', 'color')
.bind('strokeWidth', 'thickness')
.bind('strokeDashArray', 'dash'),
// this Shape prevents mouse events from reaching the middle of the port
new go.Shape({ width: 100, height: 40, strokeWidth: 0, fill: 'transparent' }),
new go.TextBlock({
margin: 1,
textAlign: 'center',
overflow: go.TextOverflow.Ellipsis,
editable: true
})
// this Binding is TwoWay due to the user editing the text with the TextEditingTool
.bindTwoWay('text')
.bind('stroke', 'color')
);
myDiagram.nodeTemplate.toolTip =
go.GraphObject.build('ToolTip') // show some detailed information
.add(
new go.Panel('Vertical', { maxSize: new go.Size(200, NaN) }) // limit width but not height
.add(
new go.TextBlock({ font: 'bold 10pt sans-serif', textAlign: 'center' })
.bind('text'),
new go.TextBlock({ font: '10pt sans-serif', textAlign: 'center' })
.bind('text', 'details')
)
);
// Node selection adornment
// Include four large triangular buttons so that the user can easily make a copy
// of the node, move it to be in that direction relative to the original node,
// and add a link to the new node.
function makeArrowButton(spot, fig) {
var maker = (e, shape) => {
e.handled = true;
e.diagram.model.commit(m => {
var selnode = shape.part.adornedPart;
// create a new node in the direction of the spot
var p = new go.Point().setRectSpot(selnode.actualBounds, spot);
p.subtract(selnode.location);
p.scale(2, 2);
p.x += Math.sign(p.x) * 30;
p.y += Math.sign(p.y) * 30;
p.add(selnode.location);
p.snapToGridPoint(e.diagram.grid.gridOrigin, e.diagram.grid.gridCellSize);
// make the new node a copy of the selected node
var nodedata = m.copyNodeData(selnode.data);
// add to same group as selected node
m.setGroupKeyForNodeData(nodedata, m.getGroupKeyForNodeData(selnode.data));
m.addNodeData(nodedata); // add to model
// create a link from the selected node to the new node
var linkdata = { from: selnode.key, to: m.getKeyForNodeData(nodedata) };
m.addLinkData(linkdata); // add to model
// move the new node to the computed location, select it, and start to edit it
var newnode = e.diagram.findNodeForData(nodedata);
newnode.location = p;
e.diagram.select(newnode);
setTimeout(() => {
e.diagram.commandHandler.editTextBlock();
}, 20);
});
};
return new go.Shape({
figure: fig,
alignment: spot,
alignmentFocus: spot.opposite(),
width: spot.equals(go.Spot.Top) || spot.equals(go.Spot.Bottom) ? 25 : 18,
height: spot.equals(go.Spot.Top) || spot.equals(go.Spot.Bottom) ? 18 : 25,
fill: 'orange',
stroke: colors.white,
strokeWidth: 4,
mouseEnter: (e, shape) => (shape.fill = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.fill = 'orange'),
isActionable: true, // needed because it's in an Adornment
click: maker,
contextClick: maker
});
}
// create a button that brings up the context menu
function CMButton(options) {
return new go.Shape({
fill: 'orange',
stroke: 'rgba(0, 0, 0, 0)',
strokeWidth: 15,
background: 'transparent',
geometryString: 'F1 M0 0 b 0 360 -4 0 4 z M10 0 b 0 360 -4 0 4 z M20 0 b 0 360 -4 0 4', // M10 0 A2 2 0 1 0 14 10 M20 0 A2 2 0 1 0 24 10,
isActionable: true,
cursor: 'context-menu',
mouseEnter: (e, shape) => (shape.fill = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.fill = 'orange'),
click: (e, shape) => {
e.diagram.commandHandler.showContextMenu(shape.part.adornedPart);
}
},
options || {}
);
}
myDiagram.nodeTemplate.selectionAdornmentTemplate =
new go.Adornment('Spot')
.add(
new go.Placeholder({ padding: 10 }),
makeArrowButton(go.Spot.Top, 'TriangleUp'),
makeArrowButton(go.Spot.Left, 'TriangleLeft'),
makeArrowButton(go.Spot.Right, 'TriangleRight'),
makeArrowButton(go.Spot.Bottom, 'TriangleDown'),
CMButton({ alignment: new go.Spot(0.75, 0) })
);
// Common context menu button definitions
// All buttons in context menu work on both click and contextClick,
// in case the user context-clicks on the button.
// All buttons modify the node data, not the Node, so the Bindings need not be TwoWay.
// A button-defining helper function that returns a click event handler.
// PROPNAME is the name of the data property that should be set to the given VALUE.
function ClickFunction(propname, value) {
return (e, obj) => {
e.handled = true; // don't let the click bubble up
e.diagram.model.commit(m => {
m.set(obj.part.adornedPart.data, propname, value);
});
};
}
// Create a context menu button for setting a data property with a color value.
function ColorButton(color, propname) {
if (!propname) propname = 'color';
return new go.Shape({
width: 16,
height: 16,
stroke: 'lightgray',
fill: color,
margin: 1,
background: 'transparent',
mouseEnter: (e, shape) => (shape.stroke = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.stroke = 'lightgray'),
click: ClickFunction(propname, color),
contextClick: ClickFunction(propname, color)
});
}
function LightFillButtons() {
// used by multiple context menus
return [
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
ColorButton(colors.white, 'fill'),
ColorButton(colors.beige, 'fill'),
ColorButton(colors.extralightblue, 'fill'),
ColorButton(colors.extralightred, 'fill')
)
),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
ColorButton(colors.lightgray, 'fill'),
ColorButton(colors.lightgreen, 'fill'),
ColorButton(colors.lightblue, 'fill'),
ColorButton(colors.lightred, 'fill')
)
)
];
}
function DarkColorButtons() {
// used by multiple context menus
return [
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
ColorButton(colors.black),
ColorButton(colors.green),
ColorButton(colors.blue),
ColorButton(colors.red)
)
),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
ColorButton(colors.white),
ColorButton(colors.magenta),
ColorButton(colors.purple),
ColorButton(colors.orange)
)
)
];
}
// Create a context menu button for setting a data property with a stroke width value.
function ThicknessButton(sw, propname) {
if (!propname) propname = 'thickness';
return new go.Shape('LineH', {
width: 16,
height: 16,
strokeWidth: sw,
margin: 1,
background: 'transparent',
mouseEnter: (e, shape) => (shape.background = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.background = 'transparent'),
click: ClickFunction(propname, sw),
contextClick: ClickFunction(propname, sw)
});
}
// Create a context menu button for setting a data property with a stroke dash Array value.
function DashButton(dash, propname) {
if (!propname) propname = 'dash';
return new go.Shape('LineH', {
width: 24,
height: 16,
strokeWidth: 2,
strokeDashArray: dash,
margin: 1,
background: 'transparent',
mouseEnter: (e, shape) => (shape.background = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.background = 'transparent'),
click: ClickFunction(propname, dash),
contextClick: ClickFunction(propname, dash)
});
}
function StrokeOptionsButtons() {
// used by multiple context menus
return [
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
ThicknessButton(1),
ThicknessButton(2),
ThicknessButton(3),
ThicknessButton(4)
)
),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
DashButton(null),
DashButton([2, 4]),
DashButton([4, 4])
)
)
];
}
// Node context menu
function FigureButton(fig, propname) {
if (!propname) propname = 'figure';
return new go.Shape({
width: 32,
height: 32,
scale: 0.5,
fill: 'lightgray',
figure: fig,
margin: 1,
background: 'transparent',
mouseEnter: (e, shape) => (shape.fill = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.fill = 'lightgray'),
click: ClickFunction(propname, fig),
contextClick: ClickFunction(propname, fig)
});
}
myDiagram.nodeTemplate.contextMenu =
go.GraphObject.build('ContextMenu')
.add(
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
FigureButton('Rectangle'),
FigureButton('RoundedRectangle'),
FigureButton('Ellipse'),
FigureButton('Diamond')
)
),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
FigureButton('Parallelogram2'),
FigureButton('ManualOperation'),
FigureButton('Procedure'),
FigureButton('Cylinder1')
)
),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
FigureButton('Terminator'),
FigureButton('CreateRequest'),
FigureButton('Document'),
FigureButton('TriangleDown')
)
),
...LightFillButtons(),
...DarkColorButtons(),
...StrokeOptionsButtons()
);
// Group template
myDiagram.groupTemplate =
new go.Group('Spot', {
layerName: 'Background',
ungroupable: true,
locationSpot: go.Spot.Center,
selectionObjectName: 'BODY',
computesBoundsAfterDrag: true, // allow dragging out of a Group that uses a Placeholder
handlesDragDropForMembers: true, // don't need to define handlers on Nodes and Links
mouseDrop: (e, grp) => {
// add dropped nodes as members of the group
var ok = grp.addMembers(grp.diagram.selection, true);
if (!ok) grp.diagram.currentTool.doCancel();
},
avoidable: false
})
.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
.add(
new go.Panel('Auto', { name: 'BODY' })
.add(
new go.Shape({
parameter1: 10,
fill: colors.white,
strokeWidth: 2,
cursor: 'pointer',
fromLinkable: true,
toLinkable: true,
fromLinkableDuplicates: true,
toLinkableDuplicates: true,
fromSpot: go.Spot.AllSides,
toSpot: go.Spot.AllSides
})
.bind('fill')
.bind('stroke', 'color')
.bind('strokeWidth', 'thickness')
.bind('strokeDashArray', 'dash'),
new go.Placeholder({ background: 'transparent', margin: 20 })
),
new go.TextBlock({
alignment: go.Spot.Top,
alignmentFocus: go.Spot.Bottom,
font: 'bold 12pt sans-serif',
editable: true
})
.bind('text')
.bind('stroke', 'color')
);
myDiagram.groupTemplate.selectionAdornmentTemplate =
new go.Adornment('Spot')
.add(
new go.Panel('Auto')
.add(
new go.Shape({ fill: null, stroke: 'dodgerblue', strokeWidth: 3 }),
new go.Placeholder({ margin: 1.5 })
),
CMButton({ alignment: go.Spot.TopRight, alignmentFocus: go.Spot.BottomRight })
);
myDiagram.groupTemplate.contextMenu =
go.GraphObject.build('ContextMenu')
.add(
...LightFillButtons(),
...DarkColorButtons(),
...StrokeOptionsButtons()
);
// Link template
myDiagram.linkTemplate =
new go.Link({
layerName: 'Foreground',
routing: go.Routing.AvoidsNodes,
corner: 10,
fromShortLength: 10,
toShortLength: 15, // assume arrowhead at "to" end, need to avoid bad appearance when path is thick
relinkableFrom: true,
relinkableTo: true,
reshapable: true,
resegmentable: true
})
.bind('fromSpot', 'fromSpot', go.Spot.parse)
.bind('toSpot', 'toSpot', go.Spot.parse)
.bind('fromShortLength', 'dir', dir => dir >= 1 ? 10 : 0)
.bind('toShortLength', 'dir', dir => dir >= 1 ? 10 : 0)
.bindTwoWay('points') // TwoWay due to user reshaping with LinkReshapingTool
.add(
new go.Shape({ strokeWidth: 2 })
.bind('stroke', 'color')
.bind('strokeWidth', 'thickness')
.bind('strokeDashArray', 'dash'),
new go.Shape({
// custom arrowheads to create the lifted effect
segmentIndex: 0,
segmentOffset: new go.Point(15, 0),
segmentOrientation: go.Orientation.Along,
alignmentFocus: go.Spot.Right,
figure: 'circle',
width: 10,
strokeWidth: 0
})
.bind('fill', 'color')
.bind('visible', 'dir', dir => dir === 1),
new go.Shape({
segmentIndex: -1,
segmentOffset: new go.Point(-10, 6),
segmentOrientation: go.Orientation.Plus90,
alignmentFocus: go.Spot.Right,
figure: 'triangle',
width: 12,
height: 12,
strokeWidth: 0
})
.bind('fill', 'color')
.bind('visible', 'dir', dir => dir >= 1)
.bind('width', 'thickness', t => 7 + 3 * t) // custom arrowhead must scale with the size of the while
.bind('height', 'thickness', t => 7 + 3 * t) // while remaining centered on line
.bind('segmentOffset', 'thickness', t => new go.Point(-15, 4 + 1.5 * t)),
new go.Shape({
segmentIndex: 0,
segmentOffset: new go.Point(15, -6),
segmentOrientation: go.Orientation.Minus90,
alignmentFocus: go.Spot.Right,
figure: 'triangle',
width: 12,
height: 12,
strokeWidth: 0
})
.bind('fill', 'color')
.bind('visible', 'dir', dir => dir === 2)
.bind('width', 'thickness', t => 7 + 3 * t)
.bind('height', 'thickness', t => 7 + 3 * t)
.bind('segmentOffset', 'thickness', t => new go.Point(-15, 4 + 1.5 * t)),
new go.TextBlock({
alignmentFocus: new go.Spot(0, 1, -4, 0),
editable: true
})
.bindTwoWay('text') // TwoWay due to user editing with TextEditingTool
.bind('stroke', 'color')
);
myDiagram.linkTemplate.selectionAdornmentTemplate =
new go.Adornment() // use a special selection Adornment that does not obscure the link path itself
.add(
new go.Shape({
// this uses a pathPattern with a gap in it, in order to avoid drawing on top of the link path Shape
isPanelMain: true,
stroke: 'transparent',
strokeWidth: 6,
pathPattern: makeAdornmentPathPattern(2) // == thickness or strokeWidth
})
.bind('pathPattern', 'thickness', makeAdornmentPathPattern),
CMButton({ alignmentFocus: new go.Spot(0, 0, -6, -4) })
);
function makeAdornmentPathPattern(w) {
return new go.Shape({
stroke: 'dodgerblue',
strokeWidth: 2,
strokeCap: 'square',
geometryString: 'M0 0 M4 2 H3 M4 ' + (w + 4).toString() + ' H3'
});
}
// Link context menu
// All buttons in context menu work on both click and contextClick,
// in case the user context-clicks on the button.
// All buttons modify the link data, not the Link, so the Bindings need not be TwoWay.
function ArrowButton(num) {
var geo = 'M0 0 M8 16 M0 8 L16 8 M12 11 L16 8 L12 5';
if (num === 0) {
geo = 'M0 0 M16 16 M0 8 L16 8';
} else if (num === 2) {
geo = 'M0 0 M16 16 M0 8 L16 8 M12 11 L16 8 L12 5 M4 11 L0 8 L4 5';
}
return new go.Shape({
geometryString: geo,
margin: 2,
background: 'transparent',
mouseEnter: (e, shape) => (shape.background = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.background = 'transparent'),
click: ClickFunction('dir', num),
contextClick: ClickFunction('dir', num)
});
}
function AllSidesButton(to) {
const setter = (e, shape) => {
e.handled = true;
e.diagram.model.commit(m => {
const link = shape.part.adornedPart;
m.set(link.data, to ? 'toSpot' : 'fromSpot', go.Spot.stringify(go.Spot.AllSides));
// re-spread the connections of other links connected with the node
(to ? link.toNode : link.fromNode).invalidateConnectedLinks();
});
};
return new go.Shape({
width: 12,
height: 12,
fill: 'transparent',
mouseEnter: (e, shape) => (shape.background = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.background = 'transparent'),
click: setter,
contextClick: setter
});
}
function SpotButton(spot, to) {
let ang = 0;
let side = go.Spot.RightSide;
if (spot.equals(go.Spot.Top)) {
ang = 270;
side = go.Spot.TopSide;
} else if (spot.equals(go.Spot.Left)) {
ang = 180;
side = go.Spot.LeftSide;
} else if (spot.equals(go.Spot.Bottom)) {
ang = 90;
side = go.Spot.BottomSide;
}
if (!to) ang -= 180;
const setter = (e, shape) => {
e.handled = true;
e.diagram.model.commit(m => {
var link = shape.part.adornedPart;
m.set(link.data, to ? 'toSpot' : 'fromSpot', go.Spot.stringify(side));
// re-spread the connections of other links connected with the node
(to ? link.toNode : link.fromNode).invalidateConnectedLinks();
});
};
return new go.Shape({
alignment: spot,
alignmentFocus: spot.opposite(),
geometryString: 'M0 0 M12 12 M12 6 L1 6 L4 4 M1 6 L4 8',
angle: ang,
background: 'transparent',
mouseEnter: (e, shape) => (shape.background = 'dodgerblue'),
mouseLeave: (e, shape) => (shape.background = 'transparent'),
click: setter,
contextClick: setter
});
}
myDiagram.linkTemplate.contextMenu =
go.GraphObject.build('ContextMenu')
.add(
...DarkColorButtons(),
...StrokeOptionsButtons(),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
ArrowButton(0),
ArrowButton(1),
ArrowButton(2)
)
),
go.GraphObject.build('ContextMenuButton')
.add(
new go.Panel('Horizontal')
.add(
new go.Panel('Spot')
.add(
AllSidesButton(false),
SpotButton(go.Spot.Top, false),
SpotButton(go.Spot.Left, false),
SpotButton(go.Spot.Right, false),
SpotButton(go.Spot.Bottom, false)
),
new go.Panel('Spot', { margin: new go.Margin(0, 0, 0, 2) })
.add(
AllSidesButton(true),
SpotButton(go.Spot.Top, true),
SpotButton(go.Spot.Left, true),
SpotButton(go.Spot.Right, true),
SpotButton(go.Spot.Bottom, true)
)
)
)
);
load();
}
// Show the diagram's model in JSON format
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 id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>
<p>
Double-click in the background to create a new node. Create groups by selecting nodes and
invoking Ctrl-G; Ctrl-Shift-G to ungroup a selected group. A selected node will have four orange
triangles that when clicked will automatically copy the node and link to it. Use the context
menu to change the shape, color, thickness, and dashed-ness.
</p>
<p>
Links can be drawn by dragging from the side of each node. A selected link can be reconnected by
dragging an end handle. Use the context menu to change the color, thickness, dashed-ness, and
which side the link should connect with. Press the F2 key to start editing the label of a
selected link.
</p>
<div id="buttons">
<button id="loadModel" onclick="load()">Load</button>
<button id="saveModel" onclick="save()">Save</button>
</div>
<textarea id="mySavedModel" style="width: 100%; height: 300px">
{ "class": "GraphLinksModel",
"pointsDigits": 0,
"nodeDataArray": [
{"text":"Find Problem","key":-9,"loc":"-20 -140","color":"#3358ff","fill":"white","figure":"Procedure","thickness":3},
{"text":"What do we want?","key":-10,"loc":"-110 -320","group":-16,"figure":"Ellipse","fill":"white"},
{"text":"What do our users want?","key":-11,"loc":"70 -320","group":-20,"figure":"Ellipse","fill":"#ffffff","color":"black"},
{"text":"Meetings","key":-12,"loc":"-110 -440","group":-16,"figure":"TriangleDown","fill":"#ffffff"},
{"text":"Reviews","key":-13,"loc":"70 -440","group":-20,"figure":"TriangleDown","fill":"#ffffff","color":"black"},
{"text":"Can we solve it?","key":-14,"loc":"190 -140","color":"#7d33ff","fill":"#ffffff","figure":"Diamond","size":"140 80","thickness":3},
{"text":"Design Prototype","key":-7,"loc":"337.191 -440","color":"black","fill":"#f2dfe0","group":-17,"figure":"Procedure","size":"140 80"},
{"text":"Does it work?","key":-8,"loc":"337.191 -300","color":"black","fill":"#f2dfe0","group":-17,"figure":"Diamond","size":"140 80"},
{"text":"Market/Sell\nSolution","key":-15,"loc":"470 -157.5","color":"#25ad23","fill":"#ffffff","group":-19,"figure":"Rectangle","size":"120 60"},
{"isGroup":true,"text":"Problem-Solving Process","key":-17,"loc":"365.40458203125 -370","fill":"#fcbbbd","thickness":3,"color":"#000000"},
{"text":"Implement \nSolution","key":-18,"loc":"470 -82.5","color":"#25ad23","fill":"#ffffff","group":-19,"figure":"Rectangle","size":"120 60"},
{"isGroup":true,"text":"Internal","key":-16,"loc":"-110 -380","fill":"#d5ebff","dash":null,"thickness":1,"group":-22},
{"isGroup":true,"text":"External","key":-20,"loc":"70 -380","fill":"#d5ebff","dash":null,"thickness":1,"group":-22},
{"isGroup":true,"text":"Distribute Solution","key":-19,"loc":"470 -120","fill":"#b3e6b3","thickness":2,"dash":[4,4]},
{"text":"Yes!","key":-21,"loc":"350 -120","size":"60 40","figure":"ManualOperation","fill":"#ffffff","color":"#25ad23","thickness":3},
{"isGroup":true,"text":"Sources","key":-22,"loc":"-20 -388.66328125","fill":"#a5d2fa","dash":[4,4],"color":"#3358ff"}
],
"linkDataArray": [
{"from":-12,"to":-10,"points":[-110,-410,-110,-400,-110,-380,-110,-380,-110,-360,-110,-350],"dash":null,"dir":1},
{"from":-13,"to":-11,"points":[70,-410,70,-400,70,-380,70,-380,70,-360,70,-350],"dash":null,"color":"#000000","dir":1},
{"from":-10,"to":-9,"points":[-110,-290,-110,-280,-110,-230,-40,-230,-40,-180,-40,-170],"dir":2,"dash":[4,4]},
{"from":-11,"to":-9,"points":[70,-290,70,-280,70,-230,0,-230,0,-180,0,-170],"dash":[4,4],"dir":2},
{"from":-9,"to":-14,"points":[40,-140,50,-140,80,-140,80,-140,110,-140,120,-140],"dir":1,"color":"#3358ff"},
{"from":-14,"to":-9,"points":[190,-100,190,-90,190,-72.39579391479492,-20,-72.39579391479492,-20,-100,-20,-110],"fromSpot":"BottomSide","toSpot":"BottomSide","text":"No","color":"#ff3333","thickness":2,"dir":1},
{"from":-14,"to":-7,"points":[190,-180,190,-190,190,-440,223.5955,-440,257.191,-440,267.191,-440],"fromSpot":"TopSide","toSpot":"LeftSide","color":"#7d33ff","dir":1},
{"from":-7,"to":-8,"points":[337.191,-400,337.191,-390,337.191,-370,337.191,-370,337.191,-350,337.191,-340],"dir":1},
{"from":-8,"to":-15,"points":[360.5243333333333,-260,360.5243333333333,-250,360.5243333333333,-157.5,380.26216666666664,-157.5,400,-157.5,410,-157.5],"toSpot":"LeftSide","dir":1},
{"from":-8,"to":-18,"points":[313.85766666666666,-260,313.85766666666666,-242,313.85766666666666,-82.5,356.92883333333333,-82.5,400,-82.5,410,-82.5],"toSpot":"LeftSide","fromSpot":"BottomSide","text":"","dir":1},
{"from":-8,"to":-7,"points":[407.191,-300,417.191,-300,443,-300,443,-440,417.191,-440,407.191,-440],"toSpot":"RightSide","fromSpot":"RightSide","text":"No\n","color":"#ff3333","dir":1},
{"from":-15,"to":-13,"points":[530,-157.5,540,-157.5,563.3333740234375,-157.5,563.3333740234375,-557.7291679382324,182.66668701171875,-557.7291679382324,182.66668701171875,-440,140,-440,130,-440],"toSpot":"RightSide","fromSpot":"RightSide","dash":[2,4],"dir":1},
{"from":-18,"to":-12,"points":[530,-82.5,540,-82.5,587.3333740234375,-82.5,587.3333740234375,-579.0624961853027,-238.66665649414062,-579.0624961853027,-238.66665649414062,-440,-180,-440,-170,-440],"fromSpot":"RightSide","toSpot":"LeftSide","dash":[2,4],"dir":1}
]}
</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>Geometry Path Strings</h4>
<p>
The <b>GoJS</b> <a href="../api/symbols/Geometry.html" target="api">Geometry</a> class controls the "shape" of a <a href="../api/symbols/Shape.html" target="api">Shape</a>,
whereas the <a href="../api/symbols/Shape.html#fill" target="api">Shape.fill</a> and <a href="../api/symbols/Shape.html#stroke" target="api">Shape.stroke</a> and other shape properties control the colors and appearance of the shape.
For common shape figures, there are predefined geometries that can be used by setting <a href="../api/symbols/Shape.html#figure" target="api">Shape.figure</a>.
However one can also define custom geometries.
</p>
<p>
One can construct any Geometry by allocating and initializing a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> of at least one <a href="../api/symbols/PathFigure.html" target="api">PathFigure</a> holding some <a href="../api/symbols/PathSegment.html" target="api">PathSegment</a>s.
But you may find that using the string representation of a Geometry is easier to write and save in a database.
Use the static method <a href="../api/symbols/Geometry.html#parse" target="api">Geometry.parse</a> or the <a href="../api/symbols/Shape.html#geometryString" target="api">Shape.geometryString</a> property to transform a geometry path string into a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> object.
</p>
<p>
More information can be found in the <a href="../intro/geometry.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#geometries">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>