gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
530 lines (486 loc) • 20.2 kB
HTML
<html>
<head>
<meta charset="UTF-8">
<title>Flowgrammer</title>
<meta name="description" content="An editor for a flowchart-like diagram with a restricted syntax -- add nodes by dropping them onto existing nodes or links." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<style>
/* Use a Flexbox to make the Palette/Overview/Diagram responsive and size things relatively */
#myFlexDiv {
display: -webkit-flex;
display: flex;
width: 100%;
height: 700px;
}
#myPODiv {
display: -webkit-flex;
display: flex;
}
@media (min-width: 768px) {
#myFlexDiv {
flex-flow: row;
}
#myPODiv {
width: 105px;
height: 100%;
margin-right: 10px;
flex-flow: column;
}
#myPaletteDiv {
height: 75%;
}
#myOverviewDiv {
margin-top: 3px;
flex: 1;
}
#myDiagramDiv {
flex: 1;
}
}
@media (max-width: 767px) {
#myFlexDiv {
flex-flow: column;
align-items: center;
}
#myPODiv {
width: 90%;
height: 105px;
margin-bottom: 10px;
flex-flow: row;
}
#myPaletteDiv {
width: 75%;
}
#myOverviewDiv {
margin-left: 3px;
flex: 1;
}
#myDiagramDiv {
width: 90%;
flex: 1;
}
}
</style>
<script src="../release/go.js"></script>
<script src="../extensions/Figures.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
initialContentAlignment: go.Spot.Top,
// make the layout a vertical Layered Digraph- links "flow" downward
layout:
$(go.LayeredDigraphLayout,
{ direction: 90, layerSpacing: 6, columnSpacing: 6, setsPortSpots: false }),
maxSelectionCount: 1,
allowCopy: false,
"textEditingTool.starting": go.TextEditingTool.SingleClick,
"SelectionDeleting": function(e) {
// assume maxSelectionCount === 1
exciseNode(e.subject.first()); // defined below
},
"SelectionDeleted": function(e) {
deleteDisconnectedNodes(e.diagram); // defined below
},
"undoManager.isEnabled": true
});
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !e.diagram.isModified;
var idx = document.title.indexOf("*");
if (e.diagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
// Parts dragged in from the Palette will be partly translucent
myDiagram.findLayer("Tool").opacity = 0.5;
// Define a gradient brush for each Node type, shared by the Diagram and Palette
var greenBrush = $(go.Brush, "Linear", { 0: "rgb(200,255,200)", .67: "rgb(15,160,15)" });
var redBrush = $(go.Brush, "Linear", { 0: "rgb(255,240,240)", .67: "rgb(255,0,0)" });
var blueBrush = $(go.Brush, "Linear", { 0: "rgb(250,250,255)", .67: "rgb(90,125,200)" });
var yellowBrush = $(go.Brush, "Linear", { 0: "rgb(255,255,240)", .67: "rgb(190,200,10)" });
var pinkBrush = $(go.Brush, "Linear", { 0: "rgb(255,250,250)", .67: "rgb(255,180,200)" });
var lightBrush = $(go.Brush, "Linear", { 0: "rgb(240,240,250)", .67: "rgb(150,200,250)" });
// Define common properties and bindings for most kinds of nodes
function nodeStyle() {
return [new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{
locationSpot: go.Spot.Center,
toSpot: go.Spot.Top,
fromSpot: go.Spot.NotTopSide, // port properties on the node
portSpreading: go.Node.SpreadingNone,
layoutConditions: go.Part.LayoutAdded | go.Part.LayoutRemoved,
// If a node from the pallette is dragged over this node, its outline will turn green
mouseDragEnter: function(e, node) { node.isHighlighted = true; },
mouseDragLeave: function(e, node) { node.isHighlighted = false; },
// A node dropped onto this will draw a link from itself to this node
mouseDrop: dropOntoNode
}];
}
function shapeStyle() {
return [
{ stroke: "rgb(63,63,63)", strokeWidth: 2 },
new go.Binding("stroke", "isHighlighted", function(h) { return h ? "chartreuse" : "rgb(63,63,63)"; }).ofObject(),
new go.Binding("strokeWidth", "isHighlighted", function(h) { return h ? 4 : 2; }).ofObject()
];
}
// Define Node templates for various categories of nodes
myDiagram.nodeTemplateMap.add("Start",
// the name of the Node category
$(go.Node, "Auto",
{
locationSpot: go.Spot.Center,
deletable: false // do not allow this node to be removed by the user
},
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, "Ellipse",
{
fill: greenBrush,
strokeWidth: 2,
stroke: "green",
width: 40,
height: 40
}),
$(go.TextBlock, "Start")
));
myDiagram.nodeTemplateMap.add("End",
$(go.Node, "Auto", nodeStyle(), // use common properties and bindings
{
deletable: false, // do not allow this node to be removed by the user
toSpot: go.Spot.NotBottomSide // port properties on the node
},
$(go.Shape, "StopSign", shapeStyle(),
{ fill: redBrush, width: 40, height: 40 }),
$(go.TextBlock, "End")
));
myDiagram.nodeTemplateMap.add("Action",
$(go.Node, "Auto", nodeStyle(),
{ fromSpot: go.Spot.Bottom }, // override fromSpot of nodeStyle()
$(go.Shape, "Rectangle", shapeStyle(),
{ fill: yellowBrush }),
$(go.TextBlock,
{ margin: 5, editable: true },
// user can edit node text by clicking on it
new go.Binding("text", "text").makeTwoWay())
));
myDiagram.nodeTemplateMap.add("Effect",
$(go.Node, "Auto", nodeStyle(),
{ fromSpot: go.Spot.Bottom }, // override fromSpot of nodeStyle()
$(go.Shape, "Rectangle", shapeStyle(),
{ fill: blueBrush }),
$(go.TextBlock,
{ margin: 5, editable: true },
new go.Binding("text", "text").makeTwoWay())
));
myDiagram.nodeTemplateMap.add("Output",
$(go.Node, "Auto", nodeStyle(),
$(go.Shape, "RoundedRectangle", shapeStyle(),
{ fill: pinkBrush }),
$(go.TextBlock,
{ margin: 5, editable: true },
new go.Binding("text", "text").makeTwoWay())
));
myDiagram.nodeTemplateMap.add("Condition",
$(go.Node, "Spot", nodeStyle(),
$(go.Panel, "Auto",
$(go.Shape, "Diamond", shapeStyle(),
{ fill: lightBrush }),
$(go.TextBlock,
{ margin: 5, editable: true },
new go.Binding("text", "text").makeTwoWay())
),
$(go.Shape, "Circle",
{
portId: "Left", fromSpot: go.Spot.Left,
alignment: go.Spot.Left, alignmentFocus: go.Spot.Left,
stroke: null, fill: null, width: 1, height: 1
}),
$(go.Shape, "Circle",
{
portId: "Right", fromSpot: go.Spot.Right,
alignment: go.Spot.Right, alignmentFocus: go.Spot.Right,
stroke: null, fill: null, width: 1, height: 1
})
));
// Define the link template
myDiagram.linkTemplate =
$(go.Link,
{
routing: go.Link.AvoidsNodes,
curve: go.Link.JumpOver,
corner: 5,
toShortLength: 4,
selectable: false,
layoutConditions: go.Part.LayoutAdded | go.Part.LayoutRemoved,
// links cannot be selected, so they cannot be deleted
// If a node from the Palette is dragged over this node, its outline will turn green
mouseDragEnter: function(e, link) { link.isHighlighted = true; },
mouseDragLeave: function(e, link) { link.isHighlighted = false; },
// if a node from the Palette is dropped on a link, the link is replaced by links to and from the new node
mouseDrop: dropOntoLink
},
$(go.Shape, shapeStyle()),
$(go.Shape,
{ toArrow: "standard", stroke: null, fill: "black" }),
$(go.Panel, // link label for conditionals, normally not visible
{ visible: false, name: "LABEL", segmentIndex: 1, segmentFraction: 0.5 },
new go.Binding("visible", "", function(link) { return link.fromNode.category === "Condition" && !!link.data.text; }).ofObject(),
new go.Binding("segmentOffset", "side", function(s) { return s === "Left" ? new go.Point(0, 14) : new go.Point(0, -14); }),
$(go.TextBlock,
{
textAlign: "center",
font: "10pt sans-serif",
margin: 2,
editable: true
},
new go.Binding("text").makeTwoWay())
)
);
myDiagram.addDiagramListener("ExternalObjectsDropped", function(e) {
var newnode = e.diagram.selection.first();
if (newnode.linksConnected.count === 0) {
// when the selection is dropped but not hooked up to the rest of the graph, delete it
e.diagram.commandHandler.deleteSelection();
}
});
// initialize Palette
var myPalette =
$(go.Palette, "myPaletteDiv", // refers to its DIV HTML element by id
{
layout: $(go.GridLayout),
maxSelectionCount: 1
});
// define simpler templates for the Palette than in the main Diagram
myPalette.nodeTemplateMap.add("Action",
$(go.Node, "Auto",
$(go.Shape, "Rectangle",
{ fill: yellowBrush, strokeWidth: 2 }),
$(go.TextBlock,
{ margin: 5 },
new go.Binding("text"))
));
myPalette.nodeTemplateMap.add("Effect",
$(go.Node, "Auto",
$(go.Shape, "Rectangle",
{ fill: blueBrush, strokeWidth: 2 }),
$(go.TextBlock,
{ margin: 5 },
new go.Binding("text"))
));
myPalette.nodeTemplateMap.add("Output",
$(go.Node, "Auto",
$(go.Shape, "RoundedRectangle",
{ fill: pinkBrush, strokeWidth: 2 }),
$(go.TextBlock,
{ margin: 5 },
new go.Binding("text"))
));
myPalette.nodeTemplateMap.add("Condition",
$(go.Node, "Auto",
$(go.Shape, "Diamond",
{ fill: lightBrush, strokeWidth: 2 }),
$(go.TextBlock,
{ margin: 5 },
new go.Binding("text"))
));
// add node data to the palette
myPalette.model.nodeDataArray = [
{ key: "if1", category: "Condition", text: "if1" },
{ key: "action1", category: "Action", text: "action1" },
{ key: "action2", category: "Action", text: "action2" },
{ key: "action3", category: "Action", text: "action3" },
{ key: "effect1", category: "Effect", text: "effect1" },
{ key: "effect2", category: "Effect", text: "effect2" },
{ key: "effect3", category: "Effect", text: "effect3" },
{ key: "output1", category: "Output", text: "output1" },
{ key: "output2", category: "Output", text: "output2" }
];
// initialize Overview
var myOverview =
$(go.Overview, "myOverviewDiv",
{
observed: myDiagram,
contentAlignment: go.Spot.Center
});
load(); // read model from textarea and initialize myDiagram
}
// Graph manipulation functions, to maintain the syntax of the diagram
function dropOntoNode(e, obj) {
var diagram = e.diagram;
var oldnode = obj.part;
if (oldnode.category === "Start") {
diagram.currentTool.doCancel();
return;
}
var newnode = diagram.selection.first();
if (!(newnode instanceof go.Node)) return;
if (newnode.linksConnected.count > 0) {
exciseNode(newnode);
}
if (newnode.category === "Effect" || newnode.category === "Action" || newnode.category === "Condition") {
// Take all links into oldnode and relink to newnode
var it = new go.List().addAll(oldnode.findLinksInto()).iterator;
while (it.next()) {
var link = it.value;
link.toNode = newnode;
}
// Then link newnode to oldnode
if (newnode.category === "Condition") {
diagram.model.addLinkData({ from: newnode.data.key, to: oldnode.data.key, text: "true", side: "Left" });
diagram.model.addLinkData({ from: newnode.data.key, to: oldnode.data.key, text: "false", side: "Right" });
} else {
diagram.model.addLinkData({ from: newnode.data.key, to: oldnode.data.key });
}
} else if (newnode.category === "Output") {
// Find the previous node and add a link from it; no links coming out of an "Output"
var prev = oldnode.findTreeParentNode();
if (prev !== null) {
if (prev.category === "Condition") {
diagram.model.addLinkData({ from: prev.data.key, to: newnode.data.key });
} else {
diagram.model.addLinkData({ from: prev.data.key, to: newnode.data.key });
}
}
}
}
function dropOntoLink(e, obj) {
var diagram = e.diagram;
var newnode = diagram.selection.first();
if (!(newnode instanceof go.Node)) return;
if (newnode.linksConnected.count > 0) {
exciseNode(newnode);
}
var oldlink = obj.part;
var fromnode = oldlink.fromNode;
var tonode = oldlink.toNode;
if (newnode.category === "Effect" || newnode.category === "Action" || newnode.category === "Condition") {
// Reconnect the existing link to the new node
oldlink.toNode = newnode;
// Then add links from the new node to the old node
if (newnode.category === "Condition") {
diagram.model.addLinkData({ from: newnode.data.key, to: tonode.data.key, text: "true", side: "Left" });
diagram.model.addLinkData({ from: newnode.data.key, to: tonode.data.key, text: "false", side: "Right" });
} else {
diagram.model.addLinkData({ from: newnode.data.key, to: tonode.data.key });
}
} else if (newnode.category === "Output") {
// Add a new link to the new node
if (fromnode.category === "Condition") {
diagram.model.addLinkData({ from: fromnode.data.key, to: newnode.data.key });
} else {
diagram.model.addLinkData({ from: fromnode.data.key, to: newnode.data.key });
}
}
}
// Draw links between the parent and children nodes of a node being deleted.
function exciseNode(node) {
if (node === null) return;
var linksOut = node.findLinksOutOf();
var to = null;
if (linksOut.count > 1) {
to = findMerge(node);
} else if (linksOut.count === 1) { // if only one link out of the node to be deleted
to = linksOut.first().toNode;
}
if (to !== null) {
// now there is only a single output node to reconnect with
// for all links coming into the node to be deleted
var linksIn = new go.List().addAll(node.findLinksInto()).iterator;
while (linksIn.next()) {
var l = linksIn.value; // reconnect all links going into deleted node
l.toNode = to; // to that one destination node
}
} else {
node.diagram.removeParts(node.findLinksInto(), false);
}
}
// If there are multiple links going out of this node,
// return the node where the links merge back into one node, if any.
function findMerge(node) {
var it = node.findLinksOutOf();
if (it.count <= 1) return null;
node.diagram.nodes.each(function(n) { n._tag = 0; });
var i = 1;
while (it.next()) {
var n = walkDown(it.value.toNode, i);
if (n !== null) return n;
i++;
}
return null;
}
// Mark all downstream nodes, but return the first node found that was already marked
function walkDown(node, tag) {
var prev = node._tag;
if (prev !== 0 && prev !== tag) return node;
node._tag = tag;
if (prev === tag) return null;
var it = node.findNodesOutOf();
while (it.next()) {
var n = walkDown(it.value, tag);
if (n !== null) return n;
}
return null;
}
// Delete a Node if there are no Links coming into it, other than the "Start" Node.
function deleteDisconnectedNodes(diagram) {
var nodesToDelete = diagram.nodes.filter(function(n) { return n.category !== "Start" && n.findLinksInto().count === 0; });
if (nodesToDelete.count > 0) {
diagram.removeParts(nodesToDelete, false);
deleteDisconnectedNodes(diagram);
}
}
// 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);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myFlexDiv">
<div id="myPODiv">
<div id="myPaletteDiv" style="border: solid 1px black"></div>
<div id="myOverviewDiv" style="border: solid 1px black"></div>
</div>
<div id="myDiagramDiv" style="border: solid 1px black"></div>
</div>
<p>
The Flowgrammer sample demonstrates how one can build a flowchart with a constrained syntax.
You can drag and drop Nodes onto Links and Nodes in the diagram in order to insert them into the graph.
Nodes dropped onto the diagram's background are ignored.
Edit text by clicking on the text of selected nodes.
The "Start" and "End" nodes are not editable and are not deletable.
</p>
<div>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
</div>
<textarea id="mySavedModel" style="width:100%;height:200px">
{
"class": "go.GraphLinksModel",
"linkFromPortIdProperty": "side",
"nodeDataArray": [
{"key":"Start", "category":"Start", "loc":"0 0"},
{"key":"End", "category":"End", "loc":"0 80"}
],
"linkDataArray": [
{"from":"Start", "to":"End"}
]
}
</textarea>
</div>
</body>
</html>