520 lines
19 KiB
HTML
520 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<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." />
|
|
<!-- Copyright 1998-2017 by Northwoods Software Corporation. -->
|
|
<meta charset="UTF-8">
|
|
<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="../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,
|
|
allowDrop: true,
|
|
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> |