UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

579 lines (540 loc) 24.7 kB
<!DOCTYPE html> <html> <head> <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-2016 by Northwoods Software Corporation. --> <meta charset="UTF-8"> <script src="go.js"></script> <link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" /> <!-- you don't need to use this --> <script src="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: 20 }), maxSelectionCount: 1, allowDrop: true, allowCopy: false, "SelectionDeleting": relinkOnDelete, // these two DiagramEvent listeners are "SelectionDeleted": deleteLoneNodes, // 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 = !myDiagram.isModified; var idx = document.title.indexOf("*"); if (myDiagram.isModified) { if (idx < 0) document.title += "*"; } else { if (idx >= 0) document.title = document.title.substr(0, idx); } }); 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, 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 // this Node cannot be removed }, 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 $(go.Shape, "StopSign", shapeStyle(), { fill: redBrush, width: 40, height: 40 }), $(go.TextBlock, "End") )); myDiagram.nodeTemplateMap.add("Action", $(go.Node, "Auto", 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(), $(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, "Auto", nodeStyle(), $(go.Shape, "Diamond", shapeStyle(), { fill: lightBrush }), $(go.TextBlock, { margin: 5, editable: true }, new go.Binding("text", "text").makeTwoWay()) )); // 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 pallette 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, // the link label, normally not visible { visible: false, name: "LABEL", segmentIndex: 2, segmentFraction: .5, segmentOffset: new go.Point(0, -10) }, $(go.TextBlock, "False", { textAlign: "center", font: "10pt helvetica, arial, sans-serif", stroke: "black", margin: 2, editable: true }) ) ); myDiagram.addDiagramListener("ExternalObjectsDropped", function (e) { var newnode = myDiagram.selection.first(); if (newnode.linksConnected.count === 0) { // when the selection is dropped but not hooked up to the rest of the graph, delete it myDiagram.commandHandler.deleteSelection(); } }); // initialize Palette var myPalette = $(go.Palette, "myPaletteDiv", // refers to its DIV HTML element by id { 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", "text")) )); myPalette.nodeTemplateMap.add("Effect", $(go.Node, "Auto", $(go.Shape, "Rectangle", { fill: blueBrush, strokeWidth: 2 }), $(go.TextBlock, { margin: 5 }, new go.Binding("text", "text")) )); myPalette.nodeTemplateMap.add("Output", $(go.Node, "Auto", $(go.Shape, "RoundedRectangle", { fill: pinkBrush, strokeWidth: 2 }), $(go.TextBlock, { margin: 5 }, new go.Binding("text", "text")) )); myPalette.nodeTemplateMap.add("Condition", $(go.Node, "Auto", $(go.Shape, "Diamond", { fill: lightBrush, strokeWidth: 2 }), $(go.TextBlock, { margin: 5 }, new go.Binding("text", "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 myOverview = $(go.Overview, "myOverviewDiv", { observed: myDiagram, contentAlignment: go.Spot.Center }); load(); // read model from textarea and initialize myDiagram } function dropOntoNode(e, obj) { var diagram = e.diagram; var oldnode = obj.part; var newnode = diagram.selection.first(); if (!(newnode instanceof go.Node)) return; if (oldnode.category === "Start") { diagram.currentTool.doCancel(); return; } var tool = diagram.toolManager.linkingTool; if (newnode.category === "Effect" || newnode.category === "Action" || newnode.category === "Condition") { // Take all links into oldnode, and relink to newnode, then link newnode to oldnode var linksIn = new go.Set(go.Link); linksIn.addAll(oldnode.findLinksInto()); var it = linksIn.iterator; while (it.next()) { var link = it.value; var fromnode = link.fromNode; var fromport = link.fromPort; diagram.remove(link); tool.insertLink(fromnode, fromport, newnode, newnode.port); } if (newnode.category === "Condition") { tool.insertLink(newnode, newnode.port, oldnode, oldnode.port); //??? tool.insertLink(newnode, newnode.port, oldnode, oldnode.port); } else { tool.insertLink(newnode, newnode.port, oldnode, oldnode.port); } } 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") { tool.insertLink(prev, prev.port, newnode, newnode.port); //??? } else { tool.insertLink(prev, prev.port, newnode, newnode.port); } } } } function dropOntoLink(e, obj) { var diagram = e.diagram; var tool = diagram.toolManager.linkingTool; var newnode = diagram.selection.first(); var link = obj.part; var fromnode = link.fromNode; var fromport = link.fromPort; var tonode = link.toNode; var toport = link.toPort; if (newnode.category === "Effect" || newnode.category === "Action" || newnode.category === "Condition") { // Delete the existing link, then add links to and from the new node diagram.remove(link); tool.insertLink(fromnode, fromport, newnode, newnode.port); tool.insertLink(newnode, newnode.port, tonode, toport); } else if (newnode.category === "Output") { // Add a new link to the new node tool.insertLink(fromnode, fromport, newnode, newnode.port); } } // Add a "false" label to the left link from all conditional nodes. function labelConditionals() { var nodes = myDiagram.nodes.iterator; while (nodes.next()) if (nodes.value.category === "Condition") { var linksOut = nodes.value.findLinksOutOf(); var right = false; while (linksOut.next()) { if (linksOut.value.fromSpot.x === 1) right = true; } linksOut.reset(); while (linksOut.next()) { if (linksOut.value.fromSpot.x !== 1 && linksOut.value.fromSpot.x !== 0) { if (right) linksOut.value.fromSpot = go.Spot.Left; else linksOut.value.fromSpot = go.Spot.Right; } } linksOut.reset(); while (linksOut.next()) if (linksOut.value.fromSpot.x === 0) { linksOut.value.findObject("LABEL").visible = true; break; } } } // Draw links when a new node is added according to what type of node it is and where it's added. function addNode(node) { var lnks = node.linksConnected; if (!lnks.next()) { myDiagram.removeNode(node); return; } var par; var chl; if (lnks.value.toNode !== node) chl = lnks.value.toNode; else { lnks.next(); chl = lnks.value.toNode; lnks.reset(); lnks.next(); } // if a Node was dropped on another Node, link it to the parent(s) of the Node it was dropped on // and remove Links between the Node it was dropped on and its parent(s) if (lnks.count === 1 && node.category !== "Output" ) { chl = lnks.value.toNode; var chlLinks = chl.findLinksInto(); var linksIn = new go.List(Link); while (chlLinks.next()) { if (chlLinks.value.fromNode !== node ) { linksIn.add(chlLinks.value); } } var allLinks = myDiagram.links.iterator; var lnkToRemove = new go.List(Link); var linksInIt = linksIn.iterator; while (linksInIt.next()) { myDiagram.model.addLinkData({ from: linksInIt.value.data.from, to: node.data.key, fromSpot: linksInIt.value.fromSpot }); lnkToRemove.add(linksInIt.value); } var lnkToRemoveIt = lnkToRemove.iterator; while (lnkToRemoveIt.next()) myDiagram.model.removeLinkData(lnkToRemoveIt.value.data); } // if the Node is an "output", it creates a new Link from the fromNode of the Link it is added to instead of splicing // if added to a Node, it forms a Link from a parent of that Node // if it is a "condition", it creates two Links to its child Node, one with a "false" label if (node.category === "Output") { lnks.reset(); while (lnks.next()) { if (lnks.value.toNode === node) par = lnks.value.fromNode; else chl = lnks.value.toNode; } if (par === undefined) { var chlLinks = chl.findLinksInto(); while (chlLinks.next()) { if (chlLinks.value.fromNode !== node) par = chlLinks.value.fromNode; } myDiagram.model.addLinkData({ from: par.data.key, to: node.data.key }); } else { if (par.category === "Condition") { if (conditionHasLabel(par)) { myDiagram.model.addLinkData({ from: par.data.key, to: chl.data.key, fromSpot: go.Spot.Right }); } else { myDiagram.model.addLinkData({ from: par.data.key, to: chl.data.key, fromSpot: go.Spot.Left }); } } else myDiagram.model.addLinkData({ from: par.data.key, to: chl.data.key }); } var lnksToRemove = new go.List(Link); var allLinks = myDiagram.links.iterator; while (allLinks.next()) { if (allLinks.value.fromNode === node) lnksToRemove.add(allLinks.value); } var lnkRemoveIt = lnksToRemove.iterator; while (lnkRemoveIt.next()) myDiagram.model.removeLinkData(lnkRemoveIt.value.data); } else if (node.category === "Condition") { lnks.reset(); var labeled = false; while (lnks.next()) { if (lnks.value.toNode === node) par = lnks.value.fromNode; else if (!labeled) { chl = lnks.value.toNode; lnks.value.findObject("LABEL").visible = true; lnks.value.fromSpot =go.Spot.Left; labeled = true; } } myDiagram.model.addLinkData({ from: node.data.key, to: chl.data.key, fromSpot: go.Spot.Right }); node.child = chl; chl.childOf = node; } // if this Node has one or more "condition" Nodes as parents, make one Link from each side and add "false" labels as needed labelConditionals(); } // Draw links between the parent and children nodes of a node being deleted. function relinkOnDelete(e) { var node = e.subject.first(); var lnks = node.linksConnected.iterator; var linksTo = new go.List(Node); var lnksFrom = new go.List(Node); while (lnks.next()) { if (lnks.value.toNode === node) { linksTo.add(lnks.value.fromNode); } else lnksFrom.add(lnks.value.toNode); } if (lnksFrom.count === 0) return; var par = linksTo.first(); var chld; var endPar = endParent(node); if (endPar !== myDiagram.findPartForKey("Start")) chld = endPar; else chld = lnksFrom.first(); if (node.category === "Condition") { chld = findConvergence(node); } linksToIt = linksTo.iterator; myDiagram.startTransaction("relink"); while (linksToIt.next()) { if (linksToIt.value.category === "Condition") if (conditionHasLabel(linksToIt.value, node)) { myDiagram.model.addLinkData({ from: linksToIt.value.data.key, to: chld.data.key, fromSpot: go.Spot.Right }) } else { myDiagram.model.addLinkData({ from: linksToIt.value.data.key, to: chld.data.key, fromSpot: go.Spot.Left }); } else myDiagram.model.addLinkData({ from: linksToIt.value.data.key, to: chld.data.key }); } labelConditionals(); myDiagram.commitTransaction("relink"); } // Delete Nodes if all Nodes linking to them have been deleted. function deleteLoneNodes(e) { var nodes = myDiagram.nodes.iterator; var nodesToDelete = new go.List(Node); while (nodes.next()) { if (nodes.value.findLinksInto().count === 0) { var cat = nodes.value.category; if (nodes.value.category !== "Start") nodesToDelete.add(nodes.value); } } nDelIt = nodesToDelete.iterator; while (nDelIt.next()) myDiagram.remove(nDelIt.value); if (nodesToDelete.count !== 0) deleteLoneNodes(e); } // Find the parents of the "end" node to ensure that it remains linked to "start". function endParent(node) { var end = myDiagram.findPartForKey("End"); var itE = end.linksConnected itE.next(); var par = itE.value.fromNode; var last = end; while (par !== null && par !== node) { // if the function has reached the top of the flowchart or the node that is being deleted, return the last Node last = par; var it = par.linksConnected; if (it.count === 1) break; if (par !== myDiagram.findPartForKey("Start")) { it.next(); while ((it.key === -1 || it.value.fromNode === par) && it.value !== node) { it.next(); } par = it.value.fromNode; } else par = null; } return last; } // Determine if a conditional Node has a route labeled "false", accounting for a Link to a child Node being removed. function conditionHasLabel(node, child) { if (node.category !== "Condition") return false; if (child === undefined) child = null; var links = node.findLinksOutOf(); while (links.next()) { if (links.value.findObject("LABEL").visible && links.value.toNode !== child) return true; } return false; } // Return the Node at which a Link from another path joins this path. function lineEnd(link) { var nextLinks = link.toNode.findLinksOutOf(); var nextNode = link.toNode; if (link.toNode.findLinksInto().count > 1) return link.toNode; if (!nextLinks.next()) return null; else if (nextLinks.count === 1) return lineEnd(nextLinks.value); else while (nextNode.findLinksOutOf().count > 1) { var nextNode = findConvergence(nextNode); if (nextNode === null) return null; } if (hasOutsideLinks(nextNode, link.toNode)) return nextNode; nextLinks = nextNode.findLinksOutOf(); if (nextLinks.next()) return lineEnd(nextLinks.value); return null; } // Return the node at which all links coming out of a node converge. function findConvergence(node) { var links = node.findLinksOutOf(); if (!links.next()) return null; if (links.count === 1) return links.value.toNode; links.reset(); while (links.next()) { var end = lineEnd(links.value); if (end !== null) return end; } return null; } // Determine if the node has any link paths to it that do not pass through the parent. function hasOutsideLinks(node, parent) { if (node === parent) return false; if (node === myDiagram.findNodeForKey("Start")) return true; // if the function reaches the start node and has not found the parent, this path does not come from the parent var linksIn = node.findLinksInto(); while (linksIn.next()) { if (hasOutsideLinks(linksIn.value.fromNode, parent)) return true; } return false; } // 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); labelConditionals(); } </script> </head> <body onload="init()"> <div id="sample"> <div style="width:100%; white-space:nowrap;"> <span style="display: inline-block; vertical-align: top; padding: 2px; width:100px"> <div id="myPaletteDiv" style="border: solid 1px black; height: 600px"></div> <div id="myOverviewDiv" style="border: solid 1px gray; height: 100px"></div> </span> <span style="display: inline-block; vertical-align: top; padding: 2px; width:85%"> <div id="myDiagramDiv" style="border: solid 1px black; height: 700px"></div> </span> </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", "nodeDataArray": [ {"key":"Start", "category":"Start", "loc":"0 0"}, {"key":"End", "category":"End", "loc":"0 80"} ], "linkDataArray": [ {"from":"Start", "to":"End"} ] } </textarea> </div> </body> </html>