/
flowBuilder.html
381 lines (354 loc) · 16.3 KB
/
flowBuilder.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
<!DOCTYPE html>
<html>
<head>
<title>Flow Builder</title>
<meta name="description" content="An editor of flow diagrams that supports deletion by dropping onto a particular node and relinking by dragging a link." />
<!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" /> <!-- you don't need to use this -->
<script src="goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagram",
{
allowCopy: false,
initialContentAlignment: go.Spot.Center,
layout:
$(go.LayeredDigraphLayout,
{
setsPortSpots: false, // Links already know their fromSpot and toSpot
columnSpacing: 5,
isInitial: false,
isOngoing: false
}),
validCycle: go.Diagram.CycleNotDirected,
"undoManager.isEnabled": true
});
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
var graygrad = $(go.Brush, "Linear",
{ 0: "white", 0.1: "whitesmoke", 0.9: "whitesmoke", 1: "lightgray" });
myDiagram.nodeTemplate = // the default node template
$(go.Node, "Spot",
{ selectionAdorned: false, textEditable: true, locationObjectName: "BODY" },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// the main body consists of a Rectangle surrounding the text
$(go.Panel, "Auto",
{ name: "BODY" },
$(go.Shape, "Rectangle",
{ fill: graygrad, stroke: "gray", minSize: new go.Size(120, 21) },
new go.Binding("fill", "isSelected", function(s) { return s ? "dodgerblue" : graygrad; }).ofObject()),
$(go.TextBlock,
{ stroke: "black", font: "12px sans-serif", editable: true,
margin: new go.Margin(3, 3+11, 3, 3+4), alignment: go.Spot.Left },
new go.Binding("text", "text"))
),
// output port
$(go.Panel, "Auto",
{ alignment: go.Spot.Right, portId: "from", fromLinkable: true, cursor: "pointer", click: addNodeAndLink },
$(go.Shape, "Circle",
{ width: 22, height: 22, fill: "white", stroke: "dodgerblue", strokeWidth: 3 }),
$(go.Shape, "PlusLine",
{ width: 11, height: 11, fill: null, stroke: "dodgerblue", strokeWidth: 3 })
),
// input port
$(go.Panel, "Auto",
{ alignment: go.Spot.Left, portId: "to", toLinkable: true },
$(go.Shape, "Circle",
{ width: 8, height: 8, fill: "white", stroke: "gray" }),
$(go.Shape, "Circle",
{ width: 4, height: 4, fill: "dodgerblue", stroke: null })
)
);
myDiagram.nodeTemplate.contextMenu =
$(go.Adornment, "Vertical",
$("ContextMenuButton",
$(go.TextBlock, "Rename"),
{ click: function(e, obj) { e.diagram.commandHandler.editTextBlock(); } },
new go.Binding("visible", "", function(o) { return o.diagram.commandHandler.canEditTextBlock(); }).ofObject()),
// add one for Editing...
$("ContextMenuButton",
$(go.TextBlock, "Delete"),
{ click: function(e, obj) { e.diagram.commandHandler.deleteSelection(); } },
new go.Binding("visible", "", function(o) { return o.diagram.commandHandler.canDeleteSelection(); }).ofObject())
);
myDiagram.nodeTemplateMap.add("Loading",
$(go.Node, "Spot",
{ selectionAdorned: false, textEditable: true, locationObjectName: "BODY" },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// the main body consists of a Rectangle surrounding the text
$(go.Panel, "Auto",
{ name: "BODY" },
$(go.Shape, "Rectangle",
{ fill: graygrad, stroke: "gray", minSize: new go.Size(120, 21) },
new go.Binding("fill", "isSelected", function(s) { return s ? "dodgerblue" : graygrad; }).ofObject()),
$(go.TextBlock,
{ stroke: "black", font: "12px sans-serif", editable: true,
margin: new go.Margin(3, 3+11, 3, 3+4), alignment: go.Spot.Left },
new go.Binding("text", "text"))
),
// output port
$(go.Panel, "Auto",
{ alignment: go.Spot.Right, portId: "from", fromLinkable: true, click: addNodeAndLink },
$(go.Shape, "Circle",
{ width: 22, height: 22, fill: "white", stroke: "dodgerblue", strokeWidth: 3 }),
$(go.Shape, "PlusLine",
{ width: 11, height: 11, fill: null, stroke: "dodgerblue", strokeWidth: 3 })
)
));
myDiagram.nodeTemplateMap.add("End",
$(go.Node, "Spot",
{ selectionAdorned: false, textEditable: true, locationObjectName: "BODY" },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// the main body consists of a Rectangle surrounding the text
$(go.Panel, "Auto",
{ name: "BODY" },
$(go.Shape, "Rectangle",
{ fill: graygrad, stroke: "gray", minSize: new go.Size(120, 21) },
new go.Binding("fill", "isSelected", function(s) { return s ? "dodgerblue" : graygrad; }).ofObject()),
$(go.TextBlock,
{ stroke: "black", font: "12px sans-serif", editable: true,
margin: new go.Margin(3, 3 + 11, 3, 3 + 4), alignment: go.Spot.Left },
new go.Binding("text", "text"))
),
// input port
$(go.Panel, "Auto",
{ alignment: go.Spot.Left, portId: "to", toLinkable: true },
$(go.Shape, "Circle",
{ width: 8, height: 8, fill: "white", stroke: "gray" }),
$(go.Shape, "Circle",
{ width: 4, height: 4, fill: "dodgerblue", stroke: null })
)
));
// dropping a node on this special node will cause the selection to be deleted;
// linking or relinking to this special node will cause the link to be deleted
myDiagram.nodeTemplateMap.add("Recycle",
$(go.Node, "Auto",
{ portId: "to", toLinkable: true, deletable: false,
layerName: "Background", locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{ dragComputation: function(node, pt, gridpt) { return pt; } },
{ mouseDrop: function(e, obj) { myDiagram.commandHandler.deleteSelection(); } },
$(go.Shape,
{ fill: "lightgray", stroke: "gray" }),
$(go.TextBlock, "Drop Here\nTo Delete",
{ margin: 5, textAlign: "center" })
));
// this is a click event handler that adds a node and a link to the diagram,
// connecting with the node on which the click occurred
function addNodeAndLink(e, obj) {
var fromNode = obj.part;
var diagram = fromNode.diagram;
diagram.startTransaction("Add State");
// get the node data for which the user clicked the button
var fromData = fromNode.data;
// create a new "State" data object, positioned off to the right of the fromNode
var p = fromNode.location.copy();
p.x += diagram.toolManager.draggingTool.gridSnapCellSize.width;
var toData = {
text: "new",
loc: go.Point.stringify(p)
};
// add the new node data to the model
var model = diagram.model;
model.addNodeData(toData);
// create a link data from the old node data to the new node data
var linkdata = {
from: model.getKeyForNodeData(fromData),
to: model.getKeyForNodeData(toData)
};
// and add the link data to the model
model.addLinkData(linkdata);
// select the new Node
var newnode = diagram.findNodeForData(toData);
diagram.select(newnode);
// snap the new node to a valid location
newnode.location = diagram.toolManager.draggingTool.computeMove(newnode, p);
// then account for any overlap
shiftNodesToEmptySpaces();
diagram.commitTransaction("Add State");
}
// Highlight ports when they are targets for linking or relinking.
var OldTarget = null; // remember the last highlit port
function highlight(port) {
if (OldTarget !== port) {
lowlight(); // remove highlight from any old port
OldTarget = port;
port.scale = 1.3; // highlight by enlarging
}
}
function lowlight() { // remove any highlight
if (OldTarget) {
OldTarget.scale = 1.0;
OldTarget = null;
}
}
// Connecting a link with the Recycle node removes the link
myDiagram.addDiagramListener("LinkDrawn", function(e) {
var link = e.subject;
if (link.toNode.category === "Recycle") myDiagram.remove(link);
lowlight();
});
myDiagram.addDiagramListener("LinkRelinked", function(e) {
var link = e.subject;
if (link.toNode.category === "Recycle") myDiagram.remove(link);
lowlight();
});
myDiagram.linkTemplate =
$(go.Link,
{ selectionAdorned: false, fromPortId: "from", toPortId: "to", relinkableTo: true },
$(go.Shape,
{ stroke: "gray", strokeWidth: 2 },
{ mouseEnter: function(e, obj) { obj.strokeWidth = 5; obj.stroke = "dodgerblue"; },
mouseLeave: function(e, obj) { obj.strokeWidth = 2; obj.stroke = "gray"; } })
);
function commonLinkingToolInit(tool) {
// the temporary link drawn during a link drawing operation (LinkingTool) is thick and blue
tool.temporaryLink =
$(go.Link, { layerName: "Tool" },
$(go.Shape, { stroke: "dodgerblue", strokeWidth: 5 }));
// change the standard proposed ports feedback from blue rectangles to transparent circles
tool.temporaryFromPort.figure = "Circle";
tool.temporaryFromPort.stroke = null;
tool.temporaryFromPort.strokeWidth = 0;
tool.temporaryToPort.figure = "Circle";
tool.temporaryToPort.stroke = null;
tool.temporaryToPort.strokeWidth = 0;
// provide customized visual feedback as ports are targeted or not
tool.portTargeted = function(realnode, realport, tempnode, tempport, toend) {
if (realport === null) { // no valid port nearby
lowlight();
} else if (toend) {
highlight(realport);
}
};
}
var ltool = myDiagram.toolManager.linkingTool;
commonLinkingToolInit(ltool);
// do not allow links to be drawn starting at the "to" port
ltool.direction = go.LinkingTool.ForwardsOnly;
var rtool = myDiagram.toolManager.relinkingTool;
commonLinkingToolInit(rtool);
// change the standard relink handle to be a shape that takes the shape of the link
rtool.toHandleArchetype =
$(go.Shape,
{ isPanelMain: true, fill: null, stroke: "dodgerblue", strokeWidth: 5 });
// use a special DraggingTool to cause the dragging of a Link to start relinking it
myDiagram.toolManager.draggingTool = new DragLinkingTool();
// detect when dropped onto an occupied cell
myDiagram.addDiagramListener("SelectionMoved", shiftNodesToEmptySpaces);
function shiftNodesToEmptySpaces() {
myDiagram.selection.each(function(node) {
if (!(node instanceof go.Node)) return;
// look for Parts overlapping the node
while (true) {
var exist = myDiagram.findObjectsIn(node.actualBounds,
// only consider Parts
function(obj) { return obj.part; },
// ignore Links and the dropped node itself
function(part) { return part instanceof go.Node && part !== node; },
// check for any overlap, not complete containment
true).first();
if (exist === null) break;
// try shifting down beyond the existing node to see if there's empty space
node.position = new go.Point(node.actualBounds.x, exist.actualBounds.bottom+10);
}
});
}
// prevent nodes from being dragged to the left of where the layout placed them
myDiagram.addDiagramListener("LayoutCompleted", function(e) {
myDiagram.nodes.each(function(node) {
if (node.category === "Recycle") return;
node.minLocation = new go.Point(node.location.x, -Infinity);
});
});
load(); // load initial diagram from the mySavedModel textarea
layout();
}
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
function layout() {
myDiagram.layoutDiagram(true);
}
// Define a custom tool that changes a drag operation on a Link to a relinking operation,
// but that operates like a normal DraggingTool otherwise.
function DragLinkingTool() {
go.DraggingTool.call(this);
this.isGridSnapEnabled = true;
this.isGridSnapRealtime = false;
this.gridSnapCellSize = new go.Size(182, 1);
this.gridSnapOrigin = new go.Point(5.5, 0);
}
go.Diagram.inherit(DragLinkingTool, go.DraggingTool);
// Handle dragging a link specially -- by starting the RelinkingTool on that Link
/** @override */
DragLinkingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this.standardMouseSelect();
var main = this.currentPart; // this is set by the standardMouseSelect
if (main instanceof go.Link) { // maybe start relinking instead of dragging
var relinkingtool = diagram.toolManager.relinkingTool;
// tell the RelinkingTool to work on this Link, not what is under the mouse
relinkingtool.originalLink = main;
// start the RelinkingTool
diagram.currentTool = relinkingtool;
// can activate it right now, because it already has the originalLink to reconnect
relinkingtool.doActivate();
relinkingtool.doMouseMove();
} else {
go.DraggingTool.prototype.doActivate.call(this);
}
};
// end DragLinkingTool
</script>
</head>
<body onload="init()">
<div id="sample">
Flow Builder
<div id="myDiagram" style="border: solid 1px black; width:100%; height:500px"></div>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
<button onclick="layout()">Do Layout</button>
<br />
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{ "key":1, "text":"Loading Screen", "category":"Loading" },
{ "key":2, "text":"Beginning" },
{ "key":3, "text":"Segment 1" },
{ "key":4, "text":"Segment 2" },
{ "key":5, "text":"Segment 3"},
{ "key":6, "text":"End Screen", "category":"End" },
{ "key":-2, "category": "Recycle" }
],
"linkDataArray": [
{ "from":1, "to":2 },
{ "from":2, "to":3 },
{ "from":2, "to":5 },
{ "from":3, "to":4 },
{ "from":4, "to":6 }
]
}
</textarea>
</div>
</body>
</html>