Related
It is not apparent to me how to edit the GoJS Pipes example here so that I can display text within the "pipes" without messing up the layout. I am trying to apply the answer to the same question given here but it's so old that it could be out of date, or maybe it's just not explicit enough for me to make sense of it properly with my limited knowledge of this lib.
I have started a hardcoded pipeline that looks like so:
The pipeline code:
const base = 15;
const minLen = base * 2;
const maxLen = base * 6;
const minInc1 = minLen + base;
const minInc2 = minLen * 2;
const minInc3 = minLen + (base * 3);
interface IShapeParams {
angle?: number
text?: string
key: number
}
const createShapeI = (params: IShapeParams): ObjectData => ({
geo: `F1 M0 0 L${minLen} 0 ${minLen} ${maxLen} 0 ${maxLen}z`,
ports: [
{ id: "U1", spot: "0.5 0 0 0.5" },
{ id: "U2", spot: "0.5 1 0 -0.5" }
],
...params
});
const startHorz = 0;
const startVert = 0;
export const pipeline = {
"class": "go.GraphLinksModel",
"copiesArrays": true,
"copiesArrayObjects": true,
"linkFromPortIdProperty": "fid",
"linkToPortIdProperty": "tid",
"nodeDataArray": [
{
...createShapeI({ key: 1, text: "Pipe 1", angle: 90 }),
"loc": `${startHorz} ${startVert}`
},
{
...createShapeI({ key: 2, text: "Pipe 2", angle: 90 }),
"loc": `${startHorz - maxLen} ${startVert}`
}
],
"linkDataArray": [
{ "from": 1, "to": 2, "fid": "U2", "tid": "U1" }
]
};
The original nodeTemplate from the Pipe source code:
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{
locationObjectName: "SHAPE",
locationSpot: go.Spot.Center,
selectionAdorned: false, // use a Binding on the Shape.stroke to show selection
itemTemplate:
// each port is an "X" shape whose alignment spot and port ID are given by the item data
$(go.Panel,
new go.Binding("portId", "id"),
new go.Binding("alignment", "spot", go.Spot.parse),
$(go.Shape, "XLine",
{ width: 6, height: 6, background: "transparent", fill: null, stroke: "gray" },
new go.Binding("figure", "id", portFigure), // portFigure converter is defined below
new go.Binding("angle", "angle"))
),
// hide a port when it is connected
linkConnected: (node, link, port) => {
if (link.category === "") {
port.visible = false;
}
},
linkDisconnected: (node, link, port) => {
if (link.category === "") {
port.visible = true;
}
}
},
// this creates the variable number of ports for this Spot Panel, based on the data
new go.Binding("itemArray", "ports"),
// remember the location of this Node
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// remember the angle of this Node
new go.Binding("angle", "angle").makeTwoWay(),
// move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts
new go.Binding("layerName", "isSelected", function(s) {
return s ? "Foreground" : "";
}).ofObject(),
$(go.Shape,
{
name: "SHAPE",
// the following are default values;
// actual values may come from the node data object via data binding
geometryString: "F1 M0 0 L20 0 20 20 0 20 z",
fill: "rgba(128, 128, 128, 0.5)"
},
// this determines the actual shape of the Shape
new go.Binding("geometryString", "geo"),
// selection causes the stroke to be blue instead of black
new go.Binding("stroke", "isSelected", (s) => {
return s ? "dodgerblue" : "black";
}).ofObject())
);
Now when I try to apply the solution given on the referenced thread I end up with the nodeTemplate like so:
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{
locationObjectName: "SHAPE",
locationSpot: go.Spot.Center,
selectionAdorned: false, // use a Binding on the Shape.stroke to show selection
// hide a port when it is connected
linkConnected: (node, link, port) => {
if (link.category === "") {
port.visible = false;
}
},
linkDisconnected: (node, link, port) => {
if (link.category === "") {
port.visible = true;
}
}
},
$(go.Panel, "Spot",
{
itemTemplate:
// each port is an "X" shape whose alignment spot and port ID are given by the item data
$(go.Panel,
new go.Binding("portId", "id"),
new go.Binding("alignment", "spot", go.Spot.parse),
$(go.Shape, "XLine",
{ width: 6, height: 6, background: "transparent", fill: null, stroke: "gray" },
new go.Binding("figure", "id", portFigure), // portFigure converter is defined below
new go.Binding("angle", "angle"))
)
},
// this creates the variable number of ports for this Spot Panel, based on the data
new go.Binding("itemArray", "ports"),
// remember the location of this Node
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// remember the angle of this Node
new go.Binding("angle", "angle").makeTwoWay(),
// move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts
new go.Binding("layerName", "isSelected", function(s) {
return s ? "Foreground" : "";
}).ofObject(),
$(go.Shape,
{
name: "SHAPE",
// the following are default values;
// actual values may come from the node data object via data binding
geometryString: "F1 M0 0 L20 0 20 20 0 20 z",
fill: "rgba(128, 128, 128, 0.5)"
},
// this determines the actual shape of the Shape
new go.Binding("geometryString", "geo"),
// selection causes the stroke to be blue instead of black
new go.Binding("stroke", "isSelected", (s) => {
return s ? "dodgerblue" : "black";
}).ofObject())
),
$(go.TextBlock, { margin: 5 }, new go.Binding("text", "text"))
);
This renders the text correctly, but it completely breaks the layout as the ports have switched sides, I have no idea why. On top of that, the space seen in between the pipes now I cannot reconcile at all by trying to alter the loc strings on the pipeline, in fact nothing I do to the loc properties does anything at all now. See below image:
How can I just add text blocks to these shapes while keeping the same layout and functionality?
So the pipe template is currently structured like this:
Node (that is a Spot Panel)
- 1st child: Shape
- 2nd-nth child: Item array of x's
Spot panels have one main element (usually the first child unless you specify it), and then N other elements that are positioned based on it. So at the very minimum you want a structure like this:
Node (that is a Spot Panel)
- 1st child: Panel (another Spot panel)
- Shape
- TextBlock
- 2nd-nth child: Item array of x's
So you are swapping out the Shape for a Panel (a container with lots of stuff) that will contain that shape. This will work as long as that Panel is exactly the same size as the shape it would be replacing. If its larger, for instance if the text is larger than the shape, then you're in trouble.
How to solve that problem really depends on what you want the result to look like. The simplest way would be to force the TextBlock to stretch: go.GraphObject.Fill, so that it always sizes itself identical to the shape, so that the Panel (Shape+TextBlock) is always identical in size to the Shape it replaced.
Separately you probably want the TextBlock, which will assume the entire area of the gray shape, to be centered vertically, so you'd want to add verticalAlignment: go.Spot.Center
Like this:
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{
locationObjectName: "SHAPE",
locationSpot: go.Spot.Center,
selectionAdorned: false, // use a Binding on the Shape.stroke to show selection
itemTemplate:
// each port is an "X" shape whose alignment spot and port ID are given by the item data
$(go.Panel,
new go.Binding("portId", "id"),
new go.Binding("alignment", "spot", go.Spot.parse),
$(go.Shape, "XLine",
{ width: 6, height: 6, background: "transparent", fill: null, stroke: "gray" },
new go.Binding("figure", "id", portFigure), // portFigure converter is defined below
new go.Binding("angle", "angle"))
),
// hide a port when it is connected
linkConnected: function(node, link, port) {
if (link.category === "") port.visible = false;
},
linkDisconnected: function(node, link, port) {
if (link.category === "") port.visible = true;
}
},
// this creates the variable number of ports for this Spot Panel, based on the data
new go.Binding("itemArray", "ports"),
// remember the location of this Node
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// remember the angle of this Node
new go.Binding("angle", "angle").makeTwoWay(),
// move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
// Everything except the ports: (the pipe body, and pipe text)
$(go.Panel, "Spot",
$(go.Shape,
{
name: "SHAPE",
// the following are default values;
// actual values may come from the node data object via data binding
geometryString: "F1 M0 0 L20 0 20 20 0 20 z",
fill: "rgba(128, 128, 128, 0.5)"
},
// this determines the actual shape of the Shape
new go.Binding("geometryString", "geo"),
// selection causes the stroke to be blue instead of black
new go.Binding("stroke", "isSelected", function(s) { return s ? "dodgerblue" : "black"; }).ofObject()),
$(go.TextBlock, "Some text")
)
);
Here is that modification live: https://codepen.io/simonsarris/pen/RwrKqrq?editors=1010
I want to make custom choices dropdown for users to edit node and link text.
So far, I used this code to make user select nodes and links text selectable in dropdown list:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>State Chart</title>
<meta name="description" content="A finite state machine chart with editable and interactive features." />
<meta charset="UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<link href="http://getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="{{url_for('static', filename='go.js')}}"></script>
<!-- custom text editors -->
<script src="{{url_for('static', filename='TextEditorSelectBox.js')}}"></script>
<script src="{{url_for('static', filename='TextEditorRadioButtons.js')}}"></script>
<script src="{{url_for('static', filename='TextEditorSelectBox.js')}}"></script>
<script src="{{url_for('static', filename='DataInspector.js')}}"></script>
<link href="https://gojs.net/latest/extensions/DataInspector.css" rel="stylesheet">
<link href="{{url_for('static', filename='DataInspector.css')}}" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
body {
overflow-x: hidden;
}
#wrapper {
padding-left: 0;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
}
#wrapper.toggled {
padding-left: 250px;
}
#sidebar-wrapper {
z-index: 1000;
position: fixed;
left: 250px;
width: 0;
height: 100%;
margin-left: -250px;
overflow-y: auto;
background: #000;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
}
#wrapper.toggled #sidebar-wrapper {
width: 250px;
}
#page-content-wrapper {
width: 100%;
position: absolute;
padding: 15px;
}
#wrapper.toggled #page-content-wrapper {
position: absolute;
margin-right: -250px;
}
/* Sidebar Styles */
.sidebar-nav {
position: absolute;
top: 0;
width: 250px;
margin: 0;
padding: 0;
list-style: none;
}
.sidebar-nav li {
text-indent: 20px;
line-height: 40px;
}
.sidebar-nav li a {
display: block;
text-decoration: none;
color: #999999;
}
.sidebar-nav li a:hover {
text-decoration: none;
color: #fff;
background: rgba(255, 255, 255, 0.2);
}
.sidebar-nav li a:active, .sidebar-nav li a:focus {
text-decoration: none;
}
.sidebar-nav>.sidebar-brand {
height: 65px;
font-size: 18px;
line-height: 60px;
}
.sidebar-nav>.sidebar-brand a {
color: #999999;
}
.sidebar-nav>.sidebar-brand a:hover {
color: #fff;
background: none;
}
#media(min-width:768px) {
#wrapper {
padding-left: 0;
}
#wrapper.toggled {
padding-left: 250px;
}
#sidebar-wrapper {
width: 0;
}
#wrapper.toggled #sidebar-wrapper {
width: 250px;
}
#page-content-wrapper {
padding: 20px;
position: relative;
}
#wrapper.toggled #page-content-wrapper {
position: relative;
margin-right: 0;
}
}
</style>
<script id="code">
var nodeChoices = ['choice 1', 'choice 2', 'choice 3', 'choice 4', 'choice 5'];
const originalNodeChoices = ['choice 1', 'choice 2', 'choice 3', 'choice 4', 'choice 5'];
var linkChoices = ['link choice 1', 'link choice 2', 'link choice 3', 'link choice 4', 'link choice 5'];
const originalLinkChoices = ['link choice 1', 'link choice 2', 'link choice 3', 'link choice 4', 'link choice 5'];
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
// start everything in the middle of the viewport
initialContentAlignment: go.Spot.Center,
// have mouse wheel events zoom in and out instead of scroll up and down
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
// support double-click in background creating a new node
"clickCreatingTool.archetypeNodeData": { text: "new node" },
// enable undo & redo
"textEditingTool.defaultTextEditor": window.TextEditorSelectBox,
"undoManager.isEnabled": true,
"layout": new go.ForceDirectedLayout(),
"ModelChanged": function(e) {
//console.log("Diagram model changed!");
if (e.change === go.ChangedEvent.Remove && e.modelChange === "linkDataArray") {
console.log("eee its linkDataArray");
console.log(e);
var linkdata = e.oldValue;
console.log("linkdata");
console.log(linkdata);
var oldstr = linkdata.text;
console.log("oldstr");
console.log(oldstr);
if (!oldstr) return;
var customModelData = e.model.modelData.choices;
var choices = e.model.modelData.choices.linkChoices;
console.log("choices");
console.log(choices);
var idx = choices.indexOf(oldstr);
if (idx < 0) {
console.log("adding choice: " + oldstr);
var newchoices = Array.prototype.slice.call(choices);
newchoices.push(oldstr);
e.model.set(e.model.modelData, "choices", newchoices);
}
}else if(e.change === go.ChangedEvent.Remove && e.modelChange === "nodeDataArray"){
console.log("eee its nodeDataArray");
}
/*
else{
console.log("elseeeeeee111!");
console.log("e.change");
console.log(e.change);
console.log("e.modelChange");
console.log(e.modelChange);
}*/
}
});
//myDiagram.model.set(myDiagram.model.modelData, "choices", JSON.parse('{{ link_choices | tojson | safe}}'));
//myDiagram.model.set(myDiagram.model.modelData, "choices", ["one", "two", "three"]);
console.log("myDiagram.model.modelData");
console.log(myDiagram.model.modelData);
console.log("myDiagram.model.modelData.choices");
console.log(myDiagram.model.modelData.choices);
// 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);
}
});
myDiagram.addDiagramListener("textEdited", function(e) {
console.log("Text is edited");
console.log(e);
//CHECK IF LINK,
//IF YES REMOVE THAT OPTION FROM LIST
});
myDiagram.addDiagramListener("SelectionDeleting", function(e) {
console.log("inside SelectionDeleting");
console.log(e);
//CHECK IF LINK,
//IF YES PUT THAT OPTION BACK IN OPTION LIST
});
// define the Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// define the node's outer shape, which will surround the TextBlock
$(go.Shape, "RoundedRectangle",
{
parameter1: 20, // the corner has a large radius
fill: $(go.Brush, "Linear", { 0: "rgb(254, 201, 0)", 1: "rgb(254, 162, 0)" }),
stroke: null,
portId: "", // this Shape is the Node's port, not the whole Node
fromLinkable: true, fromLinkableDuplicates: true,
toLinkable: true, toLinkableDuplicates: true,
cursor: "pointer"
}),
$(go.TextBlock,
{
font: "bold 11pt helvetica, bold arial, sans-serif",
editable: true, // editing the text automatically updates the model data
//textEditor: window.TextEditorRadioButtons, // defined in textEditorRadioButtons.js
// this specific TextBlock has its own choices:
textEditor: window.TextEditorRadioButtons,
textEdited: function(tb, oldstr, newstr) {
console.log("textEdited nodeeeeeeeeeeeeeeeeeeeeeeeee!!!");
var choices = tb.diagram.model.modelData.choices.nodeChoices;
console.log("choices");
console.log(choices);
console.log("newstr");
console.log(newstr);
console.log("oldstr");
console.log(oldstr);
var idx = choices.indexOf(newstr);
if (idx >= 0 && oldstr !== newstr ) {
console.log(choices);
console.log("choices");
console.log(choices);
console.log("removing choice " + idx + ": " + newstr);
var newchoices = Array.prototype.slice.call(choices);
newchoices.splice(idx, 1);
console.log("after remove");
console.log(newchoices);
var currentChoicesFromModel = tb.diagram.model.modelData.choices;
console.log("currentChoicesFromModel");
console.log(currentChoicesFromModel);
currentChoicesFromModel.nodeChoices = newchoices;
console.log("currentChoicesFromModel after update: ");
console.log(currentChoicesFromModel);
tb.diagram.model.set(tb.diagram.model.modelData, "choices", currentChoicesFromModel);
//tb.editable = false; // don't allow choice again
}else{
console.log("elseeeeee");
}
},
//choices: JSON.parse('{{ choices | tojson | safe}}')
choices: nodeChoices
},
new go.Binding("text").makeTwoWay())
);
myDiagram.nodeTemplate.selectionAdornmentTemplate =
$(go.Adornment, "Spot",
$(go.Panel, "Auto",
$(go.Shape, { stroke: "dodgerblue", strokeWidth: 2, fill: null }),
$(go.Placeholder)
),
$(go.Panel, "Horizontal",
{ alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom },
$("Button",
{ click: editText }, // defined below, to support editing the text of the node
$(go.TextBlock, "t",
{ font: "bold 10pt sans-serif", desiredSize: new go.Size(15, 15), textAlign: "center" })
),
$("Button",
{ // drawLink is defined below, to support interactively drawing new links
click: drawLink, // click on Button and then click on target node
actionMove: drawLink // drag from Button to the target node
},
$(go.Shape,
{ geometryString: "M0 0 L8 0 8 12 14 12 M12 10 L14 12 12 14" })
),
$("Button",
{
actionMove: dragNewNode, // defined below, to support dragging from the button
_dragData: { text: "?????", color: "lightgray" }, // node data to copy
click: clickNewNode // defined below, to support a click on the button
},
$(go.Shape,
{ geometryString: "M0 0 L3 0 3 10 6 10 x F1 M6 6 L14 6 14 14 6 14z", fill: "gray" })
)
)
);
//myDiagram.model.set(myDiagram.model.modelData, "choices", JSON.parse('{{ link_choices | tojson | safe}}'));
//myDiagram.model.set(myDiagram.model.modelData, "choices", ["one", "two", "three"]);
console.log("myDiagram.model.modelData");
console.log(myDiagram.model.modelData);
console.log(myDiagram.model.modelData.choices);
function editText(e, button) {
//console.log(e);
var node = button.part.adornedPart;
console.log("node");
//console.log(node);
e.diagram.commandHandler.editTextBlock(node.findObject("TEXTBLOCK"));
//$("#nodeText").val(node.findObject("TEXTBLOCK"));
}
function drawLink(e, button) {
var node = button.part.adornedPart;
var tool = e.diagram.toolManager.linkingTool;
tool.startObject = node.port;
e.diagram.currentTool = tool;
tool.doActivate();
}
// used by both clickNewNode and dragNewNode to create a node and a link
// from a given node to the new node
function createNodeAndLink(data, fromnode) {
var diagram = fromnode.diagram;
var model = diagram.model;
var nodedata = model.copyNodeData(data);
model.addNodeData(nodedata);
var newnode = diagram.findNodeForData(nodedata);
var linkdata = model.copyLinkData({});
model.setFromKeyForLinkData(linkdata, model.getKeyForNodeData(fromnode.data));
model.setToKeyForLinkData(linkdata, model.getKeyForNodeData(newnode.data));
model.addLinkData(linkdata);
diagram.select(newnode);
return newnode;
}
// the Button.click event handler, called when the user clicks the "N" button
function clickNewNode(e, button) {
var data = button._dragData;
if (!data) return;
e.diagram.startTransaction("Create Node and Link");
var fromnode = button.part.adornedPart;
var newnode = createNodeAndLink(button._dragData, fromnode);
newnode.location = new go.Point(fromnode.location.x + 200, fromnode.location.y);
e.diagram.commitTransaction("Create Node and Link");
}
// the Button.actionMove event handler, called when the user drags within the "N" button
function dragNewNode(e, button) {
var tool = e.diagram.toolManager.draggingTool;
if (tool.isBeyondDragSize()) {
var data = button._dragData;
if (!data) return;
e.diagram.startTransaction("button drag"); // see doDeactivate, below
var newnode = createNodeAndLink(data, button.part.adornedPart);
newnode.location = e.diagram.lastInput.documentPoint;
// don't commitTransaction here, but in tool.doDeactivate, after drag operation finished
// set tool.currentPart to a selected movable Part and then activate the DraggingTool
tool.currentPart = newnode;
e.diagram.currentTool = tool;
tool.doActivate();
}
}
// using dragNewNode also requires modifying the standard DraggingTool so that it
// only calls commitTransaction when dragNewNode started a "button drag" transaction;
// do this by overriding DraggingTool.doDeactivate:
var tool = myDiagram.toolManager.draggingTool;
tool.doDeactivate = function() {
// commit "button drag" transaction, if it is ongoing; see dragNewNode, above
if (tool.diagram.undoManager.nestedTransactionNames.elt(0) === "button drag") {
tool.diagram.commitTransaction();
}
go.DraggingTool.prototype.doDeactivate.call(tool); // call the base method
};
// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{
curve: go.Link.Bezier,
adjusting: go.Link.Stretch,
reshapable: true,
relinkableFrom: true,
relinkableTo: true,
toShortLength: 3
},
new go.Binding("points").makeTwoWay(),
new go.Binding("curviness"),
$(go.Shape, // the link shape
{ strokeWidth: 1.5 }),
$(go.Shape, // the arrowhead
{ toArrow: "standard", stroke: null }),
$(go.Panel, "Auto",
$(go.Shape, // the label background, which becomes transparent around the edges
{
fill: $(go.Brush, "Radial", { 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 0)" }),
stroke: null
}),
$(go.TextBlock,
{
background: "white",
editable: true,
textEditor: window.TextEditorSelectBox, // defined in extensions/textEditorSelectBox.js
textEdited: function(tb, oldstr, newstr) {
var choices = tb.diagram.model.modelData.choices.linkChoices;
console.log("choices");
console.log(choices);
console.log("newstr");
console.log(newstr);
console.log("oldstr");
console.log(oldstr);
var idx = choices.indexOf(newstr);
if (idx >= 0 && oldstr !== newstr ) {
console.log(choices);
console.log("choices");
console.log(choices);
console.log("removing choice " + idx + ": " + newstr);
var newchoices = Array.prototype.slice.call(choices);
newchoices.splice(idx, 1);
tb.diagram.model.set(tb.diagram.model.modelData.choices.linkChoices, "choices", newchoices);
//tb.editable = false; // don't allow choice again
}else{
console.log("elseeeeee");
}
}
},
// editing the text automatically updates the model data
//new go.Binding("text"),
new go.Binding("text").makeTwoWay(),
new go.Binding("choices").ofModel())
)
);
var inspector = new Inspector('myInspectorDiv', myDiagram,
{
// uncomment this line to only inspect the named properties below instead of all properties on each object:
// includesOwnProperties: false,
properties: {
"text": { },
// an example of specifying the type
"password": { show: Inspector.showIfPresent, type: 'password' },
// key would be automatically added for nodes, but we want to declare it read-only also:
"key": { readOnly: true, show: Inspector.showIfPresent },
// color would be automatically added for nodes, but we want to declare it a color also:
"color": { show: Inspector.showIfPresent, type: 'color' },
// Comments and LinkComments are not in any node or link data (yet), so we add them here:
"Comments": { show: Inspector.showIfNode },
"flag": { show: Inspector.showIfNode, type: 'checkbox' },
"LinkComments": { show: Inspector.showIfLink },
"isGroup": { readOnly: true, show: Inspector.showIfPresent }
}
});
// read in the JSON data from flask
loadGraphData();
}
function loadGraphData() {
var graphDataString = JSON.parse('{{ diagramData | tojson | safe}}');
//console.log("graphDataString");
//console.log(graphDataString);
var allChoices = JSON.parse('{{ allChoices | tojson | safe}}');
console.log("allChoices");
console.log(allChoices);
myDiagram.model = go.Model.fromJson(graphDataString);
//myDiagram.model.set(myDiagram.model.modelData, "choices", ["one", "two", "three"]);
var customModelData = {
"linkChoices": linkChoices, "nodeChoices": nodeChoices
};
myDiagram.model.set(myDiagram.model.modelData, "choices", allChoices);
//myDiagram.model.set(myDiagram.model.modelData, "choices", JSON.parse('{{ link_choices | tojson | safe}}'));
//myDiagram.model.set(myDiagram.model.modelData, "choices", customModelData);
console.log("whole model modelData");
console.log(myDiagram.model.modelData);
}
function saveGraphData(form, event) {
console.log("inside saveGraphData");
event.preventDefault();
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
form.submit();
}
function zoomToFit(){
console.log("inside zoomToFit");
myDiagram.zoomToRect(myDiagram.documentBounds);
}
function zoomIn(){
console.log("inside zoomIn");
myDiagram.commandHandler.increaseZoom();
}
function zoomOut(){
console.log("inside zoomOut");
myDiagram.commandHandler.decreaseZoom();
}
</script>
</head>
<body onload="init()">
<div id="wrapper">
<!-- Sidebar -->
<div id="sidebar-wrapper">
<ul class="sidebar-nav">
<li class="sidebar-brand"> CUSTOM CHOICES MODE </li>
<li> Free editing mode </li>
<li> Fill mode </li>
<li> Custom choices mode </li>
<li> Locked choices mode </li>
</ul>
</div>
<!-- /#sidebar-wrapper -->
<!-- Page Content -->
<div id="page-content-wrapper">
<div class="container-fluid">
<div id=formWrapper style="padding: 30px;">
Toggle Menu
<form method="POST" action="http://localhost:5000/updateResultFile" name="updateResultFileForm"
id="updateResultFileForm"
onsubmit="saveGraphData(this, event);">
<div id="graphWrapper" style="margin-bottom: 15px;">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 800px;margin-bottom: 15px;"></div>
<div style="display: none;"><input id="mySavedModel" name="mySavedModel"></div>
<button class="btn btn-default" type="submit"> Save <i class="fa fa-save"> </i> </button>
</div>
</form>
<div id="myInspectorDiv">
</div>
<div>
<button class="btn btn-default" onclick="zoomToFit()"> Zoom to fit <i class="fa fa-search"> </i> </button>
<button class="btn btn-default" onclick="zoomIn()"> Zoom in <i class="fa fa-search-plus"> </i> </button>
<button class="btn btn-default" onclick="zoomOut()"> Zoom out <i class="fa fa-search-minus"> </i> </button>
</div>
</div>
</div>
</div>
</div>
<script src="{{url_for('static', filename='jquery.min.js')}}"></script>
<script src="{{url_for('static', filename='bootstrap.bundle.min.js')}}"></script>
<!-- Menu Toggle Script -->
<script>
var menuOpen = false;
$("#menu-toggle").click(function(e) {
e.preventDefault();
$("#wrapper").toggleClass("toggled");
if(menuOpen){
menuOpen = false;
$("#myDiagramDiv").css({border: "solid 1px black"});
}else{
menuOpen = true;
$("#myDiagramDiv").css({border: ""});
}
});
</script>
</body>
</html>
So basicly I have made custom choices for nodes and links. For example, for node I have these choices:
node choice 1
node choice 2
node choice 3
and for links I have these choices:
link choice 1
link choice 2
link choice 3
What I want to accomplish is that when I select "node choice 1" from dropdown list on one node, that that option "node choice 1" can not be selected again for another node. But if user deletes node, or sets empty selection on node text, "node choice 1" must be available again.
Same thing goes for links.
I think my code works fine for links, but I have problems with detecting node text changes and updating dataModel content.
I seem to have missed this before. Here's some code that answers your question by implementing it:
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
initialContentAlignment: go.Spot.Center,
"undoManager.isEnabled": true,
"ModelChanged": function(e) { // just for demonstration purposes,
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
if (e.change === go.ChangedEvent.Remove) {
if (e.modelChange === "linkDataArray") {
var linkdata = e.oldValue;
var oldstr = linkdata.text;
if (!oldstr) return;
var linkchoices = e.model.modelData.linkchoices;
var idx = linkchoices.indexOf(oldstr);
if (idx < 0) {
console.log("deletion adding link choice: " + oldstr);
var newlinkchoices = Array.prototype.slice.call(linkchoices);
newlinkchoices.push(oldstr);
e.model.set(e.model.modelData, "linkchoices", newlinkchoices);
}
} else if (e.modelChange === "nodeDataArray") {
var nodedata = e.oldValue;
var oldstr = nodedata.text;
if (!oldstr) return;
var nodechoices = e.model.modelData.nodechoices;
var idx = nodechoices.indexOf(oldstr);
if (idx < 0) {
console.log("deletion adding node choice: " + oldstr);
var newnodechoices = Array.prototype.slice.call(nodechoices);
newnodechoices.push(oldstr);
e.model.set(e.model.modelData, "nodechoices", newnodechoices);
}
}
}
}
});
myDiagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape,
{ fill: "white", portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer" },
new go.Binding("fill", "color")),
$(go.TextBlock,
{
margin: 8,
editable: true,
textEditor: window.TextEditorSelectBox, // defined in extensions/textEditorSelectBox.js
textEdited: function(tb, oldstr, newstr) {
if (oldstr !== newstr) {
console.log("changing from " + oldstr + " to " + newstr);
var model = tb.diagram.model;
var nodechoices = model.modelData.nodechoices;
if (!nodechoices) {
nodechoices = [];
model.set(model.modelData, "nodechoices", nodechoices);
}
var idx = nodechoices.indexOf(newstr);
if (idx >= 0) {
console.log("removing node choice " + idx + ": " + newstr);
model.removeArrayItem(nodechoices, idx);
}
if (oldstr) {
console.log(" adding node choice " + oldstr);
model.addArrayItem(nodechoices, oldstr);
}
}
}
},
new go.Binding("text").makeTwoWay(),
new go.Binding("choices", "nodechoices").ofModel())
);
myDiagram.linkTemplate =
$(go.Link,
$(go.Shape),
$(go.Shape, { toArrow: "OpenTriangle" }),
$(go.TextBlock,
{
background: "white",
editable: true,
textEditor: window.TextEditorSelectBox, // defined in extensions/textEditorSelectBox.js
textEdited: function(tb, oldstr, newstr) {
if (oldstr !== newstr) {
console.log("changing from " + oldstr + " to " + newstr);
var model = tb.diagram.model;
var linkchoices = model.modelData.linkchoices;
if (!linkchoices) {
linkchoices = [];
model.set(model.modelData, "linkchoices", linkchoices);
}
var idx = linkchoices.indexOf(newstr);
if (idx >= 0) {
console.log("removing link choice " + idx + ": " + newstr);
model.removeArrayItem(linkchoices, idx);
}
if (oldstr) {
console.log(" adding link choice " + oldstr);
model.addArrayItem(linkchoices, oldstr);
}
}
}
},
new go.Binding("text").makeTwoWay(),
new go.Binding("choices", "linkchoices").ofModel())
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" }
],
[
{ from: 1, to: 2, text: "one" },
{ from: 1, to: 3, text: "two" },
{ from: 3, to: 4, text: "three" }
]);
myDiagram.model.set(myDiagram.model.modelData, "nodechoices", ["Epsilon"]);
myDiagram.model.set(myDiagram.model.modelData, "linkchoices", ["four"]);
}
I have simple python flask app where I send JSON data to my HTML and with goJS I display my graph which looks like this:
I made custom choices dropdown for users to edit node and link text. Those choice options are read from .txt file and sent to html via flask. Options in dropdown lists are made so that when option is selected once, it can not be selected again, until user delete node or link with that used option, and then he can use that option again. So far, I used this code to make nodes text selectable in dropdown list:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>State Chart</title>
<meta name="description" content="A finite state machine chart with editable and interactive features." />
<meta charset="UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<link href="http://getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="{{url_for('static', filename='go.js')}}"></script>
<!-- custom text editors -->
<script src="{{url_for('static', filename='TextEditorSelectBox.js')}}"></script>
<script src="{{url_for('static', filename='TextEditorRadioButtons.js')}}"></script>
<script src="{{url_for('static', filename='TextEditorSelectBox.js')}}"></script>
<script src="{{url_for('static', filename='DataInspector.js')}}"></script>
<link href="https://gojs.net/latest/extensions/DataInspector.css" rel="stylesheet">
<link href="{{url_for('static', filename='DataInspector.css')}}" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script id="code">
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
// start everything in the middle of the viewport
initialContentAlignment: go.Spot.Center,
// have mouse wheel events zoom in and out instead of scroll up and down
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
// support double-click in background creating a new node
"clickCreatingTool.archetypeNodeData": { text: "new node" },
// enable undo & redo
"textEditingTool.defaultTextEditor": window.TextEditorSelectBox,
"undoManager.isEnabled": true,
"layout": new go.ForceDirectedLayout(),
"ModelChanged": function(e) {
console.log("Diagram model changed!");
if (e.change === go.ChangedEvent.Remove && e.modelChange === "linkDataArray") {
console.log("eee");
console.log(e);
var linkdata = e.oldValue;
console.log("linkdata");
console.log(linkdata);
var oldstr = linkdata.text;
console.log("oldstr");
console.log(oldstr);
if (!oldstr) return;
var choices = e.model.modelData.choices;
console.log("choices");
console.log(choices);
var idx = choices.indexOf(oldstr);
if (idx < 0) {
console.log("adding choice: " + oldstr);
var newchoices = Array.prototype.slice.call(choices);
newchoices.push(oldstr);
e.model.set(e.model.modelData, "choices", newchoices);
}
}
}
});
//myDiagram.model.set(myDiagram.model.modelData, "choices", JSON.parse('{{ link_choices | tojson | safe}}'));
//myDiagram.model.set(myDiagram.model.modelData, "choices", ["one", "two", "three"]);
console.log("myDiagram.model.modelData");
console.log(myDiagram.model.modelData);
console.log("myDiagram.model.modelData.choices");
console.log(myDiagram.model.modelData.choices);
// 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);
}
});
myDiagram.addDiagramListener("textEdited", function(e) {
console.log("Text is edited");
console.log(e);
//CHECK IF LINK,
//IF YES REMOVE THAT OPTION FROM LIST
});
myDiagram.addDiagramListener("SelectionDeleting", function(e) {
console.log("inside SelectionDeleting");
console.log(e);
//CHECK IF LINK,
//IF YES PUT THAT OPTION BACK IN OPTION LIST
});
// define the Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// define the node's outer shape, which will surround the TextBlock
$(go.Shape, "RoundedRectangle",
{
parameter1: 20, // the corner has a large radius
fill: $(go.Brush, "Linear", { 0: "rgb(254, 201, 0)", 1: "rgb(254, 162, 0)" }),
stroke: null,
portId: "", // this Shape is the Node's port, not the whole Node
fromLinkable: true, fromLinkableDuplicates: true,
toLinkable: true, toLinkableDuplicates: true,
cursor: "pointer"
}),
$(go.TextBlock,
{
font: "bold 11pt helvetica, bold arial, sans-serif",
editable: true, // editing the text automatically updates the model data
//textEditor: window.TextEditorRadioButtons, // defined in textEditorRadioButtons.js
// this specific TextBlock has its own choices:
textEditor: window.TextEditorRadioButtons,
//choices: JSON.parse('{{ choices | tojson | safe}}')
choices: nodeChoices
},
new go.Binding("text").makeTwoWay())
);
myDiagram.nodeTemplate.selectionAdornmentTemplate =
$(go.Adornment, "Spot",
$(go.Panel, "Auto",
$(go.Shape, { stroke: "dodgerblue", strokeWidth: 2, fill: null }),
$(go.Placeholder)
),
$(go.Panel, "Horizontal",
{ alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom },
$("Button",
{ click: editText }, // defined below, to support editing the text of the node
$(go.TextBlock, "t",
{ font: "bold 10pt sans-serif", desiredSize: new go.Size(15, 15), textAlign: "center" })
),
$("Button",
{ // drawLink is defined below, to support interactively drawing new links
click: drawLink, // click on Button and then click on target node
actionMove: drawLink // drag from Button to the target node
},
$(go.Shape,
{ geometryString: "M0 0 L8 0 8 12 14 12 M12 10 L14 12 12 14" })
),
$("Button",
{
actionMove: dragNewNode, // defined below, to support dragging from the button
_dragData: { text: "?????", color: "lightgray" }, // node data to copy
click: clickNewNode // defined below, to support a click on the button
},
$(go.Shape,
{ geometryString: "M0 0 L3 0 3 10 6 10 x F1 M6 6 L14 6 14 14 6 14z", fill: "gray" })
)
)
);
//myDiagram.model.set(myDiagram.model.modelData, "choices", JSON.parse('{{ link_choices | tojson | safe}}'));
//myDiagram.model.set(myDiagram.model.modelData, "choices", ["one", "two", "three"]);
console.log("myDiagram.model.modelData");
console.log(myDiagram.model.modelData);
console.log(myDiagram.model.modelData.choices);
function editText(e, button) {
//console.log(e);
var node = button.part.adornedPart;
console.log("node");
//console.log(node);
e.diagram.commandHandler.editTextBlock(node.findObject("TEXTBLOCK"));
//$("#nodeText").val(node.findObject("TEXTBLOCK"));
}
function drawLink(e, button) {
var node = button.part.adornedPart;
var tool = e.diagram.toolManager.linkingTool;
tool.startObject = node.port;
e.diagram.currentTool = tool;
tool.doActivate();
}
// used by both clickNewNode and dragNewNode to create a node and a link
// from a given node to the new node
function createNodeAndLink(data, fromnode) {
var diagram = fromnode.diagram;
var model = diagram.model;
var nodedata = model.copyNodeData(data);
model.addNodeData(nodedata);
var newnode = diagram.findNodeForData(nodedata);
var linkdata = model.copyLinkData({});
model.setFromKeyForLinkData(linkdata, model.getKeyForNodeData(fromnode.data));
model.setToKeyForLinkData(linkdata, model.getKeyForNodeData(newnode.data));
model.addLinkData(linkdata);
diagram.select(newnode);
return newnode;
}
// the Button.click event handler, called when the user clicks the "N" button
function clickNewNode(e, button) {
var data = button._dragData;
if (!data) return;
e.diagram.startTransaction("Create Node and Link");
var fromnode = button.part.adornedPart;
var newnode = createNodeAndLink(button._dragData, fromnode);
newnode.location = new go.Point(fromnode.location.x + 200, fromnode.location.y);
e.diagram.commitTransaction("Create Node and Link");
}
// the Button.actionMove event handler, called when the user drags within the "N" button
function dragNewNode(e, button) {
var tool = e.diagram.toolManager.draggingTool;
if (tool.isBeyondDragSize()) {
var data = button._dragData;
if (!data) return;
e.diagram.startTransaction("button drag"); // see doDeactivate, below
var newnode = createNodeAndLink(data, button.part.adornedPart);
newnode.location = e.diagram.lastInput.documentPoint;
// don't commitTransaction here, but in tool.doDeactivate, after drag operation finished
// set tool.currentPart to a selected movable Part and then activate the DraggingTool
tool.currentPart = newnode;
e.diagram.currentTool = tool;
tool.doActivate();
}
}
// using dragNewNode also requires modifying the standard DraggingTool so that it
// only calls commitTransaction when dragNewNode started a "button drag" transaction;
// do this by overriding DraggingTool.doDeactivate:
var tool = myDiagram.toolManager.draggingTool;
tool.doDeactivate = function() {
// commit "button drag" transaction, if it is ongoing; see dragNewNode, above
if (tool.diagram.undoManager.nestedTransactionNames.elt(0) === "button drag") {
tool.diagram.commitTransaction();
}
go.DraggingTool.prototype.doDeactivate.call(tool); // call the base method
};
// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{
curve: go.Link.Bezier,
adjusting: go.Link.Stretch,
reshapable: true,
relinkableFrom: true,
relinkableTo: true,
toShortLength: 3
},
new go.Binding("points").makeTwoWay(),
new go.Binding("curviness"),
$(go.Shape, // the link shape
{ strokeWidth: 1.5 }),
$(go.Shape, // the arrowhead
{ toArrow: "standard", stroke: null }),
$(go.Panel, "Auto",
$(go.Shape, // the label background, which becomes transparent around the edges
{
fill: $(go.Brush, "Radial", { 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 0)" }),
stroke: null
}),
$(go.TextBlock,
{
background: "white",
editable: true,
textEditor: window.TextEditorSelectBox, // defined in extensions/textEditorSelectBox.js
textEdited: function(tb, oldstr, newstr) {
var choices = tb.diagram.model.modelData.choices;
console.log("choices");
console.log(choices);
console.log("newstr");
console.log(newstr);
console.log("oldstr");
console.log(oldstr);
var idx = choices.indexOf(newstr);
if (idx >= 0 && oldstr !== newstr) {
console.log(choices);
console.log("choices");
console.log(choices);
console.log("removing choice " + idx + ": " + newstr);
var newchoices = Array.prototype.slice.call(choices);
newchoices.splice(idx, 1);
tb.diagram.model.set(tb.diagram.model.modelData, "choices", newchoices);
tb.editable = false; // don't allow choice again
}
}
},
// editing the text automatically updates the model data
//new go.Binding("text"),
new go.Binding("text").makeTwoWay(),
new go.Binding("choices").ofModel())
)
);
var inspector = new Inspector('myInspectorDiv', myDiagram,
{
// uncomment this line to only inspect the named properties below instead of all properties on each object:
// includesOwnProperties: false,
properties: {
"text": { },
// an example of specifying the type
"password": { show: Inspector.showIfPresent, type: 'password' },
// key would be automatically added for nodes, but we want to declare it read-only also:
"key": { readOnly: true, show: Inspector.showIfPresent },
// color would be automatically added for nodes, but we want to declare it a color also:
"color": { show: Inspector.showIfPresent, type: 'color' },
// Comments and LinkComments are not in any node or link data (yet), so we add them here:
"Comments": { show: Inspector.showIfNode },
"flag": { show: Inspector.showIfNode, type: 'checkbox' },
"LinkComments": { show: Inspector.showIfLink },
"isGroup": { readOnly: true, show: Inspector.showIfPresent }
}
});
// read in the JSON data from flask
loadGraphData();
}
function loadGraphData() {
var graphDataString = JSON.parse('{{ diagramData | tojson | safe}}');
//console.log("graphDataString");
//console.log(graphDataString);
myDiagram.model = go.Model.fromJson(graphDataString);
//myDiagram.model.set(myDiagram.model.modelData, "choices", ["one", "two", "three"]);
myDiagram.model.set(myDiagram.model.modelData, "choices", JSON.parse('{{ link_choices | tojson | safe}}'));
console.log("whole model");
console.log(myDiagram.model);
}
function saveGraphData(form, event) {
console.log("inside saveGraphData");
event.preventDefault();
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
form.submit();
}
function zoomToFit(){
console.log("inside zoomToFit");
myDiagram.zoomToRect(myDiagram.documentBounds);
}
function zoomIn(){
console.log("inside zoomIn");
myDiagram.commandHandler.increaseZoom();
}
function zoomOut(){
console.log("inside zoomOut");
myDiagram.commandHandler.decreaseZoom();
}
</script>
</head>
<body onload="init()">
<div id=formWrapper style="padding: 30px;">
<form method="POST" action="http://localhost:5000/updateResultFile" name="updateResultFileForm"
id="updateResultFileForm"
onsubmit="saveGraphData(this, event);">
<div id="graphWrapper" style="margin-bottom: 15px;">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 800px;margin-bottom: 15px;"></div>
<div style="display: none;"><input id="mySavedModel" name="mySavedModel"></div>
<button class="btn btn-default" type="submit"> Save <i class="fa fa-save"> </i> </button>
</div>
</form>
<div id="myInspectorDiv">
</div>
<div>
<button class="btn btn-default" onclick="zoomToFit()"> Zoom to fit <i class="fa fa-search"> </i> </button>
<button class="btn btn-default" onclick="zoomIn()"> Zoom in <i class="fa fa-search-plus"> </i> </button>
<button class="btn btn-default" onclick="zoomOut()"> Zoom out <i class="fa fa-search-minus"> </i> </button>
</div>
</div>
</body>
</html>
Now I want to make when the user used all options for links and none of them is available to use, I would like to disable availability to add a new link. Also if he uses all options for node texts, I would like to disable the possibility to add new nodes. But, if the user deletes some node and that option for node text is free again, I would like to enable him the possibility to add new nodes with that option but I don't have any idea how to make diagram disable his core part to add new nodes or links?
Also, it is somehow hard to make double click on link text to make dropdown with options appear. Is there any trick or option to make it easier to use?
I didn't realize you had asked this so many times.
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
initialContentAlignment: go.Spot.Center,
"undoManager.isEnabled": true,
"ModelChanged": function(e) { // just for demonstration purposes,
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
if (e.change === go.ChangedEvent.Remove) {
if (e.modelChange === "linkDataArray") {
var linkdata = e.oldValue;
var oldstr = linkdata.text;
if (!oldstr) return;
var linkchoices = e.model.modelData.linkchoices;
var idx = linkchoices.indexOf(oldstr);
if (idx < 0) {
console.log("deletion adding link choice: " + oldstr);
var newlinkchoices = Array.prototype.slice.call(linkchoices);
newlinkchoices.push(oldstr);
e.model.set(e.model.modelData, "linkchoices", newlinkchoices);
}
} else if (e.modelChange === "nodeDataArray") {
var nodedata = e.oldValue;
var oldstr = nodedata.text;
if (!oldstr) return;
var nodechoices = e.model.modelData.nodechoices;
var idx = nodechoices.indexOf(oldstr);
if (idx < 0) {
console.log("deletion adding node choice: " + oldstr);
var newnodechoices = Array.prototype.slice.call(nodechoices);
newnodechoices.push(oldstr);
e.model.set(e.model.modelData, "nodechoices", newnodechoices);
}
}
}
}
});
myDiagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape,
{ fill: "white", portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer" },
new go.Binding("fill", "color")),
$(go.TextBlock,
{
margin: 8,
editable: true,
textEditor: window.TextEditorSelectBox, // defined in extensions/textEditorSelectBox.js
textEdited: function(tb, oldstr, newstr) {
if (oldstr !== newstr) {
console.log("changing from " + oldstr + " to " + newstr);
var model = tb.diagram.model;
var nodechoices = model.modelData.nodechoices;
if (!nodechoices) {
nodechoices = [];
model.set(model.modelData, "nodechoices", nodechoices);
}
var idx = nodechoices.indexOf(newstr);
if (idx >= 0) {
console.log("removing node choice " + idx + ": " + newstr);
model.removeArrayItem(nodechoices, idx);
}
if (oldstr) {
console.log(" adding node choice " + oldstr);
model.addArrayItem(nodechoices, oldstr);
}
}
}
},
new go.Binding("text").makeTwoWay(),
new go.Binding("choices", "nodechoices").ofModel())
);
myDiagram.linkTemplate =
$(go.Link,
$(go.Shape),
$(go.Shape, { toArrow: "OpenTriangle" }),
$(go.TextBlock,
{
background: "white",
editable: true,
textEditor: window.TextEditorSelectBox, // defined in extensions/textEditorSelectBox.js
textEdited: function(tb, oldstr, newstr) {
if (oldstr !== newstr) {
console.log("changing from " + oldstr + " to " + newstr);
var model = tb.diagram.model;
var linkchoices = model.modelData.linkchoices;
if (!linkchoices) {
linkchoices = [];
model.set(model.modelData, "linkchoices", linkchoices);
}
var idx = linkchoices.indexOf(newstr);
if (idx >= 0) {
console.log("removing link choice " + idx + ": " + newstr);
model.removeArrayItem(linkchoices, idx);
}
if (oldstr) {
console.log(" adding link choice " + oldstr);
model.addArrayItem(linkchoices, oldstr);
}
}
}
},
new go.Binding("text").makeTwoWay(),
new go.Binding("choices", "linkchoices").ofModel())
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" }
],
[
{ from: 1, to: 2, text: "one" },
{ from: 1, to: 3, text: "two" },
{ from: 3, to: 4, text: "three" }
]);
myDiagram.model.set(myDiagram.model.modelData, "nodechoices", ["Epsilon"]);
myDiagram.model.set(myDiagram.model.modelData, "linkchoices", ["four"]);
}
I have 2 JS files.
1) The first one is main one and it's organized according to module pattern. It looks as following:
"use strict";
var SchemaEditor = (function () {
/* Private members */
var black = "black";
var transparent = "transparent";
var sd = {
mode: "pointer", // Set to default mode. Alternatives are "node" and "link", for adding a new node or a new link respectively.
itemType: "pointer", // Set when user clicks on a node or link button.
nodeCounter: {}
};
var myDiagram;
var $ = go.GraphObject.make;
/* Private methods */
// update the diagram every 250 milliseconds
function loop() {
setTimeout(function () { updateStates(); loop(); }, 250);
}
// update the value and appearance of each node according to its type and input values
function updateStates() {
var oldskip = myDiagram.skipsUndoManager;
myDiagram.skipsUndoManager = true;
// do all "input" nodes first
myDiagram.nodes.each(function (node) {
if (node.category === "input") {
doInput(node);
}
});
// now we can do all other kinds of nodes
myDiagram.nodes.each(function (node) {
switch (node.category) {
case "image1": doImage1(node); break;
case "image2": doImage2(node); break;
case "image3": doImage3(node); break;
case "image4": doImage4(node); break;
case "image5": doImage5(node); break;
case "image6": doImage6(node); break;
case "image7": doImage7(node); break;
case "image8": doImage8(node); break;
case "image9": doImage9(node); break;
case "image10": doImage10(node); break;
case "table": doTable(node); break;
case "hBar": dohBar(node); break;
}
});
myDiagram.skipsUndoManager = oldskip;
}
// update nodes by the specific function for its type
// determine the color of links coming out of this node based on those coming in and node type
function doImage1(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage2(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage3(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage4(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage5(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage6(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage7(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage8(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage9(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doImage10(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function doTable(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function dohBar(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function (link) { node.findObject("NODESHAPE1").fill = link.findObject("SHAPE").stroke; });
}
function BarLink() {
go.Link.call(this);
}
go.Diagram.inherit(BarLink, go.Link);
/** #override */
BarLink.prototype.getLinkPoint = function (node, port, spot, from, ortho, othernode, otherport) {
if (node.category === "hBar") {
var op = go.Link.prototype.getLinkPoint.call(this, othernode, otherport, this.computeSpot(!from), !from, ortho, node, port);
var r = new go.Rect(port.getDocumentPoint(go.Spot.TopLeft),
port.getDocumentPoint(go.Spot.BottomRight));
var y = (op.y > r.centerY) ? r.bottom : r.top;
if (op.x < r.left) return new go.Point(r.left, y);
if (op.x > r.right) return new go.Point(r.right, y);
return new go.Point(op.x, y);
} else {
return go.Link.prototype.getLinkPoint.call(this, node, port, spot, from, ortho, othernode, otherport);
}
};
/** #override */
BarLink.prototype.getLinkDirection = function (node, port, linkpoint, spot, from, ortho, othernode, otherport) {
var p = port.getDocumentPoint(go.Spot.Center);
var op = otherport.getDocumentPoint(go.Spot.Center);
var below = op.y > p.y;
return below ? 90 : 270;
};
// end BarLink class
var setMode = function (mode, itemType) {
myDiagram.startTransaction();
sd.mode = mode;
sd.itemType = itemType;
if (mode === "link") {
if (itemType === 'default') {
if (document.getElementById("defaultMode").classList.contains('non-pushed')) {
document.getElementById("defaultMode").classList.remove('non-pushed');
document.getElementById("defaultMode").classList.add('pushed');
document.getElementById("orthoMode").classList.add('non-pushed');
document.getElementById("cornerMode").classList.add('non-pushed');
}
}
if (itemType === 'ortho') {
if (document.getElementById("orthoMode").classList.contains('non-pushed')) {
document.getElementById("orthoMode").classList.remove('non-pushed');
document.getElementById("orthoMode").classList.add('pushed');
document.getElementById("defaultMode").classList.add('non-pushed');
document.getElementById("cornerMode").classList.add('non-pushed');
}
}
if (itemType === 'corner') {
if (document.getElementById("cornerMode").classList.contains('non-pushed')) {
document.getElementById("cornerMode").classList.remove('non-pushed');
document.getElementById("cornerMode").classList.add('pushed');
document.getElementById("defaultMode").classList.add('non-pushed');
document.getElementById("orthoMode").classList.add('non-pushed');
}
}
myDiagram.allowLink = true;
myDiagram.nodes.each(function (n) { n.port.cursor = "pointer"; });
}
myDiagram.commitTransaction("mode changed");
};
// save a model to and load a model from JSON text, displayed below the Diagram
var save = function () {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
};
var load = function () {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
};
function addLinkTemplateMaps() {
// creates relinkable Links that will avoid crossing Nodes when possible and will jump over other Links in their paths
myDiagram.linkTemplateMap.add("default",
$(go.Link,
{
relinkableFrom: true, relinkableTo: true,
selectionAdorned: false, // Links are not adorned when selected so that their color remains visible.
shadowOffset: new go.Point(0, 0), shadowBlur: 5, shadowColor: "black"
},
new go.Binding("isShadowed", "isSelected").ofObject(),
$(go.Shape,
{ name: "SHAPE", strokeWidth: 2, stroke: black })));
myDiagram.linkTemplateMap.add("corner",
$(go.Link,
{ reshapable: true, resegmentable: true, routing: go.Link.AvoidsNodes },
new go.Binding("points").makeTwoWay(), // TwoWay Binding of Link.points
$(go.Shape)
));
myDiagram.linkTemplateMap.add("ortho",
$(BarLink, // subclass defined below
{
routing: go.Link.Orthogonal,
relinkableFrom: true,
relinkableTo: true,
toPortChanged: function (link, oldport, newport) {
if (newport instanceof go.Shape) link.path.stroke = newport.fill;
}
},
$(go.Shape,
{ strokeWidth: 2 })
));
};
var initSchemaEditor = function () {
myDiagram = $(go.Diagram, "myDiagramDiv",
{
initialContentAlignment: go.Spot.Center,
allowDrop: true, // Nodes from the Palette can be dropped into the Diagram
"undoManager.isEnabled": true,
"grid.visible": true,
"linkingTool.portGravity": 0, // no snapping while drawing new links
"linkingTool.doActivate": function () {
// change the curve of the LinkingTool.temporaryLink
this.temporaryLink.curve = (sd.itemType === "default") ? go.Link.Normal : go.Link.Orthogonal;
go.LinkingTool.prototype.doActivate.call(this);
},
// override the link creation process
"linkingTool.insertLink": function (fromnode, fromport, tonode, toport) {
// to control what kind of Link is created,
// change the LinkingTool.archetypeLinkData's category
myDiagram.model.setCategoryForLinkData(this.archetypeLinkData, sd.itemType);
// also change the text indicating the condition, which the user can edit
this.archetypeLinkData.text = sd.itemType;
return go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport);
},
"clickCreatingTool.archetypeNodeData": {}, // enable ClickCreatingTool
"clickCreatingTool.isDoubleClick": false, // operates on a single click in background
"clickCreatingTool.canStart": function () { // but only in "node" creation mode
return sd.mode === "node" && go.ClickCreatingTool.prototype.canStart.call(this);
},
"clickCreatingTool.insertPart": function (loc) { // customize the data for the new node
sd.nodeCounter[sd.itemType] += 1;
var newNodeId = sd.itemType + sd.nodeCounter[sd.itemType];
this.archetypeNodeData = {
key: newNodeId,
category: sd.itemType,
label: newNodeId
};
return go.ClickCreatingTool.prototype.insertPart.call(this, loc);
}
});
myDiagram.requestUpdate();
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function (e) {
var button = document.getElementById("saveModel");
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);
}
});
myDiagram.model =
$(go.GraphLinksModel,
{
linkFromPortIdProperty: "fromPort", // required information:
linkToPortIdProperty: "toPort" // identifies data property names
});
addLinkTemplateMaps();
loadNodes();
myDiagram.nodeTemplateMap.add("image1", image1Template);
myDiagram.nodeTemplateMap.add("image2", image2Template);
myDiagram.nodeTemplateMap.add("image3", image3Template);
myDiagram.nodeTemplateMap.add("image4", image4Template);
myDiagram.nodeTemplateMap.add("image5", image5Template);
myDiagram.nodeTemplateMap.add("image6", image6Template);
myDiagram.nodeTemplateMap.add("image7", image7Template);
myDiagram.nodeTemplateMap.add("image8", image8Template);
myDiagram.nodeTemplateMap.add("image9", image9Template);
myDiagram.nodeTemplateMap.add("image10", image10Template);
myDiagram.nodeTemplateMap.add("table", tableTemplate);
myDiagram.nodeTemplateMap.add("hBar", hBarTemplate);
var palette = new go.Palette("palette"); // create a new Palette in the HTML DIV element "palette"
// share the template map with the Palette
palette.nodeTemplateMap = myDiagram.nodeTemplateMap;
//palette.groupTemplateMap = myDiagram.groupTemplateMap;
palette.model.nodeDataArray = [
{ category: "image1" },
{ category: "image2" },
{ category: "image3" },
{ category: "image4" },
{ category: "image5" },
{ category: "image6" },
{ category: "image7" },
{ category: "image8" },
{ category: "image9" },
{ category: "image10" },
{ category: "table" },
{ category: "hBar" }
];
myDiagram.addDiagramListener("ExternalObjectsDropped", function (e) {
if (myDiagram.currentTool instanceof go.TextEditingTool) {
myDiagram.currentTool.acceptText(go.TextEditingTool.LostFocus);
}
myDiagram.commandHandler.ungroupSelection();
jQuery.ajax({
type: "GET",
url: '/Home/GetRandomObjectProperties'
}).done(function (data) {
// loop through selection to find table node and populate properties
myDiagram.selection.each(function (p) {
if (p instanceof go.Node && p.category === "table") {
var nodedata = p.data;
var properties = data.map(function (item) {
return { "property_name": item.Item1.toString(), "property_value": item.Item2.toString() };
});
myDiagram.model.setDataProperty(nodedata, "properties", properties);
return;
}
});
});
});
// load the initial diagram
load();
// continually update the diagram
loop();
};
/* Public methods */
return {
initSchemaEditor: initSchemaEditor,
setMode: setMode,
save: save,
load: load
};
})();
2) The second JS file is not organized according to module pattern and it looks as following:
"use strict";
var sharedToolTip;
var image1Template;
var image2Template;
var image3Template;
var image4Template;
var image5Template;
var image6Template;
var image7Template;
var image8Template;
var image9Template;
var image10Template;
var tableTemplate;
var hBarTemplate;
function loadNodes() {
var $ = go.GraphObject.make;
// node template helpers
sharedToolTip =
$(go.Adornment, "Auto",
$(go.Shape, "RoundedRectangle", { fill: "lightyellow" }),
$(go.TextBlock, { margin: 2 },
new go.Binding("text", "", function (d) { return d.category; })));
// define some common property settings
function nodeStyle() {
return [
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("isShadowed", "isSelected").ofObject(),
{
selectionAdorned: false,
shadowOffset: new go.Point(0, 0),
shadowBlur: 15,
shadowColor: "blue",
toolTip: sharedToolTip
}
];
}
function portStyle0(input) {
return {
desiredSize: new go.Size(3, 3),
fill: "black",
fromLinkable: !input,
toLinkable: input,
cursor: "pointer"
};
}
function portStyle1() {
return {
desiredSize: new go.Size(3, 3),
fill: "black",
toLinkable: true,
cursor: "pointer",
fromLinkable: true,
fromSpot: go.Spot.TopBottomSides,
toSpot: go.Spot.TopBottomSides
};
}
image1Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/Cell_1.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.18, 0) })
);
image2Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/Cell_2.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.33, 0) })
);
image3Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/GTU.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.215, 0) })
);
image4Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle0(false),
{ portId: "1", alignment: new go.Spot(0.125, 0) }),
$(go.Picture, "Images/ElectricalElements/Sec_1.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "2", alignment: new go.Spot(0.125, 0) })
);
image5Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle0(true),
{ portId: "3", alignment: new go.Spot(0.523, 0) }),
$(go.Picture, "Images/ElectricalElements/Sec_2.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "4", alignment: new go.Spot(0.523, 0) })
);
image6Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle0(true),
{ portId: "", alignment: new go.Spot(0.12, 0) }),
$(go.Picture, "Images/ElectricalElements/Sec_3.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.12, 0) })
);
image7Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/Tr_1.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.42, 0) })
);
image8Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.59, 0) }),
$(go.Picture, "Images/ElectricalElements/Tr_2.svg")
);
image9Template =
$(go.Node, "Vertical", nodeStyle(),
{
resizable: true,
resizeObjectName: "SHAPE", selectionObjectName: "SHAPE"
},
$(go.Shape, "Rectangle",
{
name: "SHAPE",
fill: transparent,
width: 60,
height: 40,
stroke: black,
strokeWidth: 1,
strokeDashArray: [5, 5]
},
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify))
);
image10Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.TextBlock,
{
text: "text",
editable: true,
isMultiline: true
},
new go.Binding("text", "text").makeTwoWay())
);
tableTemplate =
$(go.Node, go.Panel.Auto, nodeStyle(),
$(go.Shape, { fill: "white", stroke: "gray", strokeWidth: 1 }),
{ movable: true },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Panel, "Table",
new go.Binding("itemArray", "properties"),
{
defaultAlignment: go.Spot.Left,
defaultColumnSeparatorStroke: "black",
itemTemplate:
$(go.Panel, "TableRow",
$(go.TextBlock, new go.Binding("text", "property_name"),
{ column: 0, margin: 1, font: "bold 7pt sans-serif" }),
$(go.TextBlock, new go.Binding("text", "property_value"),
{ column: 1, margin: 1 })
)
},
$(go.Panel, "TableRow",
{ isPanelMain: true },
$(go.TextBlock, "Name",
{ column: 0, margin: new go.Margin(1, 1, 0, 1), font: "bold 7pt sans-serif" }),
$(go.TextBlock, "Value",
{ column: 1, margin: new go.Margin(1, 1, 0, 1), font: "bold 7pt sans-serif" })
),
$(go.RowColumnDefinition,
{ row: 0, background: "lightgray" }),
$(go.RowColumnDefinition,
{ row: 1, separatorStroke: "black" })
)
);
hBarTemplate =
$(go.Node,
new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
{
layerName: "Background",
// special resizing: just at the ends
resizable: true, resizeObjectName: "SHAPE",
resizeAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // left resize handle
{
alignment: go.Spot.Left, cursor: "col-resize",
desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "dodgerblue"
}),
$(go.Shape, // right resize handle
{
alignment: go.Spot.Right, cursor: "col-resize",
desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "dodgerblue"
})),
rotatable: true
},
$(go.Shape, "Rectangle",
{
name: "SHAPE",
fill: "black", stroke: null, strokeWidth: 0,
width: 60, height: 5,
minSize: new go.Size(50, 5),
maxSize: new go.Size(Infinity, 5)
},
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
new go.Binding("fill"),
{ portId: "", toLinkable: true })
);
};
I call the function loadNodes from second JS file in main JS file and it works.
But, when I've tried to organize the second JS file according to module pattern it stopped working.
I've tried the folowing:
1) The main file:
...
NodeRepository.loadNodes();
myDiagram.nodeTemplateMap.add("image1", NodeRepository.image1Template);
myDiagram.nodeTemplateMap.add("image2", NodeRepository.image2Template);
myDiagram.nodeTemplateMap.add("image3", NodeRepository.image3Template);
myDiagram.nodeTemplateMap.add("image4", NodeRepository.image4Template);
myDiagram.nodeTemplateMap.add("image5", NodeRepository.image5Template);
myDiagram.nodeTemplateMap.add("image6", NodeRepository.image6Template);
myDiagram.nodeTemplateMap.add("image7", NodeRepository.image7Template);
myDiagram.nodeTemplateMap.add("image8", NodeRepository.image8Template);
myDiagram.nodeTemplateMap.add("image9", NodeRepository.image9Template);
myDiagram.nodeTemplateMap.add("image10", NodeRepository.image10Template);
myDiagram.nodeTemplateMap.add("table", NodeRepository.tableTemplate);
myDiagram.nodeTemplateMap.add("hBar", NodeRepository.hBarTemplate);
...
2) The second file:
"use strict";
var NodeRepository = (function () {
var sharedToolTip;
var image1Template;
var image2Template;
var image3Template;
var image4Template;
var image5Template;
var image6Template;
var image7Template;
var image8Template;
var image9Template;
var image10Template;
var tableTemplate;
var hBarTemplate;
function loadNodes() {
var $ = go.GraphObject.make;
...
};
return {
image1Template: image1Template,
image2Template: image2Template,
image3Template: image3Template,
image4Template: image4Template,
image5Template: image5Template,
image6Template: image6Template,
image7Template: image7Template,
image8Template: image8Template,
image9Template: image9Template,
image10Template: image10Template,
tableTemplate: tableTemplate,
hBarTemplate: hBarTemplate,
loadNodes: loadNodes
};
})();
What I'm doing wrong?
Here's what I did:
"use strict";
var NodeRepository = (function () {
var sharedToolTip;
var image1Template;
var image2Template;
var image3Template;
var image4Template;
var image5Template;
var image6Template;
var image7Template;
var image8Template;
var image9Template;
var image10Template;
var tableTemplate;
var hBarTemplate;
function image1Public() {
return image1Template;
}
function image2Public() {
return image2Template;
}
function image3Public() {
return image3Template;
}
function image4Public() {
return image4Template;
}
function image5Public() {
return image5Template;
}
function image6Public() {
return image6Template;
}
function image7Public() {
return image7Template;
}
function image8Public() {
return image8Template;
}
function image9Public() {
return image9Template;
}
function image10Public() {
return image10Template;
}
function image11Public() {
return tableTemplate;
}
function image12Public() {
return hBarTemplate;
}
function loadNodes() {
var $ = go.GraphObject.make;
// node template helpers
sharedToolTip =
$(go.Adornment, "Auto",
$(go.Shape, "RoundedRectangle", { fill: "lightyellow" }),
$(go.TextBlock, { margin: 2 },
new go.Binding("text", "", function (d) { return d.category; })));
// define some common property settings
function nodeStyle() {
return [
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("isShadowed", "isSelected").ofObject(),
{
selectionAdorned: false,
shadowOffset: new go.Point(0, 0),
shadowBlur: 15,
shadowColor: "blue",
toolTip: sharedToolTip
}
];
}
function portStyle0(input) {
return {
desiredSize: new go.Size(3, 3),
fill: "black",
fromLinkable: !input,
toLinkable: input,
cursor: "pointer"
};
}
function portStyle1() {
return {
desiredSize: new go.Size(3, 3),
fill: "black",
toLinkable: true,
cursor: "pointer",
fromLinkable: true,
fromSpot: go.Spot.TopBottomSides,
toSpot: go.Spot.TopBottomSides
};
}
image1Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/Cell_1.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.18, 0) })
);
image2Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/Cell_2.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.33, 0) })
);
image3Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/GTU.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.215, 0) })
);
image4Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle0(false),
{ portId: "1", alignment: new go.Spot(0.125, 0) }),
$(go.Picture, "Images/ElectricalElements/Sec_1.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "2", alignment: new go.Spot(0.125, 0) })
);
image5Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle0(true),
{ portId: "3", alignment: new go.Spot(0.523, 0) }),
$(go.Picture, "Images/ElectricalElements/Sec_2.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "4", alignment: new go.Spot(0.523, 0) })
);
image6Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle0(true),
{ portId: "", alignment: new go.Spot(0.12, 0) }),
$(go.Picture, "Images/ElectricalElements/Sec_3.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.12, 0) })
);
image7Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Picture, "Images/ElectricalElements/Tr_1.svg"),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.42, 0) })
);
image8Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.Shape, "Rectangle", portStyle1(),
{ portId: "", alignment: new go.Spot(0.59, 0) }),
$(go.Picture, "Images/ElectricalElements/Tr_2.svg")
);
image9Template =
$(go.Node, "Vertical", nodeStyle(),
{
resizable: true,
resizeObjectName: "SHAPE", selectionObjectName: "SHAPE"
},
$(go.Shape, "Rectangle",
{
name: "SHAPE",
fill: transparent,
width: 60,
height: 40,
stroke: black,
strokeWidth: 1,
strokeDashArray: [5, 5]
},
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify))
);
image10Template =
$(go.Node, "Vertical", nodeStyle(),
$(go.TextBlock,
{
text: "text",
editable: true,
isMultiline: true
},
new go.Binding("text", "text").makeTwoWay())
);
tableTemplate =
$(go.Node, go.Panel.Auto, nodeStyle(),
$(go.Shape, { fill: "white", stroke: "gray", strokeWidth: 1 }),
{ movable: true },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Panel, "Table",
new go.Binding("itemArray", "properties"),
{
defaultAlignment: go.Spot.Left,
defaultColumnSeparatorStroke: "black",
itemTemplate:
$(go.Panel, "TableRow",
$(go.TextBlock, new go.Binding("text", "property_name"),
{ column: 0, margin: 1, font: "bold 7pt sans-serif" }),
$(go.TextBlock, new go.Binding("text", "property_value"),
{ column: 1, margin: 1 })
)
},
$(go.Panel, "TableRow",
{ isPanelMain: true },
$(go.TextBlock, "Name",
{ column: 0, margin: new go.Margin(1, 1, 0, 1), font: "bold 7pt sans-serif" }),
$(go.TextBlock, "Value",
{ column: 1, margin: new go.Margin(1, 1, 0, 1), font: "bold 7pt sans-serif" })
),
$(go.RowColumnDefinition,
{ row: 0, background: "lightgray" }),
$(go.RowColumnDefinition,
{ row: 1, separatorStroke: "black" })
)
);
hBarTemplate =
$(go.Node,
new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
{
layerName: "Background",
// special resizing: just at the ends
resizable: true, resizeObjectName: "SHAPE",
resizeAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // left resize handle
{
alignment: go.Spot.Left, cursor: "col-resize",
desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "dodgerblue"
}),
$(go.Shape, // right resize handle
{
alignment: go.Spot.Right, cursor: "col-resize",
desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "dodgerblue"
})),
rotatable: true
},
$(go.Shape, "Rectangle",
{
name: "SHAPE",
fill: "black", stroke: null, strokeWidth: 0,
width: 60, height: 5,
minSize: new go.Size(50, 5),
maxSize: new go.Size(Infinity, 5)
},
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
new go.Binding("fill"),
{ portId: "", toLinkable: true })
);
};
return {
image1Public: image1Public,
image2Public: image2Public,
image3Public: image3Public,
image4Public: image4Public,
image5Public: image5Public,
image6Public: image6Public,
image7Public: image7Public,
image8Public: image8Public,
image9Public: image9Public,
image10Public: image10Public,
image11Public: image11Public,
image12Public: image12Public,
loadNodes: loadNodes
};
})();
I would like to ask, if anyone knows how to convert the orientation of the diagram of swimlanes in GOjs library from being top-level to a single row orientation, it is because I want to have a diagram that connects a node to another from a different group.
And also is it possible to add attributes to each node, like onmouseover, class, id, name, and etc.?
Currently I have this copied and paste code:
<!DOCTYPE html>
<html>
<head>
<title>Swimlane</title>
<!-- Copyright 1998-2014 by Northwoods Software Corporation. -->
<link href="goSamples.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="go.js"></script>
<script id="code">
// These parameters need to be set before defining the templates.
// this controls whether the swimlanes are horizontal stacked vertically, or the other way:
var HORIZONTAL = true;
// this controls the minimum length of any swimlane
var MINLENGTH = 200;
// this controls the minimum breadth of any swimlane
var MINBREADTH = 100;
// compute the minimum length needed to hold all of the subgraphs
function computeMinPlaceholderSize(diagram) {
var len = MINLENGTH;
for (var it = diagram.nodes; it.next(); ) {
var group = it.value;
if (!(group instanceof go.Group)) continue;
var holder = group.placeholder;
if (holder !== null) {
var sz = holder.actualBounds;
len = Math.max(len, (HORIZONTAL ? sz.width : sz.height));
}
}
return (HORIZONTAL ? new go.Size(len, NaN) : new go.Size(NaN, len));
}
// get the minimum placeholder size for a particular Group;
// when group is null, return the minimum size
function computePlaceholderSize(group) {
if (group instanceof go.Group) {
var holder = group.placeholder;
if (holder !== null) {
return holder.actualBounds.size;
}
}
return (HORIZONTAL ? new go.Size(MINLENGTH, MINBREADTH) : new go.Size(MINBREADTH, MINLENGTH));
}
// define a custom ResizingTool to limit how far one can shrink a Group
function GroupResizingTool() {
go.ResizingTool.call(this);
}
go.Diagram.inherit(GroupResizingTool, go.ResizingTool);
GroupResizingTool.prototype.isLengthening = function() {
return (this.handle.alignment === (HORIZONTAL ? go.Spot.Right : go.Spot.Bottom));
};
GroupResizingTool.prototype.computeMinSize = function() {
var msz = computePlaceholderSize(null); // get the minimum size
if (this.isLengthening()) { // compute the minimum length of all lanes
var sz = computeMinPlaceholderSize(this.diagram);
if (HORIZONTAL) {
msz.width = Math.max(msz.width, sz.width);
} else {
msz.height = Math.max(msz.height, sz.height);
}
} else { // find the minimum size of this single lane
var sz = computePlaceholderSize(this.adornedObject.part);
msz.width = Math.max(msz.width, sz.width);
msz.height = Math.max(msz.height, sz.height);
}
return msz;
};
GroupResizingTool.prototype.resize = function(newr) {
if (this.isLengthening()) { // changing the length of all of the lanes
for (var it = myDiagram.nodes; it.next(); ) {
var group = it.value;
if (!(group instanceof go.Group)) continue;
var shape = group.findObject("SHAPE");
if (shape !== null) { // set its desiredSize, but leave the other direction alone
if (HORIZONTAL) {
shape.width = newr.width;
} else {
shape.height = newr.height;
}
}
}
} else { // changing the breadth and length of a single lane
go.ResizingTool.prototype.resize.call(this, newr);
}
};
// end GroupResizingTool class
// define a custom grid layout that makes sure the length of each lane is the same
// and that each lane is broad enough to hold its subgraph
function StackLayout() {
go.GridLayout.call(this);
}
go.Diagram.inherit(StackLayout, go.GridLayout);
StackLayout.prototype.doLayout = function(coll) {
var diagram = this.diagram;
if (diagram === null) return;
diagram.startTransaction("StackLayout");
// make sure all of the Group Shapes are big enough
var minsize = computeMinPlaceholderSize(diagram);
for (var it = diagram.nodes; it.next(); ) {
var group = it.value;
if (!(group instanceof go.Group)) continue;
var shape = group.findObject("SHAPE");
if (shape !== null) { // change the desiredSize to be big enough in both directions
var sz = computePlaceholderSize(group);
if (HORIZONTAL) {
shape.width = (isNaN(shape.width) ? minsize.width : Math.max(shape.width, minsize.width));
if (!isNaN(shape.height)) shape.height = Math.max(shape.height, sz.height);
} else {
if (!isNaN(shape.width)) shape.width = Math.max(shape.width, sz.width);
shape.height = (isNaN(shape.height) ? minsize.height : Math.max(shape.height, minsize.height));
}
var cell = group.resizeCellSize;
if (!isNaN(shape.width) && !isNaN(cell.width) && cell.width > 0) shape.width = Math.ceil(shape.width / cell.width) * cell.width;
if (!isNaN(shape.height) && !isNaN(cell.height) && cell.height > 0) shape.height = Math.ceil(shape.height / cell.height) * cell.height;
}
}
// now do all of the usual stuff, according to whatever properties have been set on this GridLayout
go.GridLayout.prototype.doLayout.call(this, coll);
diagram.commitTransaction("StackLayout");
};
// end StackLayout class
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagram",
{
// use a custom ResizingTool (along with a custom ResizeAdornment on each Group)
resizingTool: new GroupResizingTool(),
// use a simple layout that ignores links to stack the top-level Groups on top of each other
layout:
$(StackLayout,
{
cellSize: new go.Size(1, 1),
spacing: new go.Size(0, 0),
wrappingColumn: (HORIZONTAL ? 1 : Infinity),
wrappingWidth: Infinity,
isViewportSized: false
}),
// don't allow dropping onto the diagram's background
mouseDrop: function(e) { e.diagram.currentTool.doCancel(); },
// a clipboard copied node is pasted into the original node's group (i.e. lane).
"commandHandler.copiesGroupKey": true
});
// When a Node has been moved, make sure all of the top-level Groups get laid out again in a stack
function relayoutDiagramStack(e) {
myDiagram.layout.invalidateLayout(); // but don't invalidate all Layouts that are in Groups
myDiagram.layoutDiagram();
}
myDiagram.addDiagramListener("SelectionMoved", relayoutDiagramStack);
myDiagram.addDiagramListener("SelectionCopied", relayoutDiagramStack);
// this is a Part.dragComputation function for limiting where a Node may be dragged
function stayInGroup(part, pt, gridpt) {
// don't constrain top-level nodes
var grp = part.containingGroup;
if (grp === null) return pt;
// try to stay within the background Shape of the Group
var back = grp.findObject("SHAPE");
if (back === null) return pt;
// allow dragging a Node out of a Group if the Shift key is down
if (part.diagram.lastInput.shift) return pt;
var b = part.actualBounds;
var p1 = back.getDocumentPoint(go.Spot.TopLeft);
var p2 = back.getDocumentPoint(go.Spot.BottomRight);
// find the padding inside the group's placeholder that is around the member parts
var m = grp.placeholder.padding;
// now limit the location appropriately
var x = Math.max(p1.x + m.left, Math.min(pt.x, p2.x - m.right - b.width - 1));
var y = Math.max(p1.y + m.top, Math.min(pt.y, p2.y - m.bottom - b.height - 1));
return new go.Point(x, y);
}
myDiagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape, "Rectangle",
{ fill: "white", portId: "", cursor: "pointer", fromLinkable: true, toLinkable: true }),
$(go.TextBlock, { margin: 5 },
new go.Binding("text", "key")),
// limit dragging of Nodes to stay within the containing Group, defined above
{
dragComputation: stayInGroup,
mouseDrop: function (e, node) { // dropping a copy of some Nodes and Links onto this Node adds them to this Node's Group
if (!e.shift && !e.control) return; // cannot change groups with an unmodified drag-and-drop
var grp = node.containingGroup;
if (grp !== null) {
var ok = grp.addMembers(node.diagram.selection, true);
if (!ok) grp.diagram.currentTool.doCancel();
}
},
layoutConditions: go.Part.LayoutAdded | go.Part.LayoutNodeSized
}
);
// each Group is a "swimlane" with a header on the left and a resizable lane on the right
myDiagram.groupTemplate =
$(go.Group, HORIZONTAL ? "Horizontal" : "Vertical",
{
movable: false, copyable: false, deletable: false, // can't move or copy or delete lanes
avoidable: false,
selectionObjectName: "SHAPE", // selecting a lane causes the body of the lane to be highlit, not the label
resizable: true, resizeObjectName: "SHAPE", // allow lanes to be resized, but the custom resizeAdornmentTemplate only permits one kind of resizing
layout: $(go.LayeredDigraphLayout, // automatically lay out the lane's subgraph
{ direction: HORIZONTAL ? 0 : 90, columnSpacing: 10, layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource }),
computesBoundsAfterDrag: true, // needed to prevent recomputing Group.placeholder bounds too soon
computesBoundsIncludingLinks: false,
computesBoundsIncludingLocation: true,
mouseDrop: function (e, grp) { // dropping a copy of some Nodes and Links onto this Group adds them to this Group
if (!e.shift && !e.control) return; // cannot change groups with an unmodified drag-and-drop
var ok = grp.addMembers(grp.diagram.selection, true);
if (!ok) grp.diagram.currentTool.doCancel();
}
},
// the lane header consisting of a Shape and a TextBlock
$(go.Panel, "Horizontal",
{ angle: HORIZONTAL ? 270 : 0, // maybe rotate the header to read sideways going up
alignment: go.Spot.Center },
$(go.Shape, "Diamond",
{ width: 8, height: 8 },
new go.Binding("fill", "color")),
$(go.TextBlock, // the lane label
{ font: "bold 16pt sans-serif" },
new go.Binding("text", "key"))
), // end Horizontal Panel
$(go.Panel, "Auto", // the lane consisting of a background Shape and a Placeholder representing the subgraph
$(go.Shape, "Rectangle",
{ name: "SHAPE", fill: "white", minSize: computePlaceholderSize(null) },
new go.Binding("fill", "color")),
$(go.Placeholder,
{ padding: 10, alignment: go.Spot.TopLeft })
) // end Auto Panel
); // end Group
// define a custom resize adornment that only has a single resize handle
myDiagram.groupTemplate.resizeAdornmentTemplate =
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // for changing the length of a lane
{
alignment: HORIZONTAL ? go.Spot.Right: go.Spot.Bottom,
desiredSize: HORIZONTAL ? new go.Size(7, 50) : new go.Size(50, 7),
fill: "lightblue", stroke: "dodgerblue",
cursor: HORIZONTAL ? "col-resize" : "row-resize"
}),
$(go.Shape, // for changing the breadth of a lane
{
alignment: HORIZONTAL ? go.Spot.Bottom : go.Spot.Right,
desiredSize: HORIZONTAL ? new go.Size(50, 7) : new go.Size(7, 50),
fill: "lightblue", stroke: "dodgerblue",
cursor: HORIZONTAL ? "row-resize" : "col-resize"
})
);
myDiagram.linkTemplate =
$(go.Link,
{ routing: go.Link.AvoidsNodes, corner: 5 },
{ relinkableFrom: true, relinkableTo: true },
$(go.Shape),
$(go.Shape, { toArrow: "Standard" }),
{
mouseDrop: function (e, link) { // dropping a copy of some Nodes and Links onto this Link adds them to this Link's Group
if (!e.shift && !e.control) return; // cannot change groups with an unmodified drag-and-drop
var grp = link.containingGroup;
if (grp !== null) {
var ok = grp.addMembers(link.diagram.selection, true);
if (!ok) grp.diagram.currentTool.doCancel();
}
},
layoutConditions: go.Part.LayoutAdded
}
);
// define some sample graphs in some of the lanes
myDiagram.model = new go.GraphLinksModel(
[ // node data
{ key: "Lane1", isGroup: true, color: "lightblue" },
{ key: "Lane2", isGroup: true, color: "lightgreen" },
{ key: "Lane3", isGroup: true, color: "lightyellow" },
{ key: "Lane4", isGroup: true, color: "orange" },
{ key: "oneA", group: "Lane1" },
{ key: "oneB", group: "Lane1" },
{ key: "oneC", group: "Lane1" },
{ key: "oneD", group: "Lane1" },
{ key: "twoA", group: "Lane2" },
{ key: "twoB", group: "Lane2" },
{ key: "twoC", group: "Lane2" },
{ key: "twoD", group: "Lane2" },
{ key: "twoE", group: "Lane2" },
{ key: "twoF", group: "Lane2" },
{ key: "twoG", group: "Lane2" },
{ key: "fourA", group: "Lane4" },
{ key: "fourB", group: "Lane4" },
{ key: "fourC", group: "Lane4" },
{ key: "fourD", group: "Lane4" },
],
[ // link data
{ from: "oneA", to: "oneB" },
{ from: "oneA", to: "oneC" },
{ from: "oneB", to: "oneD" },
{ from: "oneC", to: "oneD" },
{ from: "twoA", to: "twoB" },
{ from: "twoA", to: "twoC" },
{ from: "twoA", to: "twoF" },
{ from: "twoB", to: "twoD" },
{ from: "twoC", to: "twoD" },
{ from: "twoD", to: "twoG" },
{ from: "twoE", to: "twoG" },
{ from: "twoF", to: "twoG" },
{ from: "fourA", to: "fourB" },
{ from: "fourB", to: "fourC" },
{ from: "fourC", to: "fourD" }
]);
myDiagram.model.undoManager.isEnabled = true;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagram" style="border: solid 1px blue; width:100%; height:600px;"></div>
</div>
</body>
</html>
I've tried setting
var HORIZONTAL = true;
to
var HORIZONTAL = false;
I may have the correct orientation though, but the flow is changed. I wonder if the column could just be horizontal and the flow runs like the top-level view.
The "flow" is changed because setting HORIZONTAL = false also changes the Group's layout that it users for its members:
myDiagram.groupTemplate =
$(go.Group, HORIZONTAL ? "Horizontal" : "Vertical",
{
...
layout: $(go.LayeredDigraphLayout, // automatically lay out the lane's subgraph
{ direction: HORIZONTAL ? 0 : 90, columnSpacing: 10, layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource }),
So you're going to have to modify that direction: HORIZONTAL ? 0 : 90 to your liking if you don't want it to change when HORIZONTAL does.
It is not possible to add HTML attributes to each node, but you can store any arbitrary data for each node in the node data itself.