gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
520 lines (485 loc) • 22.4 kB
HTML
<!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>