UNPKG

gojs

Version:

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

385 lines (356 loc) 17.3 kB
<!DOCTYPE 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="Nodes with selectable, movable, copyable, and deletable ports."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2021 by Northwoods Software Corporation. --> <title>Movable, Copyable, Deletable Ports</title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary"> <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2"> <div class="md:pl-4"> <a class="text-white hover:text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../"> <h1 class="mb-0 p-1 ">GoJS</h1> </a> </div> <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path> <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> <div id="topnavList" class="hidden lg:text-base sm:block items-center w-auto mt-0 text-white p-0 z-20"> <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0"> <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li> </ul> </div> </div> <hr class="border-b border-gray-600 opacity-50 my-0 py-0" /> </nav> <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="../release/go.js"></script> <div class="p-4 w-full"> <script id="code"> function init() { var $ = go.GraphObject.make; myDiagram = $(go.Diagram, "myDiagramDiv", { "undoManager.isEnabled": true, // don't allow links within a group "linkingTool.linkValidation": differentGroups, "relinkingTool.linkValidation": differentGroups, mouseDrop: function(e) { // when the selection is dropped in the diagram's background, // and it includes any "port"s, cancel the drop if (myDiagram.selection.any(selectionIncludesPorts)) { myDiagram.currentTool.doCancel(); } } }); function differentGroups(fromnode, fromport, tonode, toport) { return fromnode.containingGroup !== tonode.containingGroup; } function selectionIncludesPorts(n) { return n.containingGroup !== null && !myDiagram.selection.has(n.containingGroup); } var UnselectedBrush = "lightgray"; // item appearance, if not "selected" var SelectedBrush = "dodgerblue"; // item appearance, if "selected" myDiagram.nodeTemplate = $(go.Node, "Auto", { selectionAdorned: false }, { mouseDrop: function(e, n) { // when the selection is entirely ports and is dropped onto a Group, transfer membership if (n.containingGroup !== null && myDiagram.selection.all(selectionIncludesPorts)) { myDiagram.selection.each(function(p) { p.containingGroup = n.containingGroup; }); } else { myDiagram.currentTool.doCancel(); } } }, $(go.Shape, { name: "SHAPE", fill: UnselectedBrush, stroke: "gray", geometryString: "F1 m 0,0 l 5,0 1,4 -1,4 -5,0 1,-4 -1,-4 z", spot1: new go.Spot(0, 0, 5, 1), // keep the text inside the shape spot2: new go.Spot(1, 1, -5, 0), // some port-related properties portId: "", toSpot: go.Spot.Left, toLinkable: false, fromSpot: go.Spot.Right, fromLinkable: false, cursor: "pointer" }, new go.Binding("fill", "isSelected", function(s) { return s ? SelectedBrush : UnselectedBrush; }).ofObject(), new go.Binding("toLinkable", "_in"), new go.Binding("fromLinkable", "_in", function(b) { return !b; })), $(go.TextBlock, new go.Binding("text", "name")) ); myDiagram.groupTemplate = $(go.Group, "Auto", { selectionAdorned: false, locationSpot: go.Spot.Center, locationObjectName: "ICON" }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), { mouseDrop: function(e, g) { // when the selection is entirely ports and is dropped onto a Group, transfer membership if (myDiagram.selection.all(selectionIncludesPorts)) { myDiagram.selection.each(function(p) { p.containingGroup = g; }); } else { myDiagram.currentTool.doCancel(); } }, layout: new InputOutputGroupLayout() }, $(go.Shape, "RoundedRectangle", { stroke: "gray", strokeWidth: 2, fill: "transparent" }, new go.Binding("stroke", "isSelected", function(b) { return b ? SelectedBrush : UnselectedBrush; }).ofObject()), $(go.Panel, "Vertical", { margin: 6 }, $(go.TextBlock, new go.Binding("text", "name"), { alignment: go.Spot.Left }), $(go.Panel, "Spot", { name: "ICON", height: 60 }, // an initial height; size will be set by InputOutputGroupLayout $(go.Shape, { fill: null, stroke: null, stretch: go.GraphObject.Fill }), $(go.Picture, "images/60x90.png", { width: 30, height: 45 }) ) ) ); myDiagram.linkTemplate = $(go.Link, { routing: go.Link.Orthogonal, corner: 10, toShortLength: -3 }, { relinkableFrom: true, relinkableTo: true }, $(go.Shape, { stroke: "gray", strokeWidth: 2.5 }) ); load(); // initialize myDiagram's model from the TextArea } function findPortNode(g, name, input) { for (var it = g.memberParts; it.next();) { var n = it.value; if (!(n instanceof go.Node)) continue; if (n.data.name === name && n.data._in === input) return n; } return null; } // Transform the given data to the data structures needed internally. // For each data object in the "ins" Array of the node data, add a "port" Node to the Group. // For each data object in the "outs" Array, add a "port" Node to the Group. // For each link data, convert the "from" and "fromPort" information to the actual "port" Node, // and then the same for "to" and "toPort". // The internal model uses property names starting with "_" to avoid having Model.toJson() write them out. function setupDiagram(nodes, links) { var model = new go.GraphLinksModel(); model.linkFromKeyProperty = "_f"; model.linkToKeyProperty = "_t"; model.nodeIsGroupProperty = "_isg"; model.nodeGroupKeyProperty = "_g"; // first create all of the nodes, implemented as Groups for (var i = 0; i < nodes.length; i++) { var nodedata = nodes[i]; nodedata._isg = true; } model.addNodeDataCollection(nodes); // now each node data will have a unique key, if not already specified // then create all of the ports, implemented as Nodes that are members of those Groups for (var i = 0; i < nodes.length; i++) { var nodedata = nodes[i]; if (Array.isArray(nodedata.ins)) { for (var j = 0; j < nodedata.ins.length; j++) { var portdata = nodedata.ins[j]; portdata._in = true; portdata._g = nodedata.key; } model.addNodeDataCollection(nodedata.ins); nodedata.ins = undefined; } if (Array.isArray(nodedata.outs)) { for (var j = 0; j < nodedata.outs.length; j++) { var portdata = nodedata.outs[j]; portdata._in = false; portdata._g = nodedata.key; } model.addNodeDataCollection(nodedata.outs); nodedata.outs = undefined; } } myDiagram.model = model; // now Groups and Nodes exist, so can find the Node corresponding to a node's port // finally process all of the links, to account for ports actually being member nodes for (var i = 0; i < links.length; i++) { var linkdata = links[i]; var fromNode = myDiagram.findNodeForKey(linkdata.from); var toNode = myDiagram.findNodeForKey(linkdata.to); if (fromNode !== null && toNode !== null) { // look in the Group for a "port" Node with the right name and directionality var fromPortNode = findPortNode(fromNode, linkdata.fromPort, false); var toPortNode = findPortNode(toNode, linkdata.toPort, true); if (fromPortNode !== null && toPortNode !== null) { linkdata._f = fromPortNode.data.key; linkdata._t = toPortNode.data.key; linkdata.from = linkdata.fromPort = linkdata.to = linkdata.toPort = undefined; } } } model.addLinkDataCollection(links); } function save() { // can't just call myDiagram.model.toJson() -- need to transform to external format var m = new go.GraphLinksModel(); m.linkFromPortIdProperty = "fromPort"; m.linkToPortIdProperty = "toPort"; var arr = myDiagram.model.nodeDataArray; myDiagram.nodes.each(function(g) { if (g instanceof go.Group) { g.data.ins = undefined; g.data.outs = undefined; m.addNodeData(g.data); } }); myDiagram.nodes.each(function(n) { if (!(n instanceof go.Group)) { var gd = n.containingGroup.data; var a = n.data._in ? gd.ins : gd.outs; if (!a) { a = []; if (n.data._in) gd.ins = a; else gd.outs = a; } a.push(n.data); } }); myDiagram.links.each(function(l) { l.data.from = l.fromNode.containingGroup.data.key; l.data.fromPort = l.fromNode.data.name; l.data.to = l.toNode.containingGroup.data.key; l.data.toPort = l.toNode.data.name; m.addLinkData(l.data); }); document.getElementById("mySavedModel").value = m.toJson(); myDiagram.isModified = false; } function load() { var m = go.Model.fromJson(document.getElementById("mySavedModel").value); setupDiagram(m.nodeDataArray, m.linkDataArray); } // The Group.layout, for arranging the "port" Nodes within the Group function InputOutputGroupLayout() { go.Layout.call(this); } go.Diagram.inherit(InputOutputGroupLayout, go.Layout); InputOutputGroupLayout.prototype.doLayout = function(coll) { coll = this.collectParts(coll); var portSpacing = 2; var iconAreaWidth = 60; // compute the counts and areas of the inputs and the outputs var left = 0; var leftwidth = 0; // max var leftheight = 0; // total var right = 0; var rightwidth = 0; // max var rightheight = 0; // total coll.each(function(n) { if (n instanceof go.Link) return; // ignore Links if (n.data._in) { left++; leftwidth = Math.max(leftwidth, n.actualBounds.width); leftheight += n.actualBounds.height; } else { right++; rightwidth = Math.max(rightwidth, n.actualBounds.width); rightheight += n.actualBounds.height; } }); if (left > 0) leftheight += portSpacing * (left - 1); if (right > 0) rightheight += portSpacing * (right - 1); var loc = new go.Point(0, 0); if (this.group !== null && this.group.location.isReal()) loc = this.group.location; // first lay out the left side, the inputs var y = loc.y - leftheight / 2; coll.each(function(n) { if (n instanceof go.Link) return; // ignore Links if (!n.data._in) return; // ignore outputs n.position = new go.Point(loc.x - iconAreaWidth / 2 - leftwidth, y); y += n.actualBounds.height + portSpacing; }); // now the right side, the outputs y = loc.y - rightheight / 2; coll.each(function(n) { if (n instanceof go.Link) return; // ignore Links if (n.data._in) return; // ignore inputs n.position = new go.Point(loc.x + iconAreaWidth / 2 + rightwidth - n.actualBounds.width, y); y += n.actualBounds.height + portSpacing; }); // then position the group and size its icon area if (this.group !== null) { // position the group so that its ICON is in the middle, between the "ports" this.group.location = loc; // size the ICON so that it's wide enough to overlap the "ports" and tall enough to hold all of the "ports" var icon = this.group.findObject("ICON"); if (icon !== null) icon.desiredSize = new go.Size(iconAreaWidth + leftwidth / 2 + rightwidth / 2, Math.max(leftheight, rightheight) + 10); } }; window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div> <p> To allow ports to be selected, dragged, copied, and deleted, they are implemented as Nodes. That means the nodes have to be implemented as Groups. The user can delete selected ports. The user cannot drop a port onto the background, but only onto a node. </p> <p> There is a custom Layout used by such Group nodes, <code>InputOutputGroupLayout</code>, to line up the input ports on the left side and the output ports on the right side. </p> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> The transformed model data (not the actual myDiagram.model): <textarea id="mySavedModel" style="width:100%;height:300px"> { "class": "go.GraphLinksModel", "linkFromPortIdProperty": "fromPort", "linkToPortIdProperty": "toPort", "nodeDataArray": [ {"key":1, "name":"Server", "ins":[ {"name":"s1", "key":-3},{"name":"s2", "key":-4} ], "outs":[ {"name":"o1", "key":-5} ], "loc":"-80 -80"}, {"key":2, "name":"Other", "ins":[ {"name":"s1", "key":-6},{"name":"s2", "key":-7} ], "outs":[ {"name":"o1", "key":-8} ], "loc":"80 80"} ], "linkDataArray": [ {"from":1, "fromPort":"o1", "to":2, "toPort":"s2"} ] } </textarea> </div> </div> <!-- * * * * * * * * * * * * * --> <!-- End of GoJS sample code --> </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>