UNPKG

gojs

Version:

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

520 lines (485 loc) 22.4 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Dynamic Ports</title> <meta name="description" content="Nodes with varying lists of ports on each of four sides." /> <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; //for conciseness in defining node templates myDiagram = $(go.Diagram, "myDiagramDiv", //Diagram refers to its DIV HTML element by id { "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); } }); // To simplify this code we define a function for creating a context menu button: function makeButton(text, action, visiblePredicate) { return $("ContextMenuButton", $(go.TextBlock, text), { click: action }, // don't bother with binding GraphObject.visible if there's no predicate visiblePredicate ? new go.Binding("visible", "", function(o, e) { return o.diagram ? visiblePredicate(o, e) : false; }).ofObject() : {}); } var nodeMenu = // context menu for each Node $("ContextMenu", makeButton("Copy", function(e, obj) { e.diagram.commandHandler.copySelection(); }), makeButton("Delete", function(e, obj) { e.diagram.commandHandler.deleteSelection(); }), $(go.Shape, "LineH", { strokeWidth: 2, height: 1, stretch: go.GraphObject.Horizontal }), makeButton("Add top port", function(e, obj) { addPort("top"); }), makeButton("Add left port", function(e, obj) { addPort("left"); }), makeButton("Add right port", function(e, obj) { addPort("right"); }), makeButton("Add bottom port", function(e, obj) { addPort("bottom"); }) ); var portSize = new go.Size(8, 8); var portMenu = // context menu for each port $("ContextMenu", makeButton("Swap order", function(e, obj) { swapOrder(obj.part.adornedObject); }), makeButton("Remove port", // in the click event handler, the obj.part is the Adornment; // its adornedObject is the port function(e, obj) { removePort(obj.part.adornedObject); }), makeButton("Change color", function(e, obj) { changeColor(obj.part.adornedObject); }), makeButton("Remove side ports", function(e, obj) { removeAll(obj.part.adornedObject); }) ); // the node template // includes a panel on each side with an itemArray of panels containing ports myDiagram.nodeTemplate = $(go.Node, "Table", { locationObjectName: "BODY", locationSpot: go.Spot.Center, selectionObjectName: "BODY", contextMenu: nodeMenu }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), // the body $(go.Panel, "Auto", { row: 1, column: 1, name: "BODY", stretch: go.GraphObject.Fill }, $(go.Shape, "Rectangle", { fill: "#dbf6cb", stroke: null, strokeWidth: 0, minSize: new go.Size(60, 60) }), $(go.TextBlock, { margin: 10, textAlign: "center", font: "bold 14px Segoe UI,sans-serif", stroke: "#484848", editable: true }, new go.Binding("text", "name").makeTwoWay()) ), // end Auto Panel body // the Panel holding the left port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.leftArray $(go.Panel, "Vertical", new go.Binding("itemArray", "leftArray"), { row: 1, column: 0, itemTemplate: $(go.Panel, { _side: "left", // internal property to make it easier to tell which side it's on fromSpot: go.Spot.Left, toSpot: go.Spot.Left, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(1, 0) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ), // end Vertical Panel // the Panel holding the top port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.topArray $(go.Panel, "Horizontal", new go.Binding("itemArray", "topArray"), { row: 0, column: 1, itemTemplate: $(go.Panel, { _side: "top", fromSpot: go.Spot.Top, toSpot: go.Spot.Top, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(0, 1) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ), // end Horizontal Panel // the Panel holding the right port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.rightArray $(go.Panel, "Vertical", new go.Binding("itemArray", "rightArray"), { row: 1, column: 2, itemTemplate: $(go.Panel, { _side: "right", fromSpot: go.Spot.Right, toSpot: go.Spot.Right, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(1, 0) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ), // end Vertical Panel // the Panel holding the bottom port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.bottomArray $(go.Panel, "Horizontal", new go.Binding("itemArray", "bottomArray"), { row: 2, column: 1, itemTemplate: $(go.Panel, { _side: "bottom", fromSpot: go.Spot.Bottom, toSpot: go.Spot.Bottom, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(0, 1) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ) // end Horizontal Panel ); // end Node // an orthogonal link template, reshapable and relinkable myDiagram.linkTemplate = $(CustomLink, // defined below { routing: go.Link.AvoidsNodes, corner: 4, curve: go.Link.JumpGap, reshapable: true, resegmentable: true, relinkableFrom: true, relinkableTo: true }, new go.Binding("points").makeTwoWay(), $(go.Shape, { stroke: "#2F4F4F", strokeWidth: 2 }) ); // support double-clicking in the background to add a copy of this data as a node myDiagram.toolManager.clickCreatingTool.archetypeNodeData = { name: "Unit", leftArray: [], rightArray: [], topArray: [], bottomArray: [] }; myDiagram.contextMenu = $("ContextMenu", makeButton("Paste", function(e, obj) { e.diagram.commandHandler.pasteSelection(e.diagram.toolManager.contextMenuTool.mouseDownPoint); }, function(o) { return o.diagram.commandHandler.canPasteSelection(o.diagram.toolManager.contextMenuTool.mouseDownPoint); }), makeButton("Undo", function(e, obj) { e.diagram.commandHandler.undo(); }, function(o) { return o.diagram.commandHandler.canUndo(); }), makeButton("Redo", function(e, obj) { e.diagram.commandHandler.redo(); }, function(o) { return o.diagram.commandHandler.canRedo(); }) ); // load the diagram from JSON data load(); } // This custom-routing Link class tries to separate parallel links from each other. // This assumes that ports are lined up in a row/column on a side of the node. function CustomLink() { go.Link.call(this); }; go.Diagram.inherit(CustomLink, go.Link); CustomLink.prototype.findSidePortIndexAndCount = function(node, port) { var nodedata = node.data; if (nodedata !== null) { var portdata = port.data; var side = port._side; var arr = nodedata[side + "Array"]; var len = arr.length; for (var i = 0; i < len; i++) { if (arr[i] === portdata) return [i, len]; } } return [-1, len]; }; CustomLink.prototype.computeEndSegmentLength = function(node, port, spot, from) { var esl = go.Link.prototype.computeEndSegmentLength.call(this, node, port, spot, from); var other = this.getOtherPort(port); if (port !== null && other !== null) { var thispt = port.getDocumentPoint(this.computeSpot(from)); var otherpt = other.getDocumentPoint(this.computeSpot(!from)); if (Math.abs(thispt.x - otherpt.x) > 20 || Math.abs(thispt.y - otherpt.y) > 20) { var info = this.findSidePortIndexAndCount(node, port); var idx = info[0]; var count = info[1]; if (port._side == "top" || port._side == "bottom") { if (otherpt.x < thispt.x) { return esl + 4 + idx * 8; } else { return esl + (count - idx - 1) * 8; } } else { // left or right if (otherpt.y < thispt.y) { return esl + 4 + idx * 8; } else { return esl + (count - idx - 1) * 8; } } } } return esl; }; CustomLink.prototype.hasCurviness = function() { if (isNaN(this.curviness)) return true; return go.Link.prototype.hasCurviness.call(this); }; CustomLink.prototype.computeCurviness = function() { if (isNaN(this.curviness)) { var fromnode = this.fromNode; var fromport = this.fromPort; var fromspot = this.computeSpot(true); var frompt = fromport.getDocumentPoint(fromspot); var tonode = this.toNode; var toport = this.toPort; var tospot = this.computeSpot(false); var topt = toport.getDocumentPoint(tospot); if (Math.abs(frompt.x - topt.x) > 20 || Math.abs(frompt.y - topt.y) > 20) { if ((fromspot.equals(go.Spot.Left) || fromspot.equals(go.Spot.Right)) && (tospot.equals(go.Spot.Left) || tospot.equals(go.Spot.Right))) { var fromseglen = this.computeEndSegmentLength(fromnode, fromport, fromspot, true); var toseglen = this.computeEndSegmentLength(tonode, toport, tospot, false); var c = (fromseglen - toseglen) / 2; if (frompt.x + fromseglen >= topt.x - toseglen) { if (frompt.y < topt.y) return c; if (frompt.y > topt.y) return -c; } } else if ((fromspot.equals(go.Spot.Top) || fromspot.equals(go.Spot.Bottom)) && (tospot.equals(go.Spot.Top) || tospot.equals(go.Spot.Bottom))) { var fromseglen = this.computeEndSegmentLength(fromnode, fromport, fromspot, true); var toseglen = this.computeEndSegmentLength(tonode, toport, tospot, false); var c = (fromseglen - toseglen) / 2; if (frompt.x + fromseglen >= topt.x - toseglen) { if (frompt.y < topt.y) return c; if (frompt.y > topt.y) return -c; } } } } return go.Link.prototype.computeCurviness.call(this); }; // end CustomLink class // Add a port to the specified side of the selected nodes. function addPort(side) { myDiagram.startTransaction("addPort"); myDiagram.selection.each(function(node) { // skip any selected Links if (!(node instanceof go.Node)) return; // compute the next available index number for the side var i = 0; while (node.findPort(side + i.toString()) !== node) i++; // now this new port name is unique within the whole Node because of the side prefix var name = side + i.toString(); // get the Array of port data to be modified var arr = node.data[side + "Array"]; if (arr) { // create a new port data object var newportdata = { portId: name, portColor: getPortColor() // if you add port data properties here, you should copy them in copyPortData above }; // and add it to the Array of port data myDiagram.model.insertArrayItem(arr, -1, newportdata); } }); myDiagram.commitTransaction("addPort"); } // Exchange the position/order of the given port with the next one. // If it's the last one, swap with the previous one. function swapOrder(port) { var arr = port.panel.itemArray; if (arr.length >= 2) { // only if there are at least two ports! for (var i = 0; i < arr.length; i++) { if (arr[i].portId === port.portId) { myDiagram.startTransaction("swap ports"); if (i >= arr.length - 1) i--; // now can swap I and I+1, even if it's the last port var newarr = arr.slice(0); // copy Array newarr[i] = arr[i + 1]; // swap items newarr[i + 1] = arr[i]; // remember the new Array in the model myDiagram.model.setDataProperty(port.part.data, port._side + "Array", newarr); myDiagram.commitTransaction("swap ports"); break; } } } } // Remove the clicked port from the node. // Links to the port will be redrawn to the node's shape. function removePort(port) { myDiagram.startTransaction("removePort"); var pid = port.portId; var arr = port.panel.itemArray; for (var i = 0; i < arr.length; i++) { if (arr[i].portId === pid) { myDiagram.model.removeArrayItem(arr, i); break; } } myDiagram.commitTransaction("removePort"); } // Remove all ports from the same side of the node as the clicked port. function removeAll(port) { myDiagram.startTransaction("removePorts"); var nodedata = port.part.data; var side = port._side; // there are four property names, all ending in "Array" myDiagram.model.setDataProperty(nodedata, side + "Array", []); // an empty Array myDiagram.commitTransaction("removePorts"); } // Change the color of the clicked port. function changeColor(port) { myDiagram.startTransaction("colorPort"); var data = port.data; myDiagram.model.setDataProperty(data, "portColor", getPortColor()); myDiagram.commitTransaction("colorPort"); } // Use some pastel colors for ports function getPortColor() { var portColors = ["#fae3d7", "#d6effc", "#ebe3fc", "#eaeef8", "#fadfe5", "#6cafdb", "#66d6d1"] return portColors[Math.floor(Math.random() * portColors.length)]; } // Save the model to / load it from JSON text shown on the page itself, not in a database. function save() { document.getElementById("mySavedModel").value = myDiagram.model.toJson(); myDiagram.isModified = false; } function load() { myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value); // When copying a node, we need to copy the data that the node is bound to. // This JavaScript object includes properties for the node as a whole, and // four properties that are Arrays holding data for each port. // Those arrays and port data objects need to be copied too. // Thus Model.copiesArrays and Model.copiesArrayObjects both need to be true. // Link data includes the names of the to- and from- ports; // so the GraphLinksModel needs to set these property names: // linkFromPortIdProperty and linkToPortIdProperty. } </script> </head> <body onload="init()"> <div id="sample"> <div id="myDiagramDiv" style="width:600px; height:500px; border:1px solid black"></div> Add port to selected nodes: <button onclick="addPort('top')">Top</button> <button onclick="addPort('bottom')">Bottom</button> <button onclick="addPort('left')">Left</button> <button onclick="addPort('right')">Right</button> <p> Double-click in the diagram background in order to add a new node there. In this sample you can add ports to a selected node by clicking the above buttons or by using the context menu. Draw links between ports by dragging between ports. If you select a link you can relink or reshape it. Right-click or touch-hold on a port to bring up a context menu that allows you to remove it or change its color. </p> <p> The diagram also uses a custom link to allow for special routing to help parallel links avoid each other using overridden <a>Link.computeEndSegmentLength</a>, <a>Link.hasCurviness</a>, and <a>Link.computeCurviness</a> functions. </p> <p> See the <a href="../intro/ports.html">Ports Intro page</a> for an explanation of GoJS ports. </p> <div> <div> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: </div> <textarea id="mySavedModel" style="width:100%;height:250px"> { "class": "go.GraphLinksModel", "copiesArrays": true, "copiesArrayObjects": true, "linkFromPortIdProperty": "fromPort", "linkToPortIdProperty": "toPort", "nodeDataArray": [ {"key":1, "name":"Unit One", "loc":"101 204", "leftArray":[ {"portColor":"#fae3d7", "portId":"left0"} ], "topArray":[ {"portColor":"#d6effc", "portId":"top0"} ], "bottomArray":[ {"portColor":"#ebe3fc", "portId":"bottom0"} ], "rightArray":[ {"portColor":"#eaeef8", "portId":"right0"},{"portColor":"#fadfe5", "portId":"right1"} ] }, {"key":2, "name":"Unit Two", "loc":"320 152", "leftArray":[ {"portColor":"#6cafdb", "portId":"left0"},{"portColor":"#66d6d1", "portId":"left1"},{"portColor":"#fae3d7", "portId":"left2"} ], "topArray":[ {"portColor":"#d6effc", "portId":"top0"} ], "bottomArray":[ {"portColor":"#eaeef8", "portId":"bottom0"},{"portColor":"#eaeef8", "portId":"bottom1"},{"portColor":"#6cafdb", "portId":"bottom2"} ], "rightArray":[ ] }, {"key":3, "name":"Unit Three", "loc":"384 319", "leftArray":[ {"portColor":"#66d6d1", "portId":"left0"},{"portColor":"#fadfe5", "portId":"left1"},{"portColor":"#6cafdb", "portId":"left2"} ], "topArray":[ {"portColor":"#66d6d1", "portId":"top0"} ], "bottomArray":[ {"portColor":"#6cafdb", "portId":"bottom0"} ], "rightArray":[ ] }, {"key":4, "name":"Unit Four", "loc":"138 351", "leftArray":[ {"portColor":"#fae3d7", "portId":"left0"} ], "topArray":[ {"portColor":"#6cafdb", "portId":"top0"} ], "bottomArray":[ {"portColor":"#6cafdb", "portId":"bottom0"} ], "rightArray":[ {"portColor":"#6cafdb", "portId":"right0"},{"portColor":"#66d6d1", "portId":"right1"} ] } ], "linkDataArray": [ {"from":4, "to":2, "fromPort":"top0", "toPort":"bottom0"}, {"from":4, "to":2, "fromPort":"top0", "toPort":"bottom0"}, {"from":3, "to":2, "fromPort":"top0", "toPort":"bottom1"}, {"from":4, "to":3, "fromPort":"right0", "toPort":"left0"}, {"from":4, "to":3, "fromPort":"right1", "toPort":"left2"}, {"from":1, "to":2, "fromPort":"right0", "toPort":"left1"}, {"from":1, "to":2, "fromPort":"right1", "toPort":"left2"} ]} </textarea> </div> </div> </body> </html>