339 lines
13 KiB
HTML
339 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Movable, Copyable, Deletable Ports</title>
|
|
<meta name="description" content="Nodes with selectable, movable, copyable, and deletable ports." />
|
|
<!-- Copyright 1998-2017 by Northwoods Software Corporation. -->
|
|
<meta charset="UTF-8">
|
|
<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",
|
|
{
|
|
initialContentAlignment: go.Spot.Center,
|
|
"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.contains(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>
|