@dualbox/editor 1.0.74 → 1.0.76

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.
@@ -18063,6 +18063,10 @@ class History {
18063
18063
  this._holdSaving = b;
18064
18064
  }
18065
18065
 
18066
+ isHoldingSave() {
18067
+ return this._holdSaving;
18068
+ }
18069
+
18066
18070
  // execute the callback and save the history as 1 change
18067
18071
  batch(cb) {
18068
18072
  let ret = this.ignore(cb);
@@ -43753,6 +43757,53 @@ class GraphModel {
43753
43757
  return json;
43754
43758
  }
43755
43759
 
43760
+ // ctrl+v
43761
+ paste(json) {
43762
+ var currentMetanode = this.getCurrentMetanode();
43763
+
43764
+ this.batch(() => {
43765
+ // paste metanodes
43766
+ lodash.each(json.metamodules, (mmDef, mmName) => {
43767
+ var metamodule = lodash.get(currentMetanode, ["metanodes", mmName]);
43768
+ if (metamodule === undefined) {
43769
+ lodash.set(currentMetanode, ["metanodes", mmName], mmDef);
43770
+ }
43771
+ });
43772
+
43773
+ // paste inputs
43774
+ lodash.each(json.input, (inputDef, inputName) => {
43775
+ var input = lodash.get(currentMetanode, ["input", inputName]);
43776
+ if (input === undefined) {
43777
+ lodash.set(currentMetanode, ["input", inputName], inputDef);
43778
+ }
43779
+ });
43780
+
43781
+ // paste outputs
43782
+ lodash.each(json.output, (outputDef, outputName) => {
43783
+ var output = lodash.get(currentMetanode, ["output", outputName]);
43784
+ if (output === undefined) {
43785
+ lodash.set(currentMetanode, ["output", outputName], outputDef);
43786
+ }
43787
+ });
43788
+
43789
+ // paste modules
43790
+ lodash.each(json.modules, (moduleDef, moduleName) => {
43791
+ var node = lodash.get(currentMetanode, ["modules", moduleName]);
43792
+ if (node === undefined) {
43793
+ lodash.set(currentMetanode, ["modules", moduleName], moduleDef);
43794
+ }
43795
+ });
43796
+
43797
+ // paste uis
43798
+ lodash.each(json.ui, (uiDef, uiName) => {
43799
+ var ui = lodash.get(currentMetanode, ["ui", uiName]);
43800
+ if (ui === undefined) {
43801
+ lodash.set(currentMetanode, ["ui", uiName], uiDef);
43802
+ }
43803
+ });
43804
+ });
43805
+ }
43806
+
43756
43807
  /**
43757
43808
  * Json security methods
43758
43809
  */
@@ -44138,12 +44189,12 @@ class GraphNode {
44138
44189
  this.graphId = id.substring(4);
44139
44190
  this.def = this.app.output[this.graphId];
44140
44191
  } else if (this.app.modules && this.app.modules[id]) {
44141
- this.graphId = id;
44142
44192
  this.type = "module";
44193
+ this.graphId = id;
44143
44194
  this.def = this.app.modules[this.graphId];
44144
44195
  } else if (this.app.ui && this.app.ui[id]) {
44145
- this.graphId = id;
44146
44196
  this.type = "ui";
44197
+ this.graphId = id;
44147
44198
  this.def = this.app.ui[this.graphId];
44148
44199
  } else {
44149
44200
  throw "Cant find node " + id;
@@ -45285,10 +45336,14 @@ class GraphNode {
45285
45336
 
45286
45337
  // remove from current app
45287
45338
  remove() {
45288
- this.m.history.holdSaving(true);
45289
- this.removeFrom(this.app);
45290
- this.m.history.holdSaving(false);
45291
- this.m.save();
45339
+ if (this.m.history.isHoldingSave()) {
45340
+ this.removeFrom(this.app);
45341
+ } else {
45342
+ this.m.history.holdSaving(true);
45343
+ this.removeFrom(this.app);
45344
+ this.m.history.holdSaving(false);
45345
+ this.m.save();
45346
+ }
45292
45347
  }
45293
45348
 
45294
45349
  rename(newId) {
@@ -48499,8 +48554,17 @@ var script$1 = {
48499
48554
  else if((e.which == 89 || e.keyCode == 89) && e.ctrlKey) { // ctrl + y
48500
48555
  self.view.c.redo();
48501
48556
  }
48502
- else if((e.which == 46 || e.keyCode == 46)) {
48503
- // suppr: delete selection
48557
+ else if((e.which == 46 || e.keyCode == 46)) { // suppr
48558
+ self.view.c.deleteSelection();
48559
+ }
48560
+ else if((e.which == 67 || e.keyCode == 67) && e.ctrlKey) { // ctrl + c
48561
+ self.view.c.copySelection();
48562
+ }
48563
+ else if((e.which == 86 || e.keyCode == 86) && e.ctrlKey) { // ctrl + v
48564
+ self.view.c.pasteSelection();
48565
+ }
48566
+ else if((e.which == 88 || e.keyCode == 88) && e.ctrlKey) { // ctrl + x
48567
+ self.view.c.copySelection();
48504
48568
  self.view.c.deleteSelection();
48505
48569
  }
48506
48570
  }
@@ -48626,6 +48690,14 @@ var script$1 = {
48626
48690
  this.view.c.deleteSelection();
48627
48691
  },
48628
48692
 
48693
+ copySelection() {
48694
+ this.view.c.copySelection();
48695
+ },
48696
+
48697
+ pasteSelection() {
48698
+ this.view.c.pasteSelection();
48699
+ },
48700
+
48629
48701
  getCurrentMousePosition(e) {
48630
48702
  var x = e.pageX - $(this.$el).find('#dualbox-graph-canvas').offset().left;
48631
48703
  var y = e.pageY - $(this.$el).find('#dualbox-graph-canvas').offset().top;
@@ -48668,7 +48740,7 @@ var script$1 = {
48668
48740
 
48669
48741
  // show selected nodes
48670
48742
  this.selector.onSelecting((divs) => {
48671
- $(this.view.canvas).find('.card').removeClass('selected');
48743
+ $(this.canvas).find('.card').removeClass('selected');
48672
48744
  lodash.each(divs, (div) => {
48673
48745
  $(div).addClass('selected');
48674
48746
  });
@@ -48956,7 +49028,7 @@ var script$1 = {
48956
49028
  id: id,
48957
49029
  class: "connection-control capture-left-click capture-right-click"
48958
49030
  });
48959
- $(this.view.canvas).append(div);
49031
+ $(this.canvas).append(div);
48960
49032
 
48961
49033
  // position it at pos (adjust for the height of the div)
48962
49034
  var jsPlumbElement = this.jsPlumbInstance.getElement(id);
@@ -49241,6 +49313,24 @@ var __vue_render__$1 = function() {
49241
49313
  return null
49242
49314
  }
49243
49315
  return _vm.deleteSelection()
49316
+ },
49317
+ function($event) {
49318
+ if (!$event.type.indexOf("key") && $event.keyCode !== 67) {
49319
+ return null
49320
+ }
49321
+ if (!$event.ctrlKey) {
49322
+ return null
49323
+ }
49324
+ return _vm.copySelection()
49325
+ },
49326
+ function($event) {
49327
+ if (!$event.type.indexOf("key") && $event.keyCode !== 86) {
49328
+ return null
49329
+ }
49330
+ if (!$event.ctrlKey) {
49331
+ return null
49332
+ }
49333
+ return _vm.pasteSelection()
49244
49334
  }
49245
49335
  ]
49246
49336
  }
@@ -49390,7 +49480,7 @@ __vue_render__$1._withStripped = true;
49390
49480
  /* style */
49391
49481
  const __vue_inject_styles__$1 = function (inject) {
49392
49482
  if (!inject) return
49393
- inject("data-v-82f3e0bc_0", { source: "\n.dualbox-graph-container {\n padding: 0px 0px 30px 0px;\n margin-left: -25px;\n margin-right: -25px;\n height: 100%;\n width: calc(100% + 50px);\n overflow: hidden;\n transform: translateZ(0); /* chrome hack to avoid glitches */\n}\n.dualbox-graph-container.translating {\n cursor: move;\n}\n.dualbox-graph-container span {\n pointer-events: none;\n}\n.dualbox-graph-container:focus {\n outline: none;\n}\n.dualbox-graph-canvas {\n height: 100%;\n width: 100%;\n background-color: #eee;\n position: relative;\n outline-color: transparent!important;\n min-height: 100%;\n min-width: 100%;\n border-radius: 5px;\n background-color: #ECF2F8;\n border: 1px solid #2e6da4;\n display: inline-block; /* fix padding pb: https://stackoverflow.com/questions/10054870/when-a-child-element-overflows-horizontally-why-is-the-right-padding-of-the-par */\n margin-bottom: -7px;\n}\n\n/* ex-plumb.css */\n.dualbox-graph-canvas .marker-color-tag {\n height: 8px;\n margin: 0 3px 3px 0;\n padding: 0;\n width: 42px;\n line-height: 75pt;\n border-radius: 3px;\n display: block;\n width: 100%;\n}\n.dualbox-graph-canvas .jtk-connector {\n z-index: 4;\n}\n.dualbox-graph-canvas .jtk-endpoint {\n z-index: 30;\n}\n.dualbox-graph-canvas .inoutEndpointLabel, .endpointSourceLabel {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n z-index: 2000;\n transform:translate(0%,0%) !important; /* this shit is just to prevent jsplumb of centering the box on the endpoint... */\n}\n.dualbox-graph-canvas .descSpan {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n position: fixed;\n display:none;\n z-index:2010;\n}\n.dualbox-graph-canvas .aLabel {\n background-color: white;\n padding: 0.4em;\n font: 12px sans-serif;\n color: #444;\n z-index: 21;\n border: 1px dotted gray;\n opacity: 0.8;\n cursor: pointer;\n}\n.dualbox-graph-canvas .aLabel.jtk-hover {\n background-color: #5C96BC;\n color: white;\n border: 1px solid white;\n}\n.card.jtk-connected {\n /*border: 1px solid #a1a1a1; change border when connected to spot isolated elements as disabled*/\n}\n.jtk-drag {\n opacity:0.7 !important;\n}\npath, .jtk-endpoint {\n cursor: pointer;\n}\n.jtk-overlay {\n background-color:transparent;\n}\n", map: {"version":3,"sources":["/home/seb/dev/dualbox/editor/js/src/v/templates/graph.vue"],"names":[],"mappings":";AACA;IACA,yBAAA;IACA,kBAAA;IACA,mBAAA;IACA,YAAA;IACA,wBAAA;IACA,gBAAA;IACA,wBAAA,EAAA,kCAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,oBAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,YAAA;IACA,WAAA;IACA,sBAAA;IACA,kBAAA;IACA,oCAAA;IACA,gBAAA;IACA,eAAA;IACA,kBAAA;IACA,yBAAA;IACA,yBAAA;IACA,qBAAA,EAAA,iJAAA;IACA,mBAAA;AACA;;AAEA,iBAAA;AACA;IACA,WAAA;IACA,mBAAA;IACA,UAAA;IACA,WAAA;IACA,iBAAA;IACA,kBAAA;IACA,cAAA;IACA,WAAA;AACA;AAEA;IACA,UAAA;AACA;AAEA;IACA,WAAA;AACA;AACA;IACA,+BAAA;IACA,kCAAA;IACA,uCAAA;IACA,oCAAA;IACA,aAAA;IACA,qCAAA,EAAA,iFAAA;AACA;AACA;IACA,+BAAA;IACA,kCAAA;IACA,uCAAA;IACA,oCAAA;IACA,eAAA;IACA,YAAA;IACA,YAAA;AACA;AAEA;IACA,uBAAA;IACA,cAAA;IACA,qBAAA;IACA,WAAA;IACA,WAAA;IACA,uBAAA;IACA,YAAA;IACA,eAAA;AACA;AAEA;IACA,yBAAA;IACA,YAAA;IACA,uBAAA;AACA;AAEA;IACA,gGAAA;AACA;AAEA;IACA,sBAAA;AACA;AAEA;IACA,eAAA;AACA;AAEA;IACA,4BAAA;AACA","file":"graph.vue","sourcesContent":["<style>\n .dualbox-graph-container {\n padding: 0px 0px 30px 0px;\n margin-left: -25px;\n margin-right: -25px;\n height: 100%;\n width: calc(100% + 50px);\n overflow: hidden;\n transform: translateZ(0); /* chrome hack to avoid glitches */\n }\n\n .dualbox-graph-container.translating {\n cursor: move;\n }\n\n .dualbox-graph-container span {\n pointer-events: none;\n }\n\n .dualbox-graph-container:focus {\n outline: none;\n }\n\n .dualbox-graph-canvas {\n height: 100%;\n width: 100%;\n background-color: #eee;\n position: relative;\n outline-color: transparent!important;\n min-height: 100%;\n min-width: 100%;\n border-radius: 5px;\n background-color: #ECF2F8;\n border: 1px solid #2e6da4;\n display: inline-block; /* fix padding pb: https://stackoverflow.com/questions/10054870/when-a-child-element-overflows-horizontally-why-is-the-right-padding-of-the-par */\n margin-bottom: -7px;\n }\n\n /* ex-plumb.css */\n .dualbox-graph-canvas .marker-color-tag {\n height: 8px;\n margin: 0 3px 3px 0;\n padding: 0;\n width: 42px;\n line-height: 75pt;\n border-radius: 3px;\n display: block;\n width: 100%;\n }\n\n .dualbox-graph-canvas .jtk-connector {\n z-index: 4;\n }\n\n .dualbox-graph-canvas .jtk-endpoint {\n z-index: 30;\n }\n .dualbox-graph-canvas .inoutEndpointLabel, .endpointSourceLabel {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n z-index: 2000;\n transform:translate(0%,0%) !important; /* this shit is just to prevent jsplumb of centering the box on the endpoint... */\n }\n .dualbox-graph-canvas .descSpan {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n position: fixed;\n display:none;\n z-index:2010;\n }\n\n .dualbox-graph-canvas .aLabel {\n background-color: white;\n padding: 0.4em;\n font: 12px sans-serif;\n color: #444;\n z-index: 21;\n border: 1px dotted gray;\n opacity: 0.8;\n cursor: pointer;\n }\n\n .dualbox-graph-canvas .aLabel.jtk-hover {\n background-color: #5C96BC;\n color: white;\n border: 1px solid white;\n }\n\n .card.jtk-connected {\n /*border: 1px solid #a1a1a1; change border when connected to spot isolated elements as disabled*/\n }\n\n .jtk-drag {\n opacity:0.7 !important;\n }\n\n path, .jtk-endpoint {\n cursor: pointer;\n }\n\n .jtk-overlay {\n background-color:transparent;\n }\n</style>\n\n<template>\n <div class=\"grid-bg dualbox-graph-container dark\" @keyup.ctrl.89=\"redo()\" @keyup.ctrl.90=\"undo()\" @keyup.ctrl.46=\"deleteSelection()\" tabindex=1>\n <div id=\"dualbox-graph-canvas\" class=\"canvas-wide jtk-surface jtk-surface-nopan contextmenu dualbox-graph-canvas\">\n <graph-node v-for=\"(def, id) in app.input\" :key=\"getNodeUniqId(id, 'input')\" :id=\"getNodeId(id, 'input')\" :pkg=\"getNode(id, 'input').getPackage()\" :n=\"getNode(id, 'input')\" :ref=\"getNodeId(id, 'input')\"></graph-node>\n <graph-node v-for=\"(def, id) in app.output\" :key=\"getNodeUniqId(id, 'output')\" :id=\"getNodeId(id, 'output')\" :pkg=\"getNode(id, 'output').getPackage()\" :n=\"getNode(id, 'output')\" :ref=\"getNodeId(id, 'output')\"></graph-node>\n <graph-node v-for=\"(def, id) in app.modules\" :key=\"getNodeUniqId(id, 'module')\" :id=\"getNodeId(id, 'module')\" :pkg=\"getNode(id, 'module').getPackage()\" :n=\"getNode(id, 'module')\" :ref=\"getNodeId(id, 'module')\"></graph-node>\n <graph-node v-for=\"(def, id) in app.ui\" :key=\"getNodeUniqId(id, 'ui')\" :id=\"getNodeId(id, 'ui')\" :pkg=\"getNode(id, 'ui').getPackage()\" :n=\"getNode(id, 'ui')\" :ref=\"getNodeId(id, 'ui')\" :displayEvents=\"displayEvents\"></graph-node>\n </div>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-topleft\" style=\"position: absolute; top: 0; left: 0px; border-radius: 0px; border-top-left-radius: 5px; margin: 0;\" v-on:click=\"gotoTopLeft\"><i class=\"fa fa-arrows-alt\"></i></button>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-topright\" style=\"position: absolute; top: 0; right: 0px; border-radius: 0px; border-top-right-radius: 5px; margin: 0;\" v-on:click=\"gotoTopRight\"><i class=\"fa fa-arrows-alt\"></i></button>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-bottomleft\" style=\"position: absolute; bottom: 30px; left: 0px; border-radius: 0px; border-bottom-left-radius: 5px; margin: 0;\" v-on:click=\"gotoBottomLeft\"><i class=\"fa fa-arrows-alt\"></i></button>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-bottomright\" style=\"position: absolute; bottom: 30px; right: 0px; border-radius: 0px; border-bottom-right-radius: 5px; margin: 0;\" v-on:click=\"gotoBottomRight\"><i class=\"fa fa-arrows-alt\"></i></button>\n </div>\n</template>\n\n<script>\n// Utils\nimport _ from 'lodash-es';\nimport dbutils from '../../m/DualboxUtils';\nimport Utils from '../Utils';\nimport swal from 'sweetalert2';\n\n// Sub-components\nimport graphNodeVue from './graphNode.vue';\n\n// Canvas control helpers\nimport Zoomer from '../Zoomer';\nimport Selector from '../Selector';\nimport Translater from '../Translater';\nimport CanvasSizeHandler from '../CanvasSizeHandler';\nimport ContextMenu from '../ContextMenu';\n\n// jsplumb style definitins\nimport PlumbStyle from '../PlumbStyle';\n\nexport default {\n props: [\n \"app\", // the app model\n \"displayEvents\" // display events or not\n ],\n components: {\n 'graph-node' : graphNodeVue,\n },\n data: function () {\n return {\n utils: Utils,\n loading: false,\n }\n },\n created: function() {\n this.view = window.dualboxEditor.v;\n this.jsPlumbInstance = null;\n\n // Controls helpers utilities\n this.selector = null;\n this.translater = null;\n this.zoomer = null;\n this.canvasSizeHandler = null;\n\n // encapsulate the JSPlumb styles\n this.style = new PlumbStyle(this);\n },\n mounted: async function() {\n this.jsPlumbInstance = jsPlumb.getInstance({\n DragOptions: { cursor: 'pointer', zIndex: 2500 }, // default drag options\n Container: \"dualbox-graph-canvas\"\n });\n this.jsPlumbInstance.rand = dbutils.randomString(8);\n this.style.setDefault();\n this.graphContainer = $(this.$el);\n this.canvas = $(this.$el).find('.dualbox-graph-canvas');\n\n // TODO: remove theses definitions from the graphview object (bugs when entering metanode)\n this.view.selector = this.selector = new Selector(this, this.graphContainer);\n this.view.translater = this.translater = new Translater(this, this.graphContainer, this.canvas);\n this.view.zoomer = this.zoomer = new Zoomer(this, this.graphContainer, this.canvas);\n this.view.canvasSizeHandler = this.canvasSizeHandler = new CanvasSizeHandler(this, this.canvas);\n this.assignContextMenu();\n\n // autofocus editor when the div is clicked (allow to use keydown)\n // We use this way because it's easier than to set a tabindex on the dualbox-graph-container\n // to capture keyboard events (will break modals)\n var self = this;\n this.keyDownHandler = function(e) {\n // Ensure event is not null\n e = e || window.event;\n\n if( $(self.$el).is(\":hover\") ) {\n if ((e.which == 90 || e.keyCode == 90) && e.ctrlKey) { // ctrl + z\n self.view.c.undo();\n }\n else if((e.which == 89 || e.keyCode == 89) && e.ctrlKey) { // ctrl + y\n self.view.c.redo();\n }\n else if((e.which == 46 || e.keyCode == 46)) {\n // suppr: delete selection\n self.view.c.deleteSelection();\n }\n }\n }\n\n $(document).on(\"keydown\", this.keyDownHandler);\n return await this.initializeListeners();\n },\n destroyed: function() {\n $(document).off(\"keydown\", this.keyDownHandler);\n },\n beforeUpdate: async function() {\n // Reset the JSPlumb instance\n this.resetJsPlumb();\n this.refreshBoxes();\n },\n updated: async function() {\n // don't do anything for out-of-date updates (hash is not matching)\n console.log('Updating graph !');\n\n //this.canvas.empty();\n ContextMenu.clean();\n this.assignContextMenu();\n\n // We need to add links and events to the nodes\n\n // Function to gather all links from a node into the \"links\" array\n var links = []; // array of links\n var gatherLinks = (targetId, sourceDef) => {\n // for nodes and uis\n _.each(sourceDef.links, (sources, targetInput) => {\n _.each(sources, (sourceOutput, sourceId) => {\n if( sourceId === \"graph\" ) {\n return;\n }\n\n if( sourceId === \"input\" || sourceId === \"output\" ) {\n sourceId = this.view.m.prefixId(sourceOutput, sourceId);\n sourceOutput = \"value\";\n }\n\n links.push({\n sourceId: sourceId,\n sourceOutput: sourceOutput,\n targetId: targetId,\n targetInput: targetInput\n });\n });\n });\n\n // for inputs and outputs\n _.each(sourceDef.link, (sourceOutput, sourceId) => {\n if( sourceId === \"graph\" ) return;\n if( sourceId === \"input\" || sourceId === \"output\" ) {\n sourceId = this.view.m.prefixId(sourceOutput, sourceId);\n sourceOutput = \"value\";\n }\n\n links.push({\n sourceId: sourceId,\n sourceOutput: sourceOutput,\n targetId: targetId,\n targetInput: \"value\"\n });\n });\n }\n\n // Function to gather all events from a node into the \"links\" array\n var events = [];\n var gatherEvents = (sourceId, def) => {\n _.each(def.events, (evt) => {\n if( evt.node ) {\n events.push({\n sourceId: sourceId,\n targetId: evt.node,\n name: evt.event\n });\n }\n });\n }\n\n _.each(this.app.input, (def, id) => {\n id = this.view.m.prefixId(id, \"input\");\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n _.each(this.app.output, (def, id) => {\n id = this.view.m.prefixId(id, \"output\");\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n _.each(this.app.modules, (def, id) =>{\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n _.each(this.app.ui, (def, id) =>{\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n\n // Now, we gathered all links and events. Add them into jsPlumb\n // add the links\n _.each(links, (link) => {\n this.addLink(link.sourceId, link.sourceOutput, link.targetId, link.targetInput);\n });\n\n // add the events\n if( this.displayEvents ) {\n _.each(events, (e) => {\n this.addEvent(e.sourceId, e.targetId, e.name);\n });\n }\n\n await this.canvasSizeHandler.resize();\n\n // Initialize all listeners again\n await this.initializeJsPlumbListeners();\n\n return new Promise((resolve) => this.jsPlumbInstance.ready(resolve));\n },\n methods: {\n deleteSelection() {\n this.view.c.deleteSelection();\n },\n\n getCurrentMousePosition(e) {\n var x = e.pageX - $(this.$el).find('#dualbox-graph-canvas').offset().left;\n var y = e.pageY - $(this.$el).find('#dualbox-graph-canvas').offset().top;\n x = x / this.zoomer.getZoom();\n y = y / this.zoomer.getZoom();\n return {\n \"top\": y,\n \"left\": x\n };\n },\n\n resetJsPlumb : function() {\n if( this.jsPlumbInstance ) {\n this.jsPlumbInstance.reset();\n delete this.jsPlumbInstance.initializedNodes;\n }\n\n window.jsPlumbInstance = this.jsPlumbInstance = jsPlumb.getInstance({\n DragOptions: { cursor: 'pointer', zIndex: 2500 }, // default drag options\n Container: \"dualbox-graph-canvas\"\n });\n\n this.jsPlumbInstance.rand = dbutils.randomString(8); // just change the node refs to avoid vue.js optimizations\n this.style.setDefault();\n },\n\n refreshBoxes: function() {\n _.each(this.$refs, (component, name) => {\n var c = component[0];\n if(c) c.$forceUpdate();\n })\n },\n\n // bind jsPlumb events\n initializeListeners: async function() {\n this.selector.initialize();\n this.zoomer.initialize();\n ContextMenu.initialize(); // important to make this after the translater (right-clic conflicts)\n this.translater.initialize();\n\n // show selected nodes\n this.selector.onSelecting((divs) => {\n $(this.view.canvas).find('.card').removeClass('selected');\n _.each(divs, (div) => {\n $(div).addClass('selected');\n });\n });\n\n // Add every node in the selected area to JsPlumb selection\n this.selector.onSelect((divs) => {\n _.each(divs, (div) => {\n $(div).addClass('selected');\n this.jsPlumbInstance.addToDragSelection($(div)[0]);\n });\n });\n\n // remove the selection\n this.selector.onDeselect((divs) => {\n _.each(divs, (div) => {\n $(div).removeClass('selected');\n this.jsPlumbInstance.removeFromDragSelection($(div)[0]);\n });\n });\n\n return await this.initializeJsPlumbListeners();\n },\n\n initializeJsPlumbListeners: function() {\n this.refreshGraphNode = function(id) {\n var nodeVue = this.$refs[id];\n if( nodeVue[0] ) nodeVue[0].$forceUpdate();\n }\n\n this.jsPlumbInstance.bind(\"connectionDrag\", (connection) => {\n // hide the type of the source (output endpoint)\n var endpoints = connection.getAttachedElements();\n if( endpoints && endpoints[0] ) this.style.hideType(endpoints[0]);\n\n // if the connection linked a source to a target, remove the connection\n var c = connection.getParameters();\n if( c.target ) {\n if( c.type === \"data\" ) {\n // remove the link\n this.view.m.removeDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n }\n else if( c.type === \"event\" ) {\n this.view.m.removeEventLink(c.source.id, c.target.id, c.event);\n this.view.setBoxMenu(c.source.id);\n }\n }\n });\n\n this.jsPlumbInstance.bind(\"connectionDragStop\", (connection) => {\n // if the connection has not been made, show back the type of the source\n var endpoints = connection.getAttachedElements();\n if( endpoints && endpoints.length == 2 ) {\n var sourceEP = endpoints[0];\n var targetEP = endpoints[1];\n if( targetEP && targetEP.elementId.startsWith('jsPlumb')) {\n if( targetEP.elementId.startsWith('jsPlumb') ) {\n // no connection has been made, show back our output type\n var sourceOutput = connection.getParameters().source.output;\n if( !this.view.m.getNode(sourceEP.elementId).isOutputConnected(sourceOutput) ) {\n this.style.showType(sourceEP);\n }\n }\n }\n }\n });\n\n this.jsPlumbInstance.bind(\"connection\", (info, event) => {\n // only do this for connection established by user, no programmatically\n if( event ) {\n var c = info.connection.getParameters();\n if( c.type === \"data\" ) {\n // check the types\n var sourceType = this.view.m.getNode(c.source.id).getOutputType(c.source.output);\n var targetType = this.view.m.getNode(c.target.id).getInputType(c.target.input);\n if( this.view.m.compareTypes(sourceType, targetType) ) {\n this.view.m.addDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n\n // the target need to be refreshed\n this.refreshGraphNode(c.target.id);\n\n // hide source and target types\n this.style.hideType(info.sourceEndpoint);\n this.style.hideType(info.targetEndpoint);\n }\n else {\n // Error\n this.jsPlumbInstance.deleteConnection(info.connection, {\n fireEvent: false, //fire a connection detached event?\n forceDetach: false //override any beforeDetach listeners\n });\n this.style.showType(info.targetEndpoint);\n if( !this.view.m.getNode(info.sourceId).isOutputConnected(c.source.output) ) {\n this.style.showType(info.sourceEndpoint);\n }\n\n swal({\n title: \"Nope.\",\n 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.\",\n type: \"error\",\n });\n }\n }\n else if( c.type === \"event\" ) {\n var target = this.view.m.getNode(c.target.id);\n var eventNames = target.getEventsNames();\n var options = {};\n _.each(eventNames, (name) => options[name] = name );\n\n swal({\n input: 'select',\n title: 'Select an event',\n inputOptions: options\n }).then( (result) => {\n if( result.value ) {\n info.connection.setParameter('event', result.value);\n info.connection.setLabel({ label: result.value, cssClass: \"connection-label noselect\", location: 0.5 });\n this.view.m.addEventLink(c.source.id, c.target.id, result.value);\n\n this.view.setBoxMenu(c.source.id);\n }\n });\n }\n }\n });\n\n this.jsPlumbInstance.bind(\"connectionDetached\", (info, event) => {\n var c = info.connection.getParameters();\n if( c.type === \"data\" ) {\n // remove the link\n this.view.m.removeDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n\n // show source and target type\n this.style.showType(info.targetEndpoint);\n if( !info.source.id.startsWith('connection-split') &&\n !this.view.m.getNode(info.source.id).isOutputConnected(c.source.output) ) {\n this.style.showType(info.sourceEndpoint);\n }\n\n // the target need to be refreshed\n this.refreshGraphNode(c.target.id);\n }\n else if( c.type === \"event\" ) {\n this.view.m.removeEventLink(c.source.id, c.target.id, c.event);\n //this.style.showEventName(info.targetEndpoint);\n }\n\n // if the connection is splitted, remove all other connections/endpoint\n if( c.splitted ) {\n var connections = this.jsPlumbInstance.getAllConnections();\n var connToDelete = [];\n var EPToDelete = {};\n _.each(connections, conn => {\n var p = conn.getParameters();\n if( p.source.id === c.source.id &&\n p.target.id === c.target.id &&\n ((c.type == \"event\" && p.event === c.event) ||\n (c.type == \"data\" && p.source.sourceOutput == c.source.sourceOutput &&\n p.target.targetInput === p.target.targetInput))) {\n // collect the connection and the endpoints to delete\n connToDelete.push(conn);\n\n let [sourceEP, targetEP] = conn.getAttachedElements();\n if( sourceEP.getUuid().startsWith('connection-split-') ) {\n EPToDelete[ sourceEP.getUuid() ] = sourceEP;\n }\n if( targetEP.getUuid().startsWith('connection-split-') ) {\n EPToDelete[ targetEP.getUuid() ] = targetEP;\n }\n }\n });\n\n // clean all theses connections and endpoints once this call is ended\n _.each(connToDelete, conn => {\n _.defer(()=> {\n this.jsPlumbInstance.deleteConnection(conn, {\n fireEvent: false, //fire a connection detached event?\n forceDetach: false //override any beforeDetach listeners\n });\n });\n });\n\n _.each(EPToDelete, (endpoint, uuid) => {\n _.defer(()=> {\n var div = endpoint.getElement();\n this.jsPlumbInstance.deleteEndpoint(endpoint);\n $(div).remove();\n });\n });\n }\n });\n\n this.jsPlumbInstance.bind(\"dblclick\", async (connection, e) => {\n let [sourceEP, targetEP] = connection.getAttachedElements();\n let c = connection.getParameters();\n\n // split this connection into 2 by creating an endpoint at the event coordinate\n // remove 7px to align it to the center\n var pos = {\n \"top\" : e.pageY - $(this.canvas).offset().top -7,\n \"left\": e.pageX - $(this.canvas).offset().left - 7\n };\n this.splitConnection(connection, pos); // return the endpoint exact position\n\n // save to the graph\n if( c.type == \"data\" ) {\n var link = this.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n }\n else {\n var link = this.m.getEventLink(c.source.id, c.target.id, c.event);\n }\n\n if( sourceEP.getUuid().startsWith('connection-split-') ) {\n var el = sourceEP.getElement();\n var prevPos = this.jsPlumbInstance.getPosition(el);\n link.addPathStep(prevPos, pos);\n }\n else {\n link.addPathStep( \"source\", pos );\n }\n });\n\n return new Promise(resolve => this.jsPlumbInstance.ready(resolve));\n },\n\n addLink: function(sourceId, sourceOutput, targetId, targetInput) {\n var sourceUuid = [sourceId, \"output\", sourceOutput].join('#');\n var targetUuid = [targetId, \"input\", targetInput].join('#');\n\n // hide source and target types\n this.style.hideType(sourceUuid); // works with endpoint or uuid\n this.style.hideType(targetUuid);\n\n var connection = this.jsPlumbInstance.connect({\n uuids: [sourceUuid, targetUuid],\n editable: true,\n paintStyle: this.style.connectorData.paintStyle,\n cssClass: 'data-connection',\n connectorClass: 'data-connection'\n });\n\n // split the connection if we need to\n var datalink = this.view.m.getDataLink(sourceId, sourceOutput, targetId, targetInput);\n var path = datalink.getPath();\n _.each( path, (pos) => {\n var r = this.splitConnection(connection, pos);\n connection = r.c2; // switch to the 2nd child connection\n });\n },\n\n addEvent: function(sourceId, targetId, name) {\n var sourceUuid = [sourceId, \"event-out\"].join('#');\n var sourceEP = this.jsPlumbInstance.getEndpoint(sourceUuid);\n\n var connection = this.jsPlumbInstance.connect({\n source: sourceEP,\n target: targetId,\n editable: true,\n paintStyle: this.style.connectorData.paintStyle,\n cssClass: 'event-connection',\n connectorClass: 'event-connection',\n overlays: [\n [ \"PlainArrow\", { location: 1, width:10, length:10, gap: 12 }],\n [ \"Label\", { label: name, location:0.50, labelStyle: { cssClass: \"connection-label noselect\" } } ]\n ]\n });\n connection.setParameter(\"event\", name);\n\n // split the connection if we need to\n var eventlink = this.view.m.getEventLink(sourceId, targetId, name);\n var path = eventlink.getPath();\n _.each( path, (pos) => {\n var r = this.splitConnection(connection, pos);\n connection = r.c2; // switch to the 2nd child connection\n });\n },\n\n // split a connection by adding an endpoint at given pos\n splitConnection: function( connection, pos ) {\n var c = connection.getParameters();\n\n // create the endpoint div\n var id = \"connection-split-\" + dbutils.randomString(8);\n var div = $('<div/>', {\n id: id,\n class: \"connection-control capture-left-click capture-right-click\"\n });\n $(this.view.canvas).append(div);\n\n // position it at pos (adjust for the height of the div)\n var jsPlumbElement = this.jsPlumbInstance.getElement(id);\n this.jsPlumbInstance.setPosition(jsPlumbElement, pos);\n\n // make this div draggable\n var startPosition = pos;\n this.jsPlumbInstance.draggable(div, {\n start: (e) => {\n startPosition = this.jsPlumbInstance.getPosition(jsPlumbElement);\n },\n stop: (e) => {\n var stopPosition = this.jsPlumbInstance.getPosition(jsPlumbElement);\n if( c.type == \"data\" ) {\n var link = this.view.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n }\n else if( c.type == \"event\" ) {\n var link = this.view.m.getEventLink(c.source.id, c.target.id, c.event);\n }\n link.updatePathStep(startPosition, stopPosition);\n }\n });\n\n // determine the style of the 2 connections\n c.splitted = true; // indicates that this connection is splitted\n if( c.type == \"data\" ) {\n var connectionStyle = {\n paintStyle: this.style.splittedConnectorData.paintStyle,\n connector: this.style.splittedConnectorData.connector,\n connectorOverlays:this.style.splittedConnectorData.overlays,\n connectorStyle: this.style.splittedConnectorData.paintStyle,\n hoverPaintStyle: this.style.endpointData.hoverStyle,\n cssClass : 'data-connection'\n }\n var epStyle = this.style.dataLineSplitterEndpoint;\n }\n else if( c.type == \"event\" ) {\n var connectionStyle = {\n paintStyle: this.style.eventConnectorData.paintStyle,\n connector: this.style.eventConnectorData.connector,\n connectorOverlays:this.style.eventConnectorData.overlays,\n connectorStyle: this.style.eventConnectorData.paintStyle,\n hoverPaintStyle: this.style.eventEndpointData.hoverStyle,\n cssClass : 'event-connection',\n };\n var epStyle = this.style.eventLineSplitterEndpoint;\n }\n\n // create the div endpoint\n var ep = this.jsPlumbInstance.addEndpoint(id, {\n isSource : false,\n isTarget : false,\n uuid : id,\n anchor : \"Center\",\n maxConnections : 2,\n parameters : c\n }, epStyle);\n\n // make the ep transparent to pointer events, left click on it must drag the div\n $(ep.canvas).css('pointer-events', 'none');\n $(ep.canvas).css('cursor', 'move');\n $(ep.canvas).addClass('capture-left-click');\n $(ep.canvas).addClass('capture-right-click');\n $(ep.canvas).find('svg').attr('pointer-events', 'none');\n $(ep.canvas).addClass(id);\n\n\n // save the connection overlays\n var overlays = connection.getOverlays();\n var overlaysParams = [];\n _.each(overlays, (o)=> {\n overlaysParams.push([o.type, {\n \"location\" : o.loc,\n \"width\" : o.width,\n \"length\" : o.length,\n \"label\" : o.getLabel ? o.getLabel() : undefined,\n \"id\" : o.id,\n \"labelStyle\": o.labelStyle\n }]);\n });\n\n let [sourceEP, targetEP] = connection.getAttachedElements();\n let targetId = connection.targetId;\n\n // detach the original connection\n this.jsPlumbInstance.deleteConnection(connection, { fireEvent: false, forceDetach: false });\n\n // now reattach it into 2 connections\n var c1 = this.jsPlumbInstance.connect({\n source: sourceEP,\n target: ep,\n editable: true,\n }, connectionStyle);\n\n var c2 = this.jsPlumbInstance.connect({\n source: ep,\n target: targetEP._jsPlumb ? targetEP : targetId /* EP has been destroyed, reattach to window */,\n editable: true,\n }, connectionStyle);\n\n sourceEP.setParameter(\"targetConnection\", c1);\n ep.setParameter(\"sourceConnection\", c1);\n ep.setParameter(\"targetConnection\", c2);\n targetEP.setParameter(\"sourceConnection\", c2);\n\n // add the overlays to c2\n _.each(overlaysParams, (p)=> {\n c2.addOverlay(p);\n });\n\n // add menu to remove this split\n $(ep.canvas).ready(() => {\n var menu = new ContextMenu(\"#\" + id, [\n {\n name: 'Remove the split',\n fn: () => {\n this.unsplitConnection(ep);\n }\n },\n ]);\n });\n\n return {\n pos: pos,\n c1 : c1,\n c2 : c2\n }\n },\n\n // Remove a split in a connection\n // ep: entrypoint\n // c1: connection 1\n // c2: connection 2\n unsplitConnection: function(ep) {\n var c = ep.getParameters();\n var c1 = c.sourceConnection;\n var c2 = c.targetConnection;\n var sourceEP = c1.getAttachedElements()[0];\n var targetEP = c2.getAttachedElements()[1];\n\n // detach and delete the endpoint\n this.jsPlumbInstance.deleteConnection(c1, { fireEvent: false, forceDetach: false });\n this.jsPlumbInstance.deleteConnection(c2, { fireEvent: false, forceDetach: false });\n this.jsPlumbInstance.deleteEndpoint(ep);\n\n\n // reconnect directly the source and the target\n var connection = this.jsPlumbInstance.connect({\n source: sourceEP,\n target: targetEP,\n editable: true,\n });\n\n sourceEP.setParameter('targetConnection', connection);\n targetEP.setParameter('sourceConnection', connection);\n\n // find the position of the split endpoint to remove\n var id = c1.targetId;\n var element = this.jsPlumbInstance.getElement(id)\n var position = this.jsPlumbInstance.getPosition(element);\n\n // remove the element in the path\n // 1. find the link\n // 2. find the position of the split endpoint\n if( c.type == \"data\" ) {\n var link = this.view.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n link.removePathStep(position);\n }\n else if( c.type == \"event\" ) {\n var link = this.m.getEventLink();\n link.removePathStep(position);\n }\n else {\n throw \"unknown connection type\";\n }\n\n // remove the div\n $(\"#\" + id).remove(); // delete the div\n },\n\n getNodeId: function( id, nodeType ) {\n switch( nodeType ) {\n case \"input\": return this.view.m.prefixId(id, \"input\");\n case \"output\": return this.view.m.prefixId(id, \"output\");\n case \"module\":\n case \"ui\":\n default:\n return id;\n }\n },\n\n getNode( id, nodeType ) {\n var realId = this.getNodeId(id, nodeType);\n return this.view.m.getNode(realId);\n },\n\n getNodeUniqId(id, nodeType) {\n return this.getNode(id, nodeType).getUniqId() + this.jsPlumbInstance.rand;\n },\n\n setLoading(b) {\n this.loading = b;\n },\n\n gotoTopLeft: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoTopLeft();\n },\n\n gotoTopRight: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoTopRight();\n },\n\n gotoBottomLeft: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoBottomLeft();\n },\n\n gotoBottomRight: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoBottomRight();\n },\n\n assignContextMenu: function() {\n // Create a contextmenu for the div\n var contextOptions = [\n {\n name: 'Add node...',\n fn: (target, evt) => {\n this.$parent.addBox(evt);\n }\n }\n ];\n var cMenu = new ContextMenu(\"#dualbox-graph-canvas\", contextOptions);\n },\n }\n}\n</script>\n"]}, media: undefined });
49483
+ inject("data-v-6d2c9492_0", { source: "\n.dualbox-graph-container {\n padding: 0px 0px 30px 0px;\n margin-left: -25px;\n margin-right: -25px;\n height: 100%;\n width: calc(100% + 50px);\n overflow: hidden;\n transform: translateZ(0); /* chrome hack to avoid glitches */\n}\n.dualbox-graph-container.translating {\n cursor: move;\n}\n.dualbox-graph-container span {\n pointer-events: none;\n}\n.dualbox-graph-container:focus {\n outline: none;\n}\n.dualbox-graph-canvas {\n height: 100%;\n width: 100%;\n background-color: #eee;\n position: relative;\n outline-color: transparent!important;\n min-height: 100%;\n min-width: 100%;\n border-radius: 5px;\n background-color: #ECF2F8;\n border: 1px solid #2e6da4;\n display: inline-block; /* fix padding pb: https://stackoverflow.com/questions/10054870/when-a-child-element-overflows-horizontally-why-is-the-right-padding-of-the-par */\n margin-bottom: -7px;\n}\n\n/* ex-plumb.css */\n.dualbox-graph-canvas .marker-color-tag {\n height: 8px;\n margin: 0 3px 3px 0;\n padding: 0;\n width: 42px;\n line-height: 75pt;\n border-radius: 3px;\n display: block;\n width: 100%;\n}\n.dualbox-graph-canvas .jtk-connector {\n z-index: 4;\n}\n.dualbox-graph-canvas .jtk-endpoint {\n z-index: 30;\n}\n.dualbox-graph-canvas .inoutEndpointLabel, .endpointSourceLabel {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n z-index: 2000;\n transform:translate(0%,0%) !important; /* this shit is just to prevent jsplumb of centering the box on the endpoint... */\n}\n.dualbox-graph-canvas .descSpan {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n position: fixed;\n display:none;\n z-index:2010;\n}\n.dualbox-graph-canvas .aLabel {\n background-color: white;\n padding: 0.4em;\n font: 12px sans-serif;\n color: #444;\n z-index: 21;\n border: 1px dotted gray;\n opacity: 0.8;\n cursor: pointer;\n}\n.dualbox-graph-canvas .aLabel.jtk-hover {\n background-color: #5C96BC;\n color: white;\n border: 1px solid white;\n}\n.card.jtk-connected {\n /*border: 1px solid #a1a1a1; change border when connected to spot isolated elements as disabled*/\n}\n.jtk-drag {\n opacity:0.7 !important;\n}\npath, .jtk-endpoint {\n cursor: pointer;\n}\n.jtk-overlay {\n background-color:transparent;\n}\n", map: {"version":3,"sources":["/home/seb/dev/dualbox/editor/js/src/v/templates/graph.vue"],"names":[],"mappings":";AACA;IACA,yBAAA;IACA,kBAAA;IACA,mBAAA;IACA,YAAA;IACA,wBAAA;IACA,gBAAA;IACA,wBAAA,EAAA,kCAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,oBAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,YAAA;IACA,WAAA;IACA,sBAAA;IACA,kBAAA;IACA,oCAAA;IACA,gBAAA;IACA,eAAA;IACA,kBAAA;IACA,yBAAA;IACA,yBAAA;IACA,qBAAA,EAAA,iJAAA;IACA,mBAAA;AACA;;AAEA,iBAAA;AACA;IACA,WAAA;IACA,mBAAA;IACA,UAAA;IACA,WAAA;IACA,iBAAA;IACA,kBAAA;IACA,cAAA;IACA,WAAA;AACA;AAEA;IACA,UAAA;AACA;AAEA;IACA,WAAA;AACA;AACA;IACA,+BAAA;IACA,kCAAA;IACA,uCAAA;IACA,oCAAA;IACA,aAAA;IACA,qCAAA,EAAA,iFAAA;AACA;AACA;IACA,+BAAA;IACA,kCAAA;IACA,uCAAA;IACA,oCAAA;IACA,eAAA;IACA,YAAA;IACA,YAAA;AACA;AAEA;IACA,uBAAA;IACA,cAAA;IACA,qBAAA;IACA,WAAA;IACA,WAAA;IACA,uBAAA;IACA,YAAA;IACA,eAAA;AACA;AAEA;IACA,yBAAA;IACA,YAAA;IACA,uBAAA;AACA;AAEA;IACA,gGAAA;AACA;AAEA;IACA,sBAAA;AACA;AAEA;IACA,eAAA;AACA;AAEA;IACA,4BAAA;AACA","file":"graph.vue","sourcesContent":["<style>\n .dualbox-graph-container {\n padding: 0px 0px 30px 0px;\n margin-left: -25px;\n margin-right: -25px;\n height: 100%;\n width: calc(100% + 50px);\n overflow: hidden;\n transform: translateZ(0); /* chrome hack to avoid glitches */\n }\n\n .dualbox-graph-container.translating {\n cursor: move;\n }\n\n .dualbox-graph-container span {\n pointer-events: none;\n }\n\n .dualbox-graph-container:focus {\n outline: none;\n }\n\n .dualbox-graph-canvas {\n height: 100%;\n width: 100%;\n background-color: #eee;\n position: relative;\n outline-color: transparent!important;\n min-height: 100%;\n min-width: 100%;\n border-radius: 5px;\n background-color: #ECF2F8;\n border: 1px solid #2e6da4;\n display: inline-block; /* fix padding pb: https://stackoverflow.com/questions/10054870/when-a-child-element-overflows-horizontally-why-is-the-right-padding-of-the-par */\n margin-bottom: -7px;\n }\n\n /* ex-plumb.css */\n .dualbox-graph-canvas .marker-color-tag {\n height: 8px;\n margin: 0 3px 3px 0;\n padding: 0;\n width: 42px;\n line-height: 75pt;\n border-radius: 3px;\n display: block;\n width: 100%;\n }\n\n .dualbox-graph-canvas .jtk-connector {\n z-index: 4;\n }\n\n .dualbox-graph-canvas .jtk-endpoint {\n z-index: 30;\n }\n .dualbox-graph-canvas .inoutEndpointLabel, .endpointSourceLabel {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n z-index: 2000;\n transform:translate(0%,0%) !important; /* this shit is just to prevent jsplumb of centering the box on the endpoint... */\n }\n .dualbox-graph-canvas .descSpan {\n box-shadow: 1px 1px 1px #716f6f;\n -o-box-shadow: 1px 1px 1px #716f6f;\n -webkit-box-shadow: 0px 1px 1px #716f6f;\n -moz-box-shadow: 1px 1px 1px #716f6f;\n position: fixed;\n display:none;\n z-index:2010;\n }\n\n .dualbox-graph-canvas .aLabel {\n background-color: white;\n padding: 0.4em;\n font: 12px sans-serif;\n color: #444;\n z-index: 21;\n border: 1px dotted gray;\n opacity: 0.8;\n cursor: pointer;\n }\n\n .dualbox-graph-canvas .aLabel.jtk-hover {\n background-color: #5C96BC;\n color: white;\n border: 1px solid white;\n }\n\n .card.jtk-connected {\n /*border: 1px solid #a1a1a1; change border when connected to spot isolated elements as disabled*/\n }\n\n .jtk-drag {\n opacity:0.7 !important;\n }\n\n path, .jtk-endpoint {\n cursor: pointer;\n }\n\n .jtk-overlay {\n background-color:transparent;\n }\n</style>\n\n<template>\n <div class=\"grid-bg dualbox-graph-container dark\" @keyup.ctrl.89=\"redo()\" @keyup.ctrl.90=\"undo()\" @keyup.ctrl.46=\"deleteSelection()\" @keyup.ctrl.67=\"copySelection()\" @keyup.ctrl.86=\"pasteSelection()\" tabindex=1>\n <div id=\"dualbox-graph-canvas\" class=\"canvas-wide jtk-surface jtk-surface-nopan contextmenu dualbox-graph-canvas\">\n <graph-node v-for=\"(def, id) in app.input\" :key=\"getNodeUniqId(id, 'input')\" :id=\"getNodeId(id, 'input')\" :pkg=\"getNode(id, 'input').getPackage()\" :n=\"getNode(id, 'input')\" :ref=\"getNodeId(id, 'input')\"></graph-node>\n <graph-node v-for=\"(def, id) in app.output\" :key=\"getNodeUniqId(id, 'output')\" :id=\"getNodeId(id, 'output')\" :pkg=\"getNode(id, 'output').getPackage()\" :n=\"getNode(id, 'output')\" :ref=\"getNodeId(id, 'output')\"></graph-node>\n <graph-node v-for=\"(def, id) in app.modules\" :key=\"getNodeUniqId(id, 'module')\" :id=\"getNodeId(id, 'module')\" :pkg=\"getNode(id, 'module').getPackage()\" :n=\"getNode(id, 'module')\" :ref=\"getNodeId(id, 'module')\"></graph-node>\n <graph-node v-for=\"(def, id) in app.ui\" :key=\"getNodeUniqId(id, 'ui')\" :id=\"getNodeId(id, 'ui')\" :pkg=\"getNode(id, 'ui').getPackage()\" :n=\"getNode(id, 'ui')\" :ref=\"getNodeId(id, 'ui')\" :displayEvents=\"displayEvents\"></graph-node>\n </div>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-topleft\" style=\"position: absolute; top: 0; left: 0px; border-radius: 0px; border-top-left-radius: 5px; margin: 0;\" v-on:click=\"gotoTopLeft\"><i class=\"fa fa-arrows-alt\"></i></button>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-topright\" style=\"position: absolute; top: 0; right: 0px; border-radius: 0px; border-top-right-radius: 5px; margin: 0;\" v-on:click=\"gotoTopRight\"><i class=\"fa fa-arrows-alt\"></i></button>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-bottomleft\" style=\"position: absolute; bottom: 30px; left: 0px; border-radius: 0px; border-bottom-left-radius: 5px; margin: 0;\" v-on:click=\"gotoBottomLeft\"><i class=\"fa fa-arrows-alt\"></i></button>\n <button class=\"btn btn-editor-xs btn-outline-dark btn-graph-goto btn-goto-bottomright\" style=\"position: absolute; bottom: 30px; right: 0px; border-radius: 0px; border-bottom-right-radius: 5px; margin: 0;\" v-on:click=\"gotoBottomRight\"><i class=\"fa fa-arrows-alt\"></i></button>\n </div>\n</template>\n\n<script>\n// Utils\nimport _ from 'lodash-es';\nimport dbutils from '../../m/DualboxUtils';\nimport Utils from '../Utils';\nimport swal from 'sweetalert2';\n\n// Sub-components\nimport graphNodeVue from './graphNode.vue';\n\n// Canvas control helpers\nimport Zoomer from '../Zoomer';\nimport Selector from '../Selector';\nimport Translater from '../Translater';\nimport CanvasSizeHandler from '../CanvasSizeHandler';\nimport ContextMenu from '../ContextMenu';\n\n// jsplumb style definitins\nimport PlumbStyle from '../PlumbStyle';\n\nexport default {\n props: [\n \"app\", // the app model\n \"displayEvents\" // display events or not\n ],\n components: {\n 'graph-node' : graphNodeVue,\n },\n data: function () {\n return {\n utils: Utils,\n loading: false,\n }\n },\n created: function() {\n this.view = window.dualboxEditor.v;\n this.jsPlumbInstance = null;\n\n // Controls helpers utilities\n this.selector = null;\n this.translater = null;\n this.zoomer = null;\n this.canvasSizeHandler = null;\n\n // encapsulate the JSPlumb styles\n this.style = new PlumbStyle(this);\n },\n mounted: async function() {\n this.jsPlumbInstance = jsPlumb.getInstance({\n DragOptions: { cursor: 'pointer', zIndex: 2500 }, // default drag options\n Container: \"dualbox-graph-canvas\"\n });\n this.jsPlumbInstance.rand = dbutils.randomString(8);\n this.style.setDefault();\n this.graphContainer = $(this.$el);\n this.canvas = $(this.$el).find('.dualbox-graph-canvas');\n\n // TODO: remove theses definitions from the graphview object (bugs when entering metanode)\n this.view.selector = this.selector = new Selector(this, this.graphContainer);\n this.view.translater = this.translater = new Translater(this, this.graphContainer, this.canvas);\n this.view.zoomer = this.zoomer = new Zoomer(this, this.graphContainer, this.canvas);\n this.view.canvasSizeHandler = this.canvasSizeHandler = new CanvasSizeHandler(this, this.canvas);\n this.assignContextMenu();\n\n // autofocus editor when the div is clicked (allow to use keydown)\n // We use this way because it's easier than to set a tabindex on the dualbox-graph-container\n // to capture keyboard events (will break modals)\n var self = this;\n this.keyDownHandler = function(e) {\n // Ensure event is not null\n e = e || window.event;\n\n if( $(self.$el).is(\":hover\") ) {\n if ((e.which == 90 || e.keyCode == 90) && e.ctrlKey) { // ctrl + z\n self.view.c.undo();\n }\n else if((e.which == 89 || e.keyCode == 89) && e.ctrlKey) { // ctrl + y\n self.view.c.redo();\n }\n else if((e.which == 46 || e.keyCode == 46)) { // suppr\n self.view.c.deleteSelection();\n }\n else if((e.which == 67 || e.keyCode == 67) && e.ctrlKey) { // ctrl + c\n self.view.c.copySelection();\n }\n else if((e.which == 86 || e.keyCode == 86) && e.ctrlKey) { // ctrl + v\n self.view.c.pasteSelection();\n }\n else if((e.which == 88 || e.keyCode == 88) && e.ctrlKey) { // ctrl + x\n self.view.c.copySelection();\n self.view.c.deleteSelection();\n }\n }\n }\n\n $(document).on(\"keydown\", this.keyDownHandler);\n return await this.initializeListeners();\n },\n destroyed: function() {\n $(document).off(\"keydown\", this.keyDownHandler);\n },\n beforeUpdate: async function() {\n // Reset the JSPlumb instance\n this.resetJsPlumb();\n this.refreshBoxes();\n },\n updated: async function() {\n // don't do anything for out-of-date updates (hash is not matching)\n console.log('Updating graph !');\n\n //this.canvas.empty();\n ContextMenu.clean();\n this.assignContextMenu();\n\n // We need to add links and events to the nodes\n\n // Function to gather all links from a node into the \"links\" array\n var links = []; // array of links\n var gatherLinks = (targetId, sourceDef) => {\n // for nodes and uis\n _.each(sourceDef.links, (sources, targetInput) => {\n _.each(sources, (sourceOutput, sourceId) => {\n if( sourceId === \"graph\" ) {\n return;\n }\n\n if( sourceId === \"input\" || sourceId === \"output\" ) {\n sourceId = this.view.m.prefixId(sourceOutput, sourceId);\n sourceOutput = \"value\";\n }\n\n links.push({\n sourceId: sourceId,\n sourceOutput: sourceOutput,\n targetId: targetId,\n targetInput: targetInput\n });\n });\n });\n\n // for inputs and outputs\n _.each(sourceDef.link, (sourceOutput, sourceId) => {\n if( sourceId === \"graph\" ) return;\n if( sourceId === \"input\" || sourceId === \"output\" ) {\n sourceId = this.view.m.prefixId(sourceOutput, sourceId);\n sourceOutput = \"value\";\n }\n\n links.push({\n sourceId: sourceId,\n sourceOutput: sourceOutput,\n targetId: targetId,\n targetInput: \"value\"\n });\n });\n }\n\n // Function to gather all events from a node into the \"links\" array\n var events = [];\n var gatherEvents = (sourceId, def) => {\n _.each(def.events, (evt) => {\n if( evt.node ) {\n events.push({\n sourceId: sourceId,\n targetId: evt.node,\n name: evt.event\n });\n }\n });\n }\n\n _.each(this.app.input, (def, id) => {\n id = this.view.m.prefixId(id, \"input\");\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n _.each(this.app.output, (def, id) => {\n id = this.view.m.prefixId(id, \"output\");\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n _.each(this.app.modules, (def, id) =>{\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n _.each(this.app.ui, (def, id) =>{\n gatherLinks(id, def); // add all data links in our array\n gatherEvents(id, def); // add all event links in our array\n });\n\n // Now, we gathered all links and events. Add them into jsPlumb\n // add the links\n _.each(links, (link) => {\n this.addLink(link.sourceId, link.sourceOutput, link.targetId, link.targetInput);\n });\n\n // add the events\n if( this.displayEvents ) {\n _.each(events, (e) => {\n this.addEvent(e.sourceId, e.targetId, e.name);\n });\n }\n\n await this.canvasSizeHandler.resize();\n\n // Initialize all listeners again\n await this.initializeJsPlumbListeners();\n\n return new Promise((resolve) => this.jsPlumbInstance.ready(resolve));\n },\n methods: {\n deleteSelection() {\n this.view.c.deleteSelection();\n },\n\n copySelection() {\n this.view.c.copySelection();\n },\n\n pasteSelection() {\n this.view.c.pasteSelection();\n },\n\n getCurrentMousePosition(e) {\n var x = e.pageX - $(this.$el).find('#dualbox-graph-canvas').offset().left;\n var y = e.pageY - $(this.$el).find('#dualbox-graph-canvas').offset().top;\n x = x / this.zoomer.getZoom();\n y = y / this.zoomer.getZoom();\n return {\n \"top\": y,\n \"left\": x\n };\n },\n\n resetJsPlumb : function() {\n if( this.jsPlumbInstance ) {\n this.jsPlumbInstance.reset();\n delete this.jsPlumbInstance.initializedNodes;\n }\n\n window.jsPlumbInstance = this.jsPlumbInstance = jsPlumb.getInstance({\n DragOptions: { cursor: 'pointer', zIndex: 2500 }, // default drag options\n Container: \"dualbox-graph-canvas\"\n });\n\n this.jsPlumbInstance.rand = dbutils.randomString(8); // just change the node refs to avoid vue.js optimizations\n this.style.setDefault();\n },\n\n refreshBoxes: function() {\n _.each(this.$refs, (component, name) => {\n var c = component[0];\n if(c) c.$forceUpdate();\n })\n },\n\n // bind jsPlumb events\n initializeListeners: async function() {\n this.selector.initialize();\n this.zoomer.initialize();\n ContextMenu.initialize(); // important to make this after the translater (right-clic conflicts)\n this.translater.initialize();\n\n // show selected nodes\n this.selector.onSelecting((divs) => {\n $(this.canvas).find('.card').removeClass('selected');\n _.each(divs, (div) => {\n $(div).addClass('selected');\n });\n });\n\n // Add every node in the selected area to JsPlumb selection\n this.selector.onSelect((divs) => {\n _.each(divs, (div) => {\n $(div).addClass('selected');\n this.jsPlumbInstance.addToDragSelection($(div)[0]);\n });\n });\n\n // remove the selection\n this.selector.onDeselect((divs) => {\n _.each(divs, (div) => {\n $(div).removeClass('selected');\n this.jsPlumbInstance.removeFromDragSelection($(div)[0]);\n });\n });\n\n return await this.initializeJsPlumbListeners();\n },\n\n initializeJsPlumbListeners: function() {\n this.refreshGraphNode = function(id) {\n var nodeVue = this.$refs[id];\n if( nodeVue[0] ) nodeVue[0].$forceUpdate();\n }\n\n this.jsPlumbInstance.bind(\"connectionDrag\", (connection) => {\n // hide the type of the source (output endpoint)\n var endpoints = connection.getAttachedElements();\n if( endpoints && endpoints[0] ) this.style.hideType(endpoints[0]);\n\n // if the connection linked a source to a target, remove the connection\n var c = connection.getParameters();\n if( c.target ) {\n if( c.type === \"data\" ) {\n // remove the link\n this.view.m.removeDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n }\n else if( c.type === \"event\" ) {\n this.view.m.removeEventLink(c.source.id, c.target.id, c.event);\n this.view.setBoxMenu(c.source.id);\n }\n }\n });\n\n this.jsPlumbInstance.bind(\"connectionDragStop\", (connection) => {\n // if the connection has not been made, show back the type of the source\n var endpoints = connection.getAttachedElements();\n if( endpoints && endpoints.length == 2 ) {\n var sourceEP = endpoints[0];\n var targetEP = endpoints[1];\n if( targetEP && targetEP.elementId.startsWith('jsPlumb')) {\n if( targetEP.elementId.startsWith('jsPlumb') ) {\n // no connection has been made, show back our output type\n var sourceOutput = connection.getParameters().source.output;\n if( !this.view.m.getNode(sourceEP.elementId).isOutputConnected(sourceOutput) ) {\n this.style.showType(sourceEP);\n }\n }\n }\n }\n });\n\n this.jsPlumbInstance.bind(\"connection\", (info, event) => {\n // only do this for connection established by user, no programmatically\n if( event ) {\n var c = info.connection.getParameters();\n if( c.type === \"data\" ) {\n // check the types\n var sourceType = this.view.m.getNode(c.source.id).getOutputType(c.source.output);\n var targetType = this.view.m.getNode(c.target.id).getInputType(c.target.input);\n if( this.view.m.compareTypes(sourceType, targetType) ) {\n this.view.m.addDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n\n // the target need to be refreshed\n this.refreshGraphNode(c.target.id);\n\n // hide source and target types\n this.style.hideType(info.sourceEndpoint);\n this.style.hideType(info.targetEndpoint);\n }\n else {\n // Error\n this.jsPlumbInstance.deleteConnection(info.connection, {\n fireEvent: false, //fire a connection detached event?\n forceDetach: false //override any beforeDetach listeners\n });\n this.style.showType(info.targetEndpoint);\n if( !this.view.m.getNode(info.sourceId).isOutputConnected(c.source.output) ) {\n this.style.showType(info.sourceEndpoint);\n }\n\n swal({\n title: \"Nope.\",\n 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.\",\n type: \"error\",\n });\n }\n }\n else if( c.type === \"event\" ) {\n var target = this.view.m.getNode(c.target.id);\n var eventNames = target.getEventsNames();\n var options = {};\n _.each(eventNames, (name) => options[name] = name );\n\n swal({\n input: 'select',\n title: 'Select an event',\n inputOptions: options\n }).then( (result) => {\n if( result.value ) {\n info.connection.setParameter('event', result.value);\n info.connection.setLabel({ label: result.value, cssClass: \"connection-label noselect\", location: 0.5 });\n this.view.m.addEventLink(c.source.id, c.target.id, result.value);\n\n this.view.setBoxMenu(c.source.id);\n }\n });\n }\n }\n });\n\n this.jsPlumbInstance.bind(\"connectionDetached\", (info, event) => {\n var c = info.connection.getParameters();\n if( c.type === \"data\" ) {\n // remove the link\n this.view.m.removeDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n\n // show source and target type\n this.style.showType(info.targetEndpoint);\n if( !info.source.id.startsWith('connection-split') &&\n !this.view.m.getNode(info.source.id).isOutputConnected(c.source.output) ) {\n this.style.showType(info.sourceEndpoint);\n }\n\n // the target need to be refreshed\n this.refreshGraphNode(c.target.id);\n }\n else if( c.type === \"event\" ) {\n this.view.m.removeEventLink(c.source.id, c.target.id, c.event);\n //this.style.showEventName(info.targetEndpoint);\n }\n\n // if the connection is splitted, remove all other connections/endpoint\n if( c.splitted ) {\n var connections = this.jsPlumbInstance.getAllConnections();\n var connToDelete = [];\n var EPToDelete = {};\n _.each(connections, conn => {\n var p = conn.getParameters();\n if( p.source.id === c.source.id &&\n p.target.id === c.target.id &&\n ((c.type == \"event\" && p.event === c.event) ||\n (c.type == \"data\" && p.source.sourceOutput == c.source.sourceOutput &&\n p.target.targetInput === p.target.targetInput))) {\n // collect the connection and the endpoints to delete\n connToDelete.push(conn);\n\n let [sourceEP, targetEP] = conn.getAttachedElements();\n if( sourceEP.getUuid().startsWith('connection-split-') ) {\n EPToDelete[ sourceEP.getUuid() ] = sourceEP;\n }\n if( targetEP.getUuid().startsWith('connection-split-') ) {\n EPToDelete[ targetEP.getUuid() ] = targetEP;\n }\n }\n });\n\n // clean all theses connections and endpoints once this call is ended\n _.each(connToDelete, conn => {\n _.defer(()=> {\n this.jsPlumbInstance.deleteConnection(conn, {\n fireEvent: false, //fire a connection detached event?\n forceDetach: false //override any beforeDetach listeners\n });\n });\n });\n\n _.each(EPToDelete, (endpoint, uuid) => {\n _.defer(()=> {\n var div = endpoint.getElement();\n this.jsPlumbInstance.deleteEndpoint(endpoint);\n $(div).remove();\n });\n });\n }\n });\n\n this.jsPlumbInstance.bind(\"dblclick\", async (connection, e) => {\n let [sourceEP, targetEP] = connection.getAttachedElements();\n let c = connection.getParameters();\n\n // split this connection into 2 by creating an endpoint at the event coordinate\n // remove 7px to align it to the center\n var pos = {\n \"top\" : e.pageY - $(this.canvas).offset().top -7,\n \"left\": e.pageX - $(this.canvas).offset().left - 7\n };\n this.splitConnection(connection, pos); // return the endpoint exact position\n\n // save to the graph\n if( c.type == \"data\" ) {\n var link = this.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n }\n else {\n var link = this.m.getEventLink(c.source.id, c.target.id, c.event);\n }\n\n if( sourceEP.getUuid().startsWith('connection-split-') ) {\n var el = sourceEP.getElement();\n var prevPos = this.jsPlumbInstance.getPosition(el);\n link.addPathStep(prevPos, pos);\n }\n else {\n link.addPathStep( \"source\", pos );\n }\n });\n\n return new Promise(resolve => this.jsPlumbInstance.ready(resolve));\n },\n\n addLink: function(sourceId, sourceOutput, targetId, targetInput) {\n var sourceUuid = [sourceId, \"output\", sourceOutput].join('#');\n var targetUuid = [targetId, \"input\", targetInput].join('#');\n\n // hide source and target types\n this.style.hideType(sourceUuid); // works with endpoint or uuid\n this.style.hideType(targetUuid);\n\n var connection = this.jsPlumbInstance.connect({\n uuids: [sourceUuid, targetUuid],\n editable: true,\n paintStyle: this.style.connectorData.paintStyle,\n cssClass: 'data-connection',\n connectorClass: 'data-connection'\n });\n\n // split the connection if we need to\n var datalink = this.view.m.getDataLink(sourceId, sourceOutput, targetId, targetInput);\n var path = datalink.getPath();\n _.each( path, (pos) => {\n var r = this.splitConnection(connection, pos);\n connection = r.c2; // switch to the 2nd child connection\n });\n },\n\n addEvent: function(sourceId, targetId, name) {\n var sourceUuid = [sourceId, \"event-out\"].join('#');\n var sourceEP = this.jsPlumbInstance.getEndpoint(sourceUuid);\n\n var connection = this.jsPlumbInstance.connect({\n source: sourceEP,\n target: targetId,\n editable: true,\n paintStyle: this.style.connectorData.paintStyle,\n cssClass: 'event-connection',\n connectorClass: 'event-connection',\n overlays: [\n [ \"PlainArrow\", { location: 1, width:10, length:10, gap: 12 }],\n [ \"Label\", { label: name, location:0.50, labelStyle: { cssClass: \"connection-label noselect\" } } ]\n ]\n });\n connection.setParameter(\"event\", name);\n\n // split the connection if we need to\n var eventlink = this.view.m.getEventLink(sourceId, targetId, name);\n var path = eventlink.getPath();\n _.each( path, (pos) => {\n var r = this.splitConnection(connection, pos);\n connection = r.c2; // switch to the 2nd child connection\n });\n },\n\n // split a connection by adding an endpoint at given pos\n splitConnection: function( connection, pos ) {\n var c = connection.getParameters();\n\n // create the endpoint div\n var id = \"connection-split-\" + dbutils.randomString(8);\n var div = $('<div/>', {\n id: id,\n class: \"connection-control capture-left-click capture-right-click\"\n });\n $(this.canvas).append(div);\n\n // position it at pos (adjust for the height of the div)\n var jsPlumbElement = this.jsPlumbInstance.getElement(id);\n this.jsPlumbInstance.setPosition(jsPlumbElement, pos);\n\n // make this div draggable\n var startPosition = pos;\n this.jsPlumbInstance.draggable(div, {\n start: (e) => {\n startPosition = this.jsPlumbInstance.getPosition(jsPlumbElement);\n },\n stop: (e) => {\n var stopPosition = this.jsPlumbInstance.getPosition(jsPlumbElement);\n if( c.type == \"data\" ) {\n var link = this.view.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n }\n else if( c.type == \"event\" ) {\n var link = this.view.m.getEventLink(c.source.id, c.target.id, c.event);\n }\n link.updatePathStep(startPosition, stopPosition);\n }\n });\n\n // determine the style of the 2 connections\n c.splitted = true; // indicates that this connection is splitted\n if( c.type == \"data\" ) {\n var connectionStyle = {\n paintStyle: this.style.splittedConnectorData.paintStyle,\n connector: this.style.splittedConnectorData.connector,\n connectorOverlays:this.style.splittedConnectorData.overlays,\n connectorStyle: this.style.splittedConnectorData.paintStyle,\n hoverPaintStyle: this.style.endpointData.hoverStyle,\n cssClass : 'data-connection'\n }\n var epStyle = this.style.dataLineSplitterEndpoint;\n }\n else if( c.type == \"event\" ) {\n var connectionStyle = {\n paintStyle: this.style.eventConnectorData.paintStyle,\n connector: this.style.eventConnectorData.connector,\n connectorOverlays:this.style.eventConnectorData.overlays,\n connectorStyle: this.style.eventConnectorData.paintStyle,\n hoverPaintStyle: this.style.eventEndpointData.hoverStyle,\n cssClass : 'event-connection',\n };\n var epStyle = this.style.eventLineSplitterEndpoint;\n }\n\n // create the div endpoint\n var ep = this.jsPlumbInstance.addEndpoint(id, {\n isSource : false,\n isTarget : false,\n uuid : id,\n anchor : \"Center\",\n maxConnections : 2,\n parameters : c\n }, epStyle);\n\n // make the ep transparent to pointer events, left click on it must drag the div\n $(ep.canvas).css('pointer-events', 'none');\n $(ep.canvas).css('cursor', 'move');\n $(ep.canvas).addClass('capture-left-click');\n $(ep.canvas).addClass('capture-right-click');\n $(ep.canvas).find('svg').attr('pointer-events', 'none');\n $(ep.canvas).addClass(id);\n\n\n // save the connection overlays\n var overlays = connection.getOverlays();\n var overlaysParams = [];\n _.each(overlays, (o)=> {\n overlaysParams.push([o.type, {\n \"location\" : o.loc,\n \"width\" : o.width,\n \"length\" : o.length,\n \"label\" : o.getLabel ? o.getLabel() : undefined,\n \"id\" : o.id,\n \"labelStyle\": o.labelStyle\n }]);\n });\n\n let [sourceEP, targetEP] = connection.getAttachedElements();\n let targetId = connection.targetId;\n\n // detach the original connection\n this.jsPlumbInstance.deleteConnection(connection, { fireEvent: false, forceDetach: false });\n\n // now reattach it into 2 connections\n var c1 = this.jsPlumbInstance.connect({\n source: sourceEP,\n target: ep,\n editable: true,\n }, connectionStyle);\n\n var c2 = this.jsPlumbInstance.connect({\n source: ep,\n target: targetEP._jsPlumb ? targetEP : targetId /* EP has been destroyed, reattach to window */,\n editable: true,\n }, connectionStyle);\n\n sourceEP.setParameter(\"targetConnection\", c1);\n ep.setParameter(\"sourceConnection\", c1);\n ep.setParameter(\"targetConnection\", c2);\n targetEP.setParameter(\"sourceConnection\", c2);\n\n // add the overlays to c2\n _.each(overlaysParams, (p)=> {\n c2.addOverlay(p);\n });\n\n // add menu to remove this split\n $(ep.canvas).ready(() => {\n var menu = new ContextMenu(\"#\" + id, [\n {\n name: 'Remove the split',\n fn: () => {\n this.unsplitConnection(ep);\n }\n },\n ]);\n });\n\n return {\n pos: pos,\n c1 : c1,\n c2 : c2\n }\n },\n\n // Remove a split in a connection\n // ep: entrypoint\n // c1: connection 1\n // c2: connection 2\n unsplitConnection: function(ep) {\n var c = ep.getParameters();\n var c1 = c.sourceConnection;\n var c2 = c.targetConnection;\n var sourceEP = c1.getAttachedElements()[0];\n var targetEP = c2.getAttachedElements()[1];\n\n // detach and delete the endpoint\n this.jsPlumbInstance.deleteConnection(c1, { fireEvent: false, forceDetach: false });\n this.jsPlumbInstance.deleteConnection(c2, { fireEvent: false, forceDetach: false });\n this.jsPlumbInstance.deleteEndpoint(ep);\n\n\n // reconnect directly the source and the target\n var connection = this.jsPlumbInstance.connect({\n source: sourceEP,\n target: targetEP,\n editable: true,\n });\n\n sourceEP.setParameter('targetConnection', connection);\n targetEP.setParameter('sourceConnection', connection);\n\n // find the position of the split endpoint to remove\n var id = c1.targetId;\n var element = this.jsPlumbInstance.getElement(id)\n var position = this.jsPlumbInstance.getPosition(element);\n\n // remove the element in the path\n // 1. find the link\n // 2. find the position of the split endpoint\n if( c.type == \"data\" ) {\n var link = this.view.m.getDataLink(c.source.id, c.source.output, c.target.id, c.target.input);\n link.removePathStep(position);\n }\n else if( c.type == \"event\" ) {\n var link = this.m.getEventLink();\n link.removePathStep(position);\n }\n else {\n throw \"unknown connection type\";\n }\n\n // remove the div\n $(\"#\" + id).remove(); // delete the div\n },\n\n getNodeId: function( id, nodeType ) {\n switch( nodeType ) {\n case \"input\": return this.view.m.prefixId(id, \"input\");\n case \"output\": return this.view.m.prefixId(id, \"output\");\n case \"module\":\n case \"ui\":\n default:\n return id;\n }\n },\n\n getNode( id, nodeType ) {\n var realId = this.getNodeId(id, nodeType);\n return this.view.m.getNode(realId);\n },\n\n getNodeUniqId(id, nodeType) {\n return this.getNode(id, nodeType).getUniqId() + this.jsPlumbInstance.rand;\n },\n\n setLoading(b) {\n this.loading = b;\n },\n\n gotoTopLeft: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoTopLeft();\n },\n\n gotoTopRight: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoTopRight();\n },\n\n gotoBottomLeft: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoBottomLeft();\n },\n\n gotoBottomRight: function(e) {\n e.preventDefault();\n e.stopPropagation();\n this.translater.gotoBottomRight();\n },\n\n assignContextMenu: function() {\n // Create a contextmenu for the div\n var contextOptions = [\n {\n name: 'Add node...',\n fn: (target, evt) => {\n this.$parent.addBox(evt);\n }\n }\n ];\n var cMenu = new ContextMenu(\"#dualbox-graph-canvas\", contextOptions);\n },\n }\n}\n</script>\n"]}, media: undefined });
49394
49484
 
49395
49485
  };
49396
49486
  /* scoped */
@@ -89862,12 +89952,140 @@ class GraphController {
89862
89952
  this.v.repaint();
89863
89953
  }
89864
89954
 
89955
+ setClipboard(str) {
89956
+ const el = document.createElement('textarea');
89957
+ el.value = str;
89958
+ document.body.appendChild(el);
89959
+ el.select();
89960
+ document.execCommand('copy');
89961
+ document.body.removeChild(el);
89962
+ }
89963
+
89964
+ async getClipboard() {
89965
+ return await navigator.clipboard.readText();
89966
+ }
89967
+
89865
89968
  deleteSelection() {
89969
+ this.m.batch(() => {
89970
+ var selectedDivs = this.v.selector.getSelection();
89971
+ lodash.each(selectedDivs, (div) => {
89972
+ this.m.getNode($(div).attr('id')).remove();
89973
+ });
89974
+ });
89975
+ this.v.repaint();
89976
+ }
89977
+
89978
+ copySelection() {
89979
+ var nodes = [];
89980
+ var json = {
89981
+ object: "dualbox c/c",
89982
+ modules: {},
89983
+ ui: {},
89984
+ input: {},
89985
+ output: {},
89986
+ metanodes: {},
89987
+ };
89866
89988
  var selectedDivs = this.v.selector.getSelection();
89989
+
89990
+ var isInSelection = nodeId => {
89991
+ var r = false;
89992
+ lodash.each(nodes, n => {
89993
+ if (n.graphId === nodeId) {
89994
+ r = true;
89995
+ return false; // eol
89996
+ }
89997
+ });
89998
+ return r;
89999
+ };
90000
+
90001
+ // strip a node definition from it's content that is outside
90002
+ // of the scope of selection
90003
+ var stripContentOutsideOfSelection = (n) => {
90004
+ var def = lodash.clone(n.def);
90005
+
90006
+ // delete links from outside
90007
+ var links = n.getInboundLinks();
90008
+ lodash.each(links, (l) => {
90009
+ var sourceNode = l.getSourceNode();
90010
+ if (!isInSelection(sourceNode.graphId)) {
90011
+ if (n.isInput() || n.isOutput()) {
90012
+ delete def.link;
90013
+ } else {
90014
+ delete def.links[l.targetInput];
90015
+ }
90016
+ }
90017
+ });
90018
+
90019
+ // delete events and UI stuff
90020
+ delete def.events;
90021
+ delete def.registerTo;
90022
+
90023
+ return def;
90024
+ };
90025
+
90026
+ // first get nodes
89867
90027
  lodash.each(selectedDivs, (div) => {
89868
- this.m.getNode($(div).attr('id')).remove();
90028
+ var n = this.m.getNode($(div).attr('id'));
90029
+ nodes.push(n);
89869
90030
  });
89870
- this.v.repaint();
90031
+
90032
+
90033
+ // add node, but strip content
90034
+ lodash.each(nodes, n => {
90035
+ if (n.isInput()) {
90036
+ json.input[n.graphId] = stripContentOutsideOfSelection(n);
90037
+ } else if (n.isOutput()) {
90038
+ json.output[n.graphId] = stripContentOutsideOfSelection(n);
90039
+ } else if (n.isUI()) {
90040
+ json.ui[n.graphId] = stripContentOutsideOfSelection(n);
90041
+ } else if (n.isModule()) {
90042
+ json.modules[n.graphId] = stripContentOutsideOfSelection(n);
90043
+ if (n.isMetanode()) {
90044
+ json.metanodes[n.def.module] = n.getMetanodeDefinition();
90045
+ }
90046
+ }
90047
+ });
90048
+
90049
+ this.setClipboard(JSON.stringify(json));
90050
+
90051
+ // indicates to the user that the selection is saved
90052
+ const Toast = sweetalert2_all.mixin({
90053
+ toast: true,
90054
+ position: 'bottom-end',
90055
+ showConfirmButton: false,
90056
+ timer: 2000,
90057
+ timerProgressBar: true,
90058
+ onOpen: (toast) => {
90059
+ toast.addEventListener('mouseenter', sweetalert2_all.stopTimer);
90060
+ toast.addEventListener('mouseleave', sweetalert2_all.resumeTimer);
90061
+ }
90062
+ });
90063
+
90064
+ Toast.fire({
90065
+ icon: 'success',
90066
+ title: 'Selection copied to clipboard'
90067
+ });
90068
+ }
90069
+
90070
+ async pasteSelection() {
90071
+ var clipboard = await this.getClipboard();
90072
+ try {
90073
+ var json = JSON.parse(clipboard);
90074
+ } catch (e) {
90075
+ sweetalert2_all.fire("Error", "You must first copy boxes from Dualbox");
90076
+ }
90077
+
90078
+ sweetalert2_all.queue([{
90079
+ title: 'Dependencies required',
90080
+ text: 'Dualbox will load additional required dependencies to paste your selection',
90081
+ showLoaderOnConfirm: true,
90082
+ preConfirm: async () => {
90083
+ await this.e.loadPackages(json);
90084
+ this.m.paste(json);
90085
+ await this.v.repaint();
90086
+ }
90087
+ }]);
90088
+
89871
90089
  }
89872
90090
 
89873
90091
  mergeSelection() {