@dualbox/editor 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/js/src/GraphEditor.js +159 -0
  2. package/js/src/c/GraphController.js +646 -0
  3. package/js/src/libs/CodeMirror.js +8 -0
  4. package/js/src/libs/fontawesome.js +1 -0
  5. package/js/src/libs/jsoneditor.css +2 -0
  6. package/js/src/libs/jsoneditor.js +4 -0
  7. package/js/src/m/DualboxUtils.js +35 -0
  8. package/js/src/m/GraphModel.js +2167 -0
  9. package/js/src/m/History.js +94 -0
  10. package/js/src/m/Merger.js +357 -0
  11. package/js/src/v/AppManager.js +61 -0
  12. package/js/src/v/CanvasSizeHandler.js +136 -0
  13. package/js/src/v/ContextMenu.css +45 -0
  14. package/js/src/v/ContextMenu.js +239 -0
  15. package/js/src/v/GraphView.js +928 -0
  16. package/js/src/v/PlumbStyle.js +254 -0
  17. package/js/src/v/Selector.js +239 -0
  18. package/js/src/v/TemplateManager.js +79 -0
  19. package/js/src/v/Translater.js +174 -0
  20. package/js/src/v/Utils.js +7 -0
  21. package/js/src/v/Zoomer.js +201 -0
  22. package/js/src/v/templates/addNode.css +45 -0
  23. package/js/src/v/templates/addNode.html +62 -0
  24. package/js/src/v/templates/addNode.js +34 -0
  25. package/js/src/v/templates/debugNodeInfos.css +5 -0
  26. package/js/src/v/templates/debugNodeInfos.html +336 -0
  27. package/js/src/v/templates/debugNodeInfos.js +31 -0
  28. package/js/src/v/templates/editMainSettings.css +67 -0
  29. package/js/src/v/templates/editMainSettings.html +265 -0
  30. package/js/src/v/templates/editMainSettings.js +240 -0
  31. package/js/src/v/templates/editNodeSettings.css +86 -0
  32. package/js/src/v/templates/editNodeSettings.html +539 -0
  33. package/js/src/v/templates/editNodeSettings.js +356 -0
  34. package/js/src/v/templates/graphNode.css +333 -0
  35. package/js/src/v/templates/graphNode.html +227 -0
  36. package/js/src/v/templates/graphNode.js +412 -0
  37. package/js/src/v/templates/main.css +353 -0
  38. package/js/src/v/templates/main.html +149 -0
  39. package/js/src/v/templates/main.js +511 -0
  40. package/js/src/v/templates/searchResults.css +50 -0
  41. package/js/src/v/templates/searchResults.html +46 -0
  42. package/js/src/v/templates/searchResults.js +176 -0
  43. package/package.json +3 -2
@@ -0,0 +1,928 @@
1
+ /**
2
+ * Implementing the View of the GraphEditor
3
+ * Everything related to display should be here
4
+ */
5
+
6
+ const _ = require('lodash');
7
+ const swal = require('sweetalert2');
8
+ const PlumbStyle = require('./PlumbStyle');
9
+
10
+ // Manage the start/stop lifetime of the application
11
+ const AppManager = require('./AppManager');
12
+
13
+ // for instanciation and adding templates to DOM
14
+ const TemplateManager = require('./TemplateManager');
15
+
16
+ // Tools for the graph
17
+ const Zoomer = require('./Zoomer');
18
+ const Selector = require('./Selector');
19
+ const Translater = require('./Translater');
20
+ const CanvasSizeHandler = require('./CanvasSizeHandler');
21
+ const ContextMenu = require('./ContextMenu');
22
+ const Utils = require('./Utils');
23
+ const dbUtils = require('../m/DualboxUtils');
24
+
25
+ class GraphView {
26
+ constructor(editor, div, attrs) {
27
+ this.e = editor;
28
+ this.m = editor.m;
29
+ this.c = editor.c;
30
+
31
+ this.utils = Utils;
32
+
33
+ this.div = $(div);
34
+ this.canvas = null;
35
+ this.attrs = attrs;
36
+
37
+ // Manage the different HTML templates
38
+ this.templateMgr = new TemplateManager(this);
39
+
40
+ // jsplumb
41
+ this.jsPlumbInstance = null; // main canvas graph
42
+
43
+ // encapsulate the JSPlumb styles
44
+ this.style = new PlumbStyle(this);
45
+
46
+ // Controls for different views
47
+ this.showEvents = false;
48
+
49
+ // Controls helpers utilities
50
+ this.selector = null;
51
+ this.translater = null;
52
+ this.zoomer = null;
53
+ this.canvasSizeHandler = null;
54
+
55
+ // Application manager
56
+ this.appManager = null;
57
+
58
+ // Code mirror boxes
59
+ this.htmlCode = null;
60
+ this.cssCode = null;
61
+
62
+ // load the main template
63
+ this.initialize();
64
+ }
65
+
66
+ hideCanvas() {
67
+ this.canvas.css('visibility', 'hidden');
68
+ return new Promise((resolve) => {
69
+ this.canvas.ready(resolve);
70
+ });
71
+ }
72
+
73
+ showCanvas() {
74
+ this.canvas.css('visibility', 'visible');
75
+ return new Promise( (resolve) => {
76
+ this.canvas.ready(resolve);
77
+ });
78
+ }
79
+
80
+ initialize() {
81
+ // add main template
82
+ this.templateMgr.appendTemplate(this.div, "main", {showLoadSaveButtons: this.attrs.showLoadSaveButtons}, () => {
83
+ // for convenience, define thoses once the main div is ready
84
+ this.div.ready(() => {
85
+ this.graphContainer = this.div.find('.dualbox-graph-container');
86
+ this.canvas = this.div.find('.dualbox-graph-canvas');
87
+ this.selector = new Selector(this, this.graphContainer);
88
+ this.translater = new Translater(this, this.graphContainer, this.canvas);
89
+ this.zoomer = new Zoomer(this, this.graphContainer, this.canvas);
90
+ this.jsPlumbInstance = jsPlumb.getInstance({
91
+ DragOptions: { cursor: 'pointer', zIndex: 2500 }, // default drag options
92
+ Container: "dualbox-graph-canvas"
93
+ });
94
+ this.style.setDefault();
95
+ this.initializeListeners();
96
+ this.setNavigation();
97
+ this.canvasSizeHandler = new CanvasSizeHandler(this, this.canvas);
98
+ this.appManager = new AppManager(this, this.div.find('.application') );
99
+ });
100
+ });
101
+
102
+ this.templateMgr.appendTemplate(this.div, "addNode"); // "add node" modal
103
+ }
104
+
105
+ // bind jsplumb events
106
+ initializeListeners() {
107
+ this.selector.initialize();
108
+ this.zoomer.initialize();
109
+ ContextMenu.initialize(); // important to make this after the translater (right-clic conflicts)
110
+ this.translater.initialize();
111
+
112
+ // show selected nodes
113
+ this.selector.onSelecting((divs) => {
114
+ $(this.canvas).find('.card').removeClass('selected');
115
+ _.each(divs, (div) => {
116
+ $(div).addClass('selected');
117
+ });
118
+ });
119
+
120
+ // Add every node in the selected area to JsPlumb selection
121
+ this.selector.onSelect((divs) => {
122
+ _.each(divs, (div) => {
123
+ $(div).addClass('selected');
124
+ this.jsPlumbInstance.addToDragSelection($(div)[0]);
125
+ });
126
+ });
127
+
128
+ // remove the selection
129
+ this.selector.onDeselect((divs) => {
130
+ _.each(divs, (div) => {
131
+ $(div).removeClass('selected');
132
+ this.jsPlumbInstance.removeFromDragSelection($(div)[0]);
133
+ });
134
+ });
135
+
136
+ this.jsPlumbInstance.bind("connectionDrag", (connection) => {
137
+ // hide the type of the source (output endpoint)
138
+ var endpoints = connection.getAttachedElements();
139
+ if( endpoints && endpoints[0] ) this.style.hideType(endpoints[0]);
140
+
141
+ // if the connection linked a source to a target, remove the connection
142
+ var c = connection.getParameters();
143
+ if( c.target ) {
144
+ if( c.type === "data" ) {
145
+ // remove the link
146
+ this.m.removeDataLink(c.source.id, c.source.output, c.target.id, c.target.input);
147
+ }
148
+ else if( c.type === "event" ) {
149
+ this.m.removeEventLink(c.source.id, c.target.id, c.event);
150
+ this.setBoxMenu(c.source.id);
151
+ }
152
+ }
153
+ });
154
+
155
+ this.jsPlumbInstance.bind("connectionDragStop", (connection) => {
156
+ // if the connection has not been made, show back the type of the source
157
+ var endpoints = connection.getAttachedElements();
158
+ if( endpoints && endpoints.length == 2 ) {
159
+ var sourceEP = endpoints[0];
160
+ var targetEP = endpoints[1];
161
+ if( targetEP && targetEP.elementId.startsWith('jsPlumb')) {
162
+ if( targetEP.elementId.startsWith('jsPlumb') ) {
163
+ // no connection has been made, show back our output type
164
+ var sourceOutput = connection.getParameters().source.output;
165
+ if( !this.m.getNode(sourceEP.elementId).isOutputConnected(sourceOutput) ) {
166
+ this.style.showType(sourceEP);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ });
172
+
173
+ this.jsPlumbInstance.bind("connection", (info, event) => {
174
+ // only do this for connection established by user, no programmatically
175
+ if( event ) {
176
+ var c = info.connection.getParameters();
177
+ if( c.type === "data" ) {
178
+ // check the types
179
+ var sourceType = this.m.getNode(c.source.id).getOutputType(c.source.output);
180
+ var targetType = this.m.getNode(c.target.id).getInputType(c.target.input);
181
+ if( this.m.compareTypes(sourceType, targetType) ) {
182
+ this.m.addDataLink(c.source.id, c.source.output, c.target.id, c.target.input);
183
+
184
+ // hide source and target types
185
+ this.style.hideType(info.sourceEndpoint);
186
+ this.style.hideType(info.targetEndpoint);
187
+ }
188
+ else {
189
+ // Error
190
+ this.jsPlumbInstance.detach(info.connection, {
191
+ fireEvent: false, //fire a connection detached event?
192
+ forceDetach: false //override any beforeDetach listeners
193
+ });
194
+ this.style.showType(info.targetEndpoint);
195
+ if( !this.m.getNode(info.sourceId).isOutputConnected(c.source.output) ) {
196
+ this.style.showType(info.sourceEndpoint);
197
+ }
198
+
199
+ swal({
200
+ title: "Nope.",
201
+ html: "<div style='text-align: left;'>Output <b style='font-weight: bold;'>" + c.source.output + "</b> of <b style='font-weight: bold;'>" + info.sourceId + "</b> is of type <u>" + Utils.htmlentities(sourceType) + "</u><br/>Input <b style='font-weight: bold;'>" + c.target.input + "</b> of <b style='font-weight: bold;'>" + info.targetId + "</b> is of type <u>" + Utils.htmlentities(targetType) + "</u>.</div><br/>Type mismatch.",
202
+ type: "error",
203
+ });
204
+ }
205
+ }
206
+ else if( c.type === "event" ) {
207
+ var target = this.m.getNode(c.target.id);
208
+ var eventNames = target.getEventsNames();
209
+ var options = {};
210
+ _.each(eventNames, (name) => options[name] = name );
211
+
212
+ swal({
213
+ input: 'select',
214
+ title: 'Select an event',
215
+ inputOptions: options
216
+ }).then( (result) => {
217
+ if( result.value ) {
218
+ info.connection.setParameter('event', result.value);
219
+ info.connection.setLabel({ label: result.value, cssClass: "connection-label noselect", location: 0.5 });
220
+ this.m.addEventLink(c.source.id, c.target.id, result.value);
221
+
222
+ this.setBoxMenu(c.source.id);
223
+ }
224
+ });
225
+ }
226
+ }
227
+ });
228
+
229
+ this.jsPlumbInstance.bind("connectionDetached", (info, event) => {
230
+ var c = info.connection.getParameters();
231
+ if( c.type === "data" ) {
232
+ // remove the link
233
+ this.m.removeDataLink(c.source.id, c.source.output, c.target.id, c.target.input);
234
+
235
+ // show source and target type
236
+ this.style.showType(info.targetEndpoint);
237
+ if( !info.source.id.startsWith('connection-split') &&
238
+ !this.m.getNode(info.source.id).isOutputConnected(c.source.output) ) {
239
+ this.style.showType(info.sourceEndpoint);
240
+ }
241
+ }
242
+ else if( c.type === "event" ) {
243
+ this.m.removeEventLink(c.source.id, c.target.id, c.event);
244
+ //this.style.showEventName(info.targetEndpoint);
245
+ }
246
+
247
+ // if the connection is splitted, remove all other connections/endpoint
248
+ if( c.splitted ) {
249
+ var connections = this.jsPlumbInstance.getAllConnections();
250
+ var connToDelete = [];
251
+ var EPToDelete = {};
252
+ _.each(connections, conn => {
253
+ var p = conn.getParameters();
254
+ if( p.source.id === c.source.id &&
255
+ p.target.id === c.target.id &&
256
+ ((c.type == "event" && p.event === c.event) ||
257
+ (c.type == "data" && p.source.sourceOutput == c.source.sourceOutput &&
258
+ p.target.targetInput === p.target.targetInput))) {
259
+ // collect the connection and the endpoints to delete
260
+ connToDelete.push(conn);
261
+
262
+ let [sourceEP, targetEP] = conn.getAttachedElements();
263
+ if( sourceEP.getUuid().startsWith('connection-split-') ) {
264
+ EPToDelete[ sourceEP.getUuid() ] = sourceEP;
265
+ }
266
+ if( targetEP.getUuid().startsWith('connection-split-') ) {
267
+ EPToDelete[ targetEP.getUuid() ] = targetEP;
268
+ }
269
+ }
270
+ });
271
+
272
+ // clean all theses connections and endpoints once this call is ended
273
+ _.each(connToDelete, conn => {
274
+ _.defer(()=> {
275
+ this.jsPlumbInstance.detach(conn, {
276
+ fireEvent: false, //fire a connection detached event?
277
+ forceDetach: false //override any beforeDetach listeners
278
+ });
279
+ });
280
+ });
281
+
282
+ _.each(EPToDelete, (endpoint, uuid) => {
283
+ _.defer(()=> {
284
+ var div = endpoint.getElement();
285
+ this.jsPlumbInstance.deleteEndpoint(endpoint);
286
+ $(div).remove();
287
+ });
288
+ });
289
+ }
290
+ });
291
+
292
+ this.jsPlumbInstance.bind("dblclick", async (connection, e) => {
293
+ let [sourceEP, targetEP] = connection.getAttachedElements();
294
+ let c = connection.getParameters();
295
+
296
+ // split this connection into 2 by creating an endpoint at the event coordinate
297
+ // remove 7px to align it to the center
298
+ var pos = {
299
+ "top" : e.pageY - $(this.canvas).offset().top -7,
300
+ "left": e.pageX - $(this.canvas).offset().left - 7
301
+ };
302
+ this.splitConnection(connection, pos); // return the endpoint exact position
303
+
304
+ // save to the graph
305
+ if( c.type == "data" ) {
306
+ var link = this.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);
307
+ }
308
+ else {
309
+ var link = this.m.getEventLink(c.source.id, c.target.id, c.event);
310
+ }
311
+
312
+ if( sourceEP.getUuid().startsWith('connection-split-') ) {
313
+ var el = sourceEP.getElement();
314
+ var prevPos = this.jsPlumbInstance.getPosition(el);
315
+ link.addPathStep(prevPos, pos);
316
+ }
317
+ else {
318
+ link.addPathStep( "source", pos );
319
+ }
320
+ });
321
+ }
322
+
323
+ detachConnection(info) {
324
+
325
+ }
326
+
327
+ setEventsVisibility( b ) {
328
+ this.showEvents = b;
329
+ this.repaint();
330
+ }
331
+
332
+ // set some transparency to show event better
333
+ setEventMode(b) {
334
+ if( b ) {
335
+ // add opacity to non-events
336
+ $(this.canvas).find('.card:not(.card-ui)').addClass('transparent');
337
+ $(this.canvas).find('.data-connection').each(function() {
338
+ if( $(this).is('svg') ) {
339
+ // addclass doesnt work with svg
340
+ $(this).attr('class', $(this).attr('class') + " transparent");
341
+ }
342
+ else {
343
+ $(this).addClass('transparent');;
344
+ }
345
+ });
346
+ }
347
+ else {
348
+ // add opacity to non-events
349
+ $(this.canvas).find('.card:not(.card-ui)').removeClass('transparent');
350
+ $(this.canvas).find('.data-connection').removeClass('transparent');;
351
+ }
352
+
353
+ return new Promise((resolve) => {
354
+ $(this.canvas).ready(resolve);
355
+ });
356
+ }
357
+
358
+ addNode(id, pkg, def) {
359
+ return new Promise( (resolve, reject) => {
360
+ this.templateMgr.appendTemplate(this.canvas, "graphNode", { id: id, pkg: pkg, def:def, n: this.m.getNode(id), utils: this.utils }, (div) => {
361
+ div.ready(() => resolve());
362
+ });
363
+ });
364
+ }
365
+
366
+ addLink(sourceId, sourceOutput, targetId, targetInput) {
367
+ var sourceUuid = [sourceId, "output", sourceOutput].join('#');
368
+ var targetUuid = [targetId, "input", targetInput].join('#');
369
+
370
+ // hide source and target types
371
+ this.style.hideType(sourceUuid); // works with endpoint or uuid
372
+ this.style.hideType(targetUuid);
373
+
374
+ var connection = this.jsPlumbInstance.connect({
375
+ uuids: [sourceUuid, targetUuid],
376
+ editable: true,
377
+ paintStyle: this.style.connectorData.paintStyle,
378
+ cssClass: 'data-connection',
379
+ connectorClass: 'data-connection'
380
+ });
381
+
382
+ // split the connection if we need to
383
+ var datalink = this.m.getDataLink(sourceId, sourceOutput, targetId, targetInput);
384
+ var path = datalink.getPath();
385
+ _.each( path, (pos) => {
386
+ var r = this.splitConnection(connection, pos);
387
+ connection = r.c2; // switch to the 2nd child connection
388
+ });
389
+ }
390
+
391
+ addEvent(sourceId, targetId, name) {
392
+ var sourceUuid = [sourceId, "event-out"].join('#');
393
+ var sourceEP = this.jsPlumbInstance.getEndpoint(sourceUuid);
394
+
395
+ var connection = this.jsPlumbInstance.connect({
396
+ source: sourceEP,
397
+ target: targetId,
398
+ editable: true,
399
+ paintStyle: this.style.connectorData.paintStyle,
400
+ cssClass: 'event-connection',
401
+ connectorClass: 'event-connection',
402
+ overlays: [
403
+ [ "PlainArrow", { location: 1, width:10, length:10, gap: 12 }],
404
+ [ "Label", { label: name, location:0.50, labelStyle: { cssClass: "connection-label noselect" } } ]
405
+ ]
406
+ });
407
+ connection.setParameter("event", name);
408
+
409
+ // split the connection if we need to
410
+ var eventlink = this.m.getEventLink(sourceId, targetId, name);
411
+ var path = eventlink.getPath();
412
+ _.each( path, (pos) => {
413
+ var r = this.splitConnection(connection, pos);
414
+ connection = r.c2; // switch to the 2nd child connection
415
+ });
416
+ }
417
+
418
+ // split a connection by adding an endpoint at given pos
419
+ splitConnection( connection, pos ) {
420
+ var c = connection.getParameters();
421
+
422
+ // create the endpoint div
423
+ var id = "connection-split-" + dbUtils.randomString(8);
424
+ var div = $('<div/>', {
425
+ id: id,
426
+ class: "connection-control capture-left-click capture-right-click"
427
+ });
428
+ $(this.canvas).append(div);
429
+
430
+ // position it at pos (adjust for the height of the div)
431
+ var jsPlumbElement = this.jsPlumbInstance.getElement(id);
432
+ this.jsPlumbInstance.setPosition(jsPlumbElement, pos);
433
+
434
+ // make this div draggable
435
+ var startPosition = pos;
436
+ this.jsPlumbInstance.draggable(div, {
437
+ start: (e) => {
438
+ startPosition = this.jsPlumbInstance.getPosition(jsPlumbElement);
439
+ },
440
+ stop: (e) => {
441
+ var stopPosition = this.jsPlumbInstance.getPosition(jsPlumbElement);
442
+ if( c.type == "data" ) {
443
+ var link = this.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);
444
+ }
445
+ else if( c.type == "event" ) {
446
+ var link = this.m.getEventLink(c.source.id, c.target.id, c.event);
447
+ }
448
+ link.updatePathStep(startPosition, stopPosition);
449
+ }
450
+ });
451
+
452
+ // determine the style of the 2 connections
453
+ c.splitted = true; // indicates that this connection is splitted
454
+ if( c.type == "data" ) {
455
+ var connectionStyle = {
456
+ paintStyle: this.style.splittedConnectorData.paintStyle,
457
+ connector: this.style.splittedConnectorData.connector,
458
+ connectorOverlays:this.style.splittedConnectorData.overlays,
459
+ connectorStyle: this.style.splittedConnectorData.paintStyle,
460
+ hoverPaintStyle: this.style.endpointData.hoverStyle,
461
+ cssClass : 'data-connection'
462
+ }
463
+ var epStyle = this.style.dataLineSplitterEndpoint;
464
+ }
465
+ else if( c.type == "event" ) {
466
+ var connectionStyle = {
467
+ paintStyle: this.style.eventConnectorData.paintStyle,
468
+ connector: this.style.eventConnectorData.connector,
469
+ connectorOverlays:this.style.eventConnectorData.overlays,
470
+ connectorStyle: this.style.eventConnectorData.paintStyle,
471
+ hoverPaintStyle: this.style.eventEndpointData.hoverStyle,
472
+ cssClass : 'event-connection',
473
+ };
474
+ var epStyle = this.style.eventLineSplitterEndpoint;
475
+ }
476
+
477
+ // create the div endpoint
478
+ var ep = this.jsPlumbInstance.addEndpoint(id, {
479
+ isSource : false,
480
+ isTarget : false,
481
+ uuid : id,
482
+ anchor : "Center",
483
+ maxConnections : 2,
484
+ parameters : c
485
+ }, epStyle);
486
+
487
+ // make the ep transparent to pointer events, left click on it must drag the div
488
+ $(ep.canvas).css('pointer-events', 'none');
489
+ $(ep.canvas).css('cursor', 'move');
490
+ $(ep.canvas).addClass('capture-left-click');
491
+ $(ep.canvas).addClass('capture-right-click');
492
+ $(ep.canvas).find('svg').attr('pointer-events', 'none');
493
+ $(ep.canvas).addClass(id);
494
+
495
+
496
+ // save the connection overlays
497
+ var overlays = connection.getOverlays();
498
+ var overlaysParams = [];
499
+ _.each(overlays, (o)=> {
500
+ overlaysParams.push([o.type, {
501
+ "location" : o.loc,
502
+ "width" : o.width,
503
+ "length" : o.length,
504
+ "label" : o.getLabel ? o.getLabel() : undefined,
505
+ "id" : o.id,
506
+ "labelStyle": o.labelStyle
507
+ }]);
508
+ });
509
+
510
+ let [sourceEP, targetEP] = connection.getAttachedElements();
511
+ let targetId = connection.targetId;
512
+
513
+ // detach the original connection
514
+ this.jsPlumbInstance.detach(connection, { fireEvent: false, forceDetach: false });
515
+
516
+ // now reattach it into 2 connections
517
+ var c1 = this.jsPlumbInstance.connect({
518
+ source: sourceEP,
519
+ target: ep,
520
+ editable: true,
521
+ }, connectionStyle);
522
+
523
+ var c2 = this.jsPlumbInstance.connect({
524
+ source: ep,
525
+ target: targetEP._jsPlumb ? targetEP : targetId /* EP has been destroyed, reattach to window */,
526
+ editable: true,
527
+ }, connectionStyle);
528
+
529
+ sourceEP.setParameter("targetConnection", c1);
530
+ ep.setParameter("sourceConnection", c1);
531
+ ep.setParameter("targetConnection", c2);
532
+ targetEP.setParameter("sourceConnection", c2);
533
+
534
+ // add the overlays to c2
535
+ _.each(overlaysParams, (p)=> {
536
+ c2.addOverlay(p);
537
+ });
538
+
539
+ // add menu to remove this split
540
+ $(ep.canvas).ready(() => {
541
+ var menu = new ContextMenu("#" + id, [
542
+ {
543
+ name: 'Remove the split',
544
+ fn: () => {
545
+ this.unsplitConnection(ep);
546
+ }
547
+ },
548
+ ]);
549
+ });
550
+
551
+ return {
552
+ pos: pos,
553
+ c1 : c1,
554
+ c2 : c2
555
+ }
556
+ }
557
+
558
+ // Remove a split in a connection
559
+ // ep: entrypoint
560
+ // c1: connection 1
561
+ // c2: connection 2
562
+ unsplitConnection(ep) {
563
+ var c = ep.getParameters();
564
+ var c1 = c.sourceConnection;
565
+ var c2 = c.targetConnection;
566
+ var sourceEP = c1.getAttachedElements()[0];
567
+ var targetEP = c2.getAttachedElements()[1];
568
+
569
+ // detach and delete the endpoint
570
+ this.jsPlumbInstance.detach(c1, { fireEvent: false, forceDetach: false });
571
+ this.jsPlumbInstance.detach(c2, { fireEvent: false, forceDetach: false });
572
+ this.jsPlumbInstance.deleteEndpoint(ep);
573
+
574
+
575
+ // reconnect directly the source and the target
576
+ var connection = this.jsPlumbInstance.connect({
577
+ source: sourceEP,
578
+ target: targetEP,
579
+ editable: true,
580
+ });
581
+
582
+ sourceEP.setParameter('targetConnection', connection);
583
+ targetEP.setParameter('sourceConnection', connection);
584
+
585
+ // find the position of the split endpoint to remove
586
+ var id = c1.targetId;
587
+ var element = this.jsPlumbInstance.getElement(id)
588
+ var position = this.jsPlumbInstance.getPosition(element);
589
+
590
+ // remove the element in the path
591
+ // 1. find the link
592
+ // 2. find the position of the split endpoint
593
+ if( c.type == "data" ) {
594
+ var link = this.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);
595
+ link.removePathStep(position);
596
+ }
597
+ else if( c.type == "event" ) {
598
+ var link = this.m.getEventLink();
599
+ link.removePathStep(position);
600
+ }
601
+ else {
602
+ throw "unknown connection type";
603
+ }
604
+
605
+ // remove the div
606
+ $("#" + id).remove(); // delete the div
607
+ }
608
+
609
+ // set the navigation breadcrumb into the canvas
610
+ setNavigation() {
611
+ var appNav = this.div.find('.dualbox-app-navigation');
612
+ var container = appNav.parent();
613
+ appNav.remove();
614
+
615
+ var crumb = $('<ol/>', { class: 'breadcrumb dualbox-app-navigation'});
616
+ _.each(this.m.getWindows(), (w, index, array) => {
617
+ if( index === array.length - 1 ) {
618
+ // last element, don't make it a link
619
+ var li = $('<li/>', { class:"breadcrumb-item" }).append(w[0])
620
+ if( index !== 0 ) {
621
+ // we're in a metanode, add a download button
622
+ var downloadButton = $('<button class="btn btn-xs btn-secondary btn-download-metanode ml-2">' +
623
+ '<i class="fa fa-download"></i></button>');
624
+ downloadButton.click((e) => {
625
+ e.preventDefault();
626
+ e.stopPropagation();
627
+
628
+ var app = this.m.getCurrentMetanode();
629
+ var text = JSON.stringify(app, null, 2);
630
+ var blob = new Blob([text], { "type" : "application/octet-stream" });
631
+
632
+ var a = document.createElement('a');
633
+ a.href = window.URL.createObjectURL(blob);
634
+ a.download = this.m.getCurrentMetanodeName() + ".json";
635
+
636
+ // simulate a click on the link
637
+ if (document.createEvent) {
638
+ var event = document.createEvent("MouseEvents");
639
+ event.initEvent("click", true, true);
640
+ a.dispatchEvent(event);
641
+ } else {
642
+ a.click();
643
+ }
644
+ });
645
+ li.append(downloadButton)
646
+ }
647
+
648
+ crumb.append(li);
649
+ }
650
+ else {
651
+ // append a link to this metabox
652
+ var link = $('<a/>', { href: '#', class:"text-light" }).append(w[0]);
653
+ link.click((e) => {
654
+ e.preventDefault();
655
+ e.stopPropagation();
656
+ this.c.setWindow(w);
657
+ });
658
+ crumb.append($('<li/>', { class:"breadcrumb-item" }).append(link));
659
+ }
660
+ });
661
+ container.append(crumb);
662
+ return new Promise((resolve) => { container.ready(resolve) });
663
+ }
664
+
665
+ isLeftMenuExpanded() {
666
+ return this.div.find('.btn-toggle-left-window').data('expanded')
667
+ }
668
+
669
+ isRightMenuExpanded() {
670
+ return this.div.find('.btn-toggle-right-window').data('expanded')
671
+ }
672
+
673
+ openBoxSettings(id) {
674
+ var expandSettings = () => {
675
+ this.div.find('.dualbox-editor-body').trigger('expandSettings');
676
+ };
677
+
678
+ if( id ) {
679
+ this.setBoxMenu(id, expandSettings);
680
+ }
681
+ else {
682
+ this.setMainMenu(expandSettings);
683
+ }
684
+ }
685
+
686
+ openDebug(id) {
687
+ var expandDebug = () => {
688
+ this.div.find('.dualbox-editor-body').trigger('expandDebug');
689
+ };
690
+
691
+ this.setDebugMenu(id, expandDebug);
692
+ }
693
+
694
+ setBoxMenu(id, cb) {
695
+ var targetDiv = this.div.find('.dualbox-graph-left-panel');
696
+ var expanded = this.getLeftMenuTarget();
697
+ targetDiv.html('');
698
+ this.templateMgr.appendTemplate(targetDiv, "editNodeSettings", { node: this.m.getNode(id) }, () => {
699
+ this.setLeftMenuTarget(expanded);
700
+ if(cb) cb();
701
+ });
702
+ }
703
+
704
+ setMainMenu(cb) {
705
+ var targetDiv = this.div.find('.dualbox-graph-left-panel');
706
+ var expanded = this.getLeftMenuTarget();
707
+ targetDiv.html('');
708
+ this.templateMgr.appendTemplate(targetDiv, "editMainSettings", { app : this.m.getCurrentApp() }, () => {
709
+ this.setLeftMenuTarget(expanded);
710
+ if(cb) cb();
711
+ });
712
+ }
713
+
714
+ setDebugMenu(id, cb) {
715
+ var targetDiv = this.div.find('.dualbox-graph-right-panel');
716
+ var expanded = this.getRightMenuTarget();
717
+ targetDiv.html('');
718
+ this.templateMgr.appendTemplate(targetDiv, "debugNodeInfos", { m : this.m, snapshot : this.m.get(), node: this.m.getNode(id) }, () => {
719
+ this.setRightMenuTarget(expanded);
720
+ if(cb) cb();
721
+ });
722
+ }
723
+
724
+ // save the actual position of where we are in the menu
725
+ getLeftMenuTarget() {
726
+ var expanded = [];
727
+ var panel = this.div.find('.dualbox-graph-left-panel');
728
+ panel.find('div[aria-expanded="true"]').each(function() {
729
+ expanded.push($(this).attr('id'));
730
+ });
731
+ return expanded;
732
+ }
733
+
734
+ // restore where we were before in the menu
735
+ setLeftMenuTarget(expanded) {
736
+ var panel = this.div.find('.dualbox-graph-left-panel');
737
+ var divs = panel.find('div[data-toggle="collapse"]').each(function() {
738
+ if( expanded.indexOf( $(this).attr('id') ) !== -1 ) {
739
+ // this menu was open, restore it
740
+ var dataTarget = $(this).attr('data-target');
741
+ $(dataTarget).collapse('show');
742
+ }
743
+ });
744
+ }
745
+
746
+ // save the actual position of where we are in the menu
747
+ getRightMenuTarget() {
748
+ var expanded = [];
749
+ var panel = this.div.find('.dualbox-graph-right-panel');
750
+ panel.find('div[aria-expanded="true"]').each(function() {
751
+ expanded.push($(this).attr('id'));
752
+ });
753
+ return expanded;
754
+ }
755
+
756
+ // restore where we were before in the menu
757
+ setRightMenuTarget(expanded) {
758
+ var panel = this.div.find('.dualbox-graph-right-panel');
759
+ var divs = panel.find('div[data-toggle="collapse"]').each(function() {
760
+ if( expanded.indexOf( $(this).attr('id') ) !== -1 ) {
761
+ // this menu was open, restore it
762
+ var dataTarget = $(this).attr('data-target');
763
+ $(dataTarget).collapse('show');
764
+ }
765
+ });
766
+ }
767
+
768
+ // reinitialize the view
769
+ reset() {
770
+ this.jsPlumbInstance.reset();
771
+ this.jsPlumbInstance = jsPlumb.getInstance({
772
+ DragOptions: { cursor: 'pointer', zIndex: 2500 }, // default drag options
773
+ Container: "dualbox-graph-canvas"
774
+ });
775
+ this.style.setDefault();
776
+ this.canvas.empty();
777
+ ContextMenu.clean();
778
+ }
779
+
780
+
781
+ // reset graph view and repaint from the graph model
782
+ async repaint() {
783
+ await this.hideCanvas();
784
+
785
+ this.reset();
786
+ await this.setNavigation();
787
+
788
+ // Function to gather all links from a node into the "links" array
789
+ var links = []; // array of links
790
+ var gatherLinks = (targetId, sourceDef) => {
791
+ // for nodes and uis
792
+ _.each(sourceDef.links, (sources, targetInput) => {
793
+ _.each(sources, (sourceOutput, sourceId) => {
794
+ if( sourceId === "graph" ) {
795
+ return;
796
+ }
797
+
798
+ if( sourceId === "input" || sourceId === "output" ) {
799
+ sourceId = this.m.prefixId(sourceOutput, sourceId);
800
+ sourceOutput = "value";
801
+ }
802
+
803
+ links.push({
804
+ sourceId: sourceId,
805
+ sourceOutput: sourceOutput,
806
+ targetId: targetId,
807
+ targetInput: targetInput
808
+ });
809
+ });
810
+ });
811
+
812
+ // for inputs and outputs
813
+ _.each(sourceDef.link, (sourceOutput, sourceId) => {
814
+ if( sourceId === "graph" ) return;
815
+ if( sourceId === "input" || sourceId === "output" ) {
816
+ sourceId = this.m.prefixId(sourceOutput, sourceId);
817
+ sourceOutput = "value";
818
+ }
819
+
820
+ links.push({
821
+ sourceId: sourceId,
822
+ sourceOutput: sourceOutput,
823
+ targetId: targetId,
824
+ targetInput: "value"
825
+ });
826
+ });
827
+ }
828
+
829
+ // Function to gather all links from a node into the "links" array
830
+ var events = [];
831
+ var gatherEvents = (sourceId, def) => {
832
+ _.each(def.events, (evt) => {
833
+ if( evt.node ) {
834
+ events.push({
835
+ sourceId: sourceId,
836
+ targetId: evt.node,
837
+ name: evt.event
838
+ });
839
+ }
840
+ });
841
+ }
842
+
843
+ var app = this.m.getCurrentWindow();
844
+ var addNodePromises = []; // array of promises that will be resolved once all node are added
845
+
846
+ var processModule = (def, id) => {
847
+ addNodePromises.push( new Promise( async (resolve) => {
848
+ var pkg = this.m.getNode(id).getPackage();
849
+ await this.addNode(id, pkg, def); // add the node
850
+ gatherLinks(id, def); // add all data links in our array
851
+ gatherEvents(id, def); // add all event links in our array
852
+ resolve();
853
+ }));
854
+ }
855
+
856
+ var processInput = (def, id) => {
857
+ addNodePromises.push( new Promise( async (resolve) => {
858
+ id = this.m.prefixId(id, "input");
859
+ var pkg = this.m.getNode(id).getPackage();
860
+ await this.addNode(id, pkg, def); // add the node
861
+ gatherLinks(id, def); // add all data links in our array
862
+ gatherEvents(id, def); // add all event links in our array
863
+ resolve();
864
+ }));
865
+ }
866
+
867
+ var processOutput = (def, id) => {
868
+ addNodePromises.push( new Promise( async (resolve) => {
869
+ id = this.m.prefixId(id, "output");
870
+ var pkg = this.m.getNode(id).getPackage();
871
+ await this.addNode(id, pkg, def); // add the node
872
+ gatherLinks(id, def); // add all data links in our array
873
+ gatherEvents(id, def); // add all event links in our array
874
+ resolve();
875
+ }));
876
+ }
877
+
878
+ return new Promise(async (resolve) => {
879
+ // All nodes are now added
880
+ await this.zoomer.deactivate();
881
+ _.each(app.modules, processModule);
882
+ _.each(app.ui, processModule);
883
+ _.each(app.input, processInput);
884
+ _.each(app.output, processOutput);
885
+
886
+ Promise.all(addNodePromises).then(async () => {
887
+ // add the links
888
+ _.each(links, (link) => {
889
+ this.addLink(link.sourceId, link.sourceOutput, link.targetId, link.targetInput);
890
+ });
891
+
892
+ // add the events
893
+ if( this.showEvents ) {
894
+ _.each(events, (e) => {
895
+ this.addEvent(e.sourceId, e.targetId, e.name);
896
+ });
897
+ }
898
+
899
+ await this.canvasSizeHandler.resize();
900
+ await this.zoomer.activate();
901
+ // Initialize all listeners again
902
+ this.initializeListeners();
903
+ await this.setEventMode(this.showEvents);
904
+
905
+ await this.showCanvas();
906
+ resolve();
907
+ });
908
+ });
909
+ }
910
+
911
+ runApp(options) {
912
+ var json = this.m.getEditorCompatibleJson();
913
+ this.appManager.run(json, options);
914
+ }
915
+
916
+ takeAndLoadSnapshot() {
917
+ var json = this.appManager.getSnapshot();
918
+ // if we're not on the 1st tab, go there
919
+ this.div.find("a[data-toggle='tab'][href='#1']").click();
920
+ this.e.setApp(json);
921
+ }
922
+
923
+ killApp() {
924
+ this.appManager.kill();
925
+ }
926
+ }
927
+
928
+ module.exports = GraphView;