UNPKG

gojs

Version:

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

339 lines (312 loc) 14 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Movable, Copyable, Deletable Ports</title> <meta name="description" content="Nodes with selectable, movable, copyable, and deletable ports." /> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Copyright 1998-2020 by Northwoods Software Corporation. --> <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; 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); } }; </script> </head> <body onload="init()"> <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> </body> </html>