| 
 | |
| let util = require('../../util'); | |
| let Hammer = require('../../module/hammer'); | |
| let hammerUtil = require('../../hammerUtil'); | |
| 
 | |
| /** | |
|  * clears the toolbar div element of children | |
|  * | |
|  * @private | |
|  */ | |
| class ManipulationSystem { | |
|   constructor(body, canvas, selectionHandler) { | |
|     this.body = body; | |
|     this.canvas = canvas; | |
|     this.selectionHandler = selectionHandler; | |
| 
 | |
|     this.editMode = false; | |
|     this.manipulationDiv = undefined; | |
|     this.editModeDiv     = undefined; | |
|     this.closeDiv        = undefined; | |
| 
 | |
|     this.manipulationHammers = []; | |
|     this.temporaryUIFunctions = {}; | |
|     this.temporaryEventFunctions = []; | |
| 
 | |
|     this.touchTime = 0; | |
|     this.temporaryIds = {nodes: [], edges:[]}; | |
|     this.guiEnabled = false; | |
|     this.inMode = false; | |
|     this.selectedControlNode = undefined; | |
| 
 | |
|     this.options = {}; | |
|     this.defaultOptions = { | |
|       enabled: false, | |
|       initiallyActive: false, | |
|       addNode: true, | |
|       addEdge: true, | |
|       editNode: undefined, | |
|       editEdge: true, | |
|       deleteNode: true, | |
|       deleteEdge: true, | |
|       controlNodeStyle:{ | |
|         shape:'dot', | |
|         size:6, | |
|         color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968', border: '#3c3c3c'}}, | |
|         borderWidth: 2, | |
|         borderWidthSelected: 2 | |
|       } | |
|     }; | |
|     util.extend(this.options, this.defaultOptions); | |
| 
 | |
|     this.body.emitter.on('destroy',     () => {this._clean();}); | |
|     this.body.emitter.on('_dataChanged',this._restore.bind(this)); | |
|     this.body.emitter.on('_resetData',  this._restore.bind(this)); | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. | |
|    * @private | |
|    */ | |
|   _restore() { | |
|     if (this.inMode !== false) { | |
|       if (this.options.initiallyActive === true) { | |
|         this.enableEditMode(); | |
|       } | |
|       else { | |
|         this.disableEditMode(); | |
|       } | |
|     } | |
|   } | |
| 
 | |
|   /** | |
|    * Set the Options | |
|    * @param options | |
|    */ | |
|   setOptions(options, allOptions, globalOptions) { | |
|     if (allOptions !== undefined) { | |
|       if (allOptions.locale !== undefined)  {this.options.locale = allOptions.locale}   else {this.options.locale = globalOptions.locale;} | |
|       if (allOptions.locales !== undefined) {this.options.locales = allOptions.locales} else {this.options.locales = globalOptions.locales;} | |
|     } | |
| 
 | |
|     if (options !== undefined) { | |
|       if (typeof options === 'boolean') { | |
|         this.options.enabled = options; | |
|       } | |
|       else { | |
|         this.options.enabled = true; | |
|         util.deepExtend(this.options, options); | |
|       } | |
|       if (this.options.initiallyActive === true) { | |
|         this.editMode = true; | |
|       } | |
|       this._setup(); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. | |
|    * | |
|    * @private | |
|    */ | |
|   toggleEditMode() { | |
|     if (this.editMode === true) { | |
|       this.disableEditMode(); | |
|     } | |
|     else { | |
|       this.enableEditMode(); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   enableEditMode() { | |
|     this.editMode = true; | |
| 
 | |
|     this._clean(); | |
|     if (this.guiEnabled === true) { | |
|       this.manipulationDiv.style.display = 'block'; | |
|       this.closeDiv.style.display = 'block'; | |
|       this.editModeDiv.style.display = 'none'; | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
|   disableEditMode() { | |
|     this.editMode = false; | |
| 
 | |
|     this._clean(); | |
|     if (this.guiEnabled === true) { | |
|       this.manipulationDiv.style.display = 'none'; | |
|       this.closeDiv.style.display = 'none'; | |
|       this.editModeDiv.style.display = 'block'; | |
|       this._createEditButton(); | |
|     } | |
|   } | |
| 
 | |
|   /** | |
|    * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. | |
|    * | |
|    * @private | |
|    */ | |
|   showManipulatorToolbar() { | |
|     // restore the state of any bound functions or events, remove control nodes, restore physics | |
|     this._clean(); | |
| 
 | |
|     // reset global variables | |
|     this.manipulationDOM = {}; | |
| 
 | |
|     // if the gui is enabled, draw all elements. | |
|     if (this.guiEnabled === true) { | |
|       // a _restore will hide these menus | |
|       this.editMode = true; | |
|       this.manipulationDiv.style.display = 'block'; | |
|       this.closeDiv.style.display = 'block'; | |
| 
 | |
|       let selectedNodeCount  = this.selectionHandler._getSelectedNodeCount(); | |
|       let selectedEdgeCount  = this.selectionHandler._getSelectedEdgeCount(); | |
|       let selectedTotalCount = selectedNodeCount + selectedEdgeCount; | |
|       let locale = this.options.locales[this.options.locale]; | |
|       let needSeperator = false; | |
| 
 | |
| 
 | |
|       if (this.options.addNode !== false) { | |
|         this._createAddNodeButton(locale); | |
|         needSeperator = true; | |
|       } | |
|       if (this.options.addEdge !== false) { | |
|         if (needSeperator === true) { | |
|           this._createSeperator(1); | |
|         } else { | |
|           needSeperator = true; | |
|         } | |
|         this._createAddEdgeButton(locale); | |
|       } | |
| 
 | |
|       if (selectedNodeCount === 1 && typeof this.options.editNode === 'function') { | |
|         if (needSeperator === true) { | |
|           this._createSeperator(2); | |
|         } else { | |
|           needSeperator = true; | |
|         } | |
|         this._createEditNodeButton(locale); | |
|       } | |
|       else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.editEdge !== false) { | |
|         if (needSeperator === true) { | |
|           this._createSeperator(3); | |
|         } else { | |
|           needSeperator = true; | |
|         } | |
|         this._createEditEdgeButton(locale); | |
|       } | |
| 
 | |
|       // remove buttons | |
|       if (selectedTotalCount !== 0) { | |
|         if (selectedNodeCount > 0 && this.options.deleteNode !== false) { | |
|           if (needSeperator === true) { | |
|             this._createSeperator(4); | |
|           } | |
|           this._createDeleteButton(locale); | |
|         } | |
|         else if (selectedNodeCount === 0 && this.options.deleteEdge !== false) { | |
|           if (needSeperator === true) { | |
|             this._createSeperator(4); | |
|           } | |
|           this._createDeleteButton(locale); | |
|         } | |
|       } | |
| 
 | |
|       // bind the close button | |
|       this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); | |
| 
 | |
|       // refresh this bar based on what has been selected | |
|       this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); | |
|     } | |
| 
 | |
|     // redraw to show any possible changes | |
|     this.body.emitter.emit('_redraw'); | |
| 
 | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Create the toolbar for adding Nodes | |
|    */ | |
|   addNodeMode() { | |
|     // when using the gui, enable edit mode if it wasnt already. | |
|     if (this.editMode !== true) { | |
|       this.enableEditMode(); | |
|     } | |
| 
 | |
|     // restore the state of any bound functions or events, remove control nodes, restore physics | |
|     this._clean(); | |
| 
 | |
|     this.inMode = 'addNode'; | |
|     if (this.guiEnabled === true) { | |
|       let locale = this.options.locales[this.options.locale]; | |
|       this.manipulationDOM = {}; | |
|       this._createBackButton(locale); | |
|       this._createSeperator(); | |
|       this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']); | |
| 
 | |
|       // bind the close button | |
|       this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); | |
|     } | |
| 
 | |
|     this._temporaryBindEvent('click', this._performAddNode.bind(this)); | |
|   } | |
| 
 | |
|   /** | |
|    * call the bound function to handle the editing of the node. The node has to be selected. | |
|    */ | |
|   editNode() { | |
|     // when using the gui, enable edit mode if it wasnt already. | |
|     if (this.editMode !== true) { | |
|       this.enableEditMode(); | |
|     } | |
| 
 | |
|     // restore the state of any bound functions or events, remove control nodes, restore physics | |
|     this._clean(); | |
|     let node = this.selectionHandler._getSelectedNode(); | |
|     if (node !== undefined) { | |
|       this.inMode = 'editNode'; | |
|       if (typeof this.options.editNode === 'function') { | |
|         if (node.isCluster !== true) { | |
|           let data = util.deepExtend({}, node.options, true); | |
|           data.x = node.x; | |
|           data.y = node.y; | |
| 
 | |
|           if (this.options.editNode.length === 2) { | |
|             this.options.editNode(data, (finalizedData) => { | |
|               if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'editNode') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { | |
|                 this.body.data.nodes.getDataSet().update(finalizedData); | |
|               } | |
|               this.showManipulatorToolbar(); | |
|             }); | |
|           } | |
|           else { | |
|             throw new Error('The function for edit does not support two arguments (data, callback)'); | |
|           } | |
|         } | |
|         else { | |
|           alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); | |
|         } | |
|       } | |
|       else { | |
|         throw new Error('No function has been configured to handle the editing of nodes.'); | |
|       } | |
|     } | |
|     else { | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * create the toolbar to connect nodes | |
|    */ | |
|   addEdgeMode() { | |
|     // when using the gui, enable edit mode if it wasnt already. | |
|     if (this.editMode !== true) { | |
|       this.enableEditMode(); | |
|     } | |
| 
 | |
|     // restore the state of any bound functions or events, remove control nodes, restore physics | |
|     this._clean(); | |
| 
 | |
|     this.inMode = 'addEdge'; | |
|     if (this.guiEnabled === true) { | |
|       let locale = this.options.locales[this.options.locale]; | |
|       this.manipulationDOM = {}; | |
|       this._createBackButton(locale); | |
|       this._createSeperator(); | |
|       this._createDescription(locale['edgeDescription'] || this.options.locales['en']['edgeDescription']); | |
| 
 | |
|       // bind the close button | |
|       this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); | |
|     } | |
| 
 | |
|     // temporarily overload functions | |
|     this._temporaryBindUI('onTouch',    this._handleConnect.bind(this)); | |
|     this._temporaryBindUI('onDragEnd',  this._finishConnect.bind(this)); | |
|     this._temporaryBindUI('onDrag',     this._dragControlNode.bind(this)); | |
|     this._temporaryBindUI('onRelease',  this._finishConnect.bind(this)); | |
| 
 | |
|     this._temporaryBindUI('onDragStart', () => {}); | |
|     this._temporaryBindUI('onHold', () => {}); | |
|   } | |
| 
 | |
|   /** | |
|    * create the toolbar to edit edges | |
|    */ | |
|   editEdgeMode() { | |
|     // when using the gui, enable edit mode if it wasn't already. | |
|     if (this.editMode !== true) { | |
|       this.enableEditMode(); | |
|     } | |
| 
 | |
|     // restore the state of any bound functions or events, remove control nodes, restore physics | |
|     this._clean(); | |
| 
 | |
|     this.inMode = 'editEdge'; | |
|     if (this.guiEnabled === true) { | |
|       let locale = this.options.locales[this.options.locale]; | |
|       this.manipulationDOM = {}; | |
|       this._createBackButton(locale); | |
|       this._createSeperator(); | |
|       this._createDescription(locale['editEdgeDescription'] || this.options.locales['en']['editEdgeDescription']); | |
| 
 | |
|       // bind the close button | |
|       this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); | |
|     } | |
| 
 | |
|     this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; | |
|     if (this.edgeBeingEditedId !== undefined) { | |
|       let edge = this.body.edges[this.edgeBeingEditedId]; | |
| 
 | |
|       // create control nodes | |
|       let controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); | |
|       let controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); | |
| 
 | |
|       this.temporaryIds.nodes.push(controlNodeFrom.id); | |
|       this.temporaryIds.nodes.push(controlNodeTo.id); | |
| 
 | |
|       this.body.nodes[controlNodeFrom.id] = controlNodeFrom; | |
|       this.body.nodeIndices.push(controlNodeFrom.id); | |
|       this.body.nodes[controlNodeTo.id] = controlNodeTo; | |
|       this.body.nodeIndices.push(controlNodeTo.id); | |
| 
 | |
|       // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI | |
|       this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this));    // used to get the position | |
|       this._temporaryBindUI('onTap', () => {});                             // disabled | |
|       this._temporaryBindUI('onHold', () => {});                             // disabled | |
|       this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this));// used to select control node | |
|       this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this));     // used to drag control node | |
|       this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this));  // used to connect or revert control nodes | |
|       this._temporaryBindUI('onMouseMove', () => {});                             // disabled | |
|  | |
|       // create function to position control nodes correctly on movement | |
|       // automatically cleaned up because we use the temporary bind | |
|       this._temporaryBindEvent('beforeDrawing', (ctx) => { | |
|         let positions = edge.edgeType.findBorderPositions(ctx); | |
|         if (controlNodeFrom.selected === false) { | |
|           controlNodeFrom.x = positions.from.x; | |
|           controlNodeFrom.y = positions.from.y; | |
|         } | |
|         if (controlNodeTo.selected === false) { | |
|           controlNodeTo.x = positions.to.x; | |
|           controlNodeTo.y = positions.to.y; | |
|         } | |
|       }); | |
| 
 | |
|       this.body.emitter.emit('_redraw'); | |
|     } | |
|     else { | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
|   /** | |
|    * delete everything in the selection | |
|    */ | |
|   deleteSelected() { | |
|     // when using the gui, enable edit mode if it wasnt already. | |
|     if (this.editMode !== true) { | |
|       this.enableEditMode(); | |
|     } | |
| 
 | |
|     // restore the state of any bound functions or events, remove control nodes, restore physics | |
|     this._clean(); | |
| 
 | |
|     this.inMode = 'delete'; | |
|     let selectedNodes = this.selectionHandler.getSelectedNodes(); | |
|     let selectedEdges = this.selectionHandler.getSelectedEdges(); | |
|     let deleteFunction = undefined; | |
|     if (selectedNodes.length > 0) { | |
|       for (let i = 0; i < selectedNodes.length; i++) { | |
|         if (this.body.nodes[selectedNodes[i]].isCluster === true) { | |
|           alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']); | |
|           return; | |
|         } | |
|       } | |
| 
 | |
|       if (typeof this.options.deleteNode === 'function') { | |
|         deleteFunction = this.options.deleteNode; | |
|       } | |
|     } | |
|     else if (selectedEdges.length > 0) { | |
|       if (typeof this.options.deleteEdge === 'function') { | |
|         deleteFunction = this.options.deleteEdge; | |
|       } | |
|     } | |
| 
 | |
|     if (typeof deleteFunction === 'function') { | |
|       let data = {nodes: selectedNodes, edges: selectedEdges}; | |
|       if (deleteFunction.length === 2) { | |
|         deleteFunction(data, (finalizedData) => { | |
|           if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'delete') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { | |
|             this.body.data.edges.getDataSet().remove(finalizedData.edges); | |
|             this.body.data.nodes.getDataSet().remove(finalizedData.nodes); | |
|             this.body.emitter.emit('startSimulation'); | |
|             this.showManipulatorToolbar(); | |
|           } | |
|           else { | |
|             this.body.emitter.emit('startSimulation'); | |
|             this.showManipulatorToolbar(); | |
|           } | |
|         }); | |
|       } | |
|       else { | |
|         throw new Error('The function for delete does not support two arguments (data, callback)') | |
|       } | |
|     } | |
|     else { | |
|       this.body.data.edges.getDataSet().remove(selectedEdges); | |
|       this.body.data.nodes.getDataSet().remove(selectedNodes); | |
|       this.body.emitter.emit('startSimulation'); | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
|   //********************************************** PRIVATE ***************************************// | |
|  | |
|   /** | |
|    * draw or remove the DOM | |
|    * @private | |
|    */ | |
|   _setup() { | |
|     if (this.options.enabled === true) { | |
|       // Enable the GUI | |
|       this.guiEnabled = true; | |
| 
 | |
|       this._createWrappers(); | |
|       if (this.editMode === false) { | |
|         this._createEditButton(); | |
|       } | |
|       else { | |
|         this.showManipulatorToolbar(); | |
|       } | |
|     } | |
|     else { | |
|       this._removeManipulationDOM(); | |
| 
 | |
|       // disable the gui | |
|       this.guiEnabled = false; | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * create the div overlays that contain the DOM | |
|    * @private | |
|    */ | |
|   _createWrappers() { | |
|     // load the manipulator HTML elements. All styling done in css. | |
|     if (this.manipulationDiv === undefined) { | |
|       this.manipulationDiv = document.createElement('div'); | |
|       this.manipulationDiv.className = 'vis-manipulation'; | |
|       if (this.editMode === true) { | |
|         this.manipulationDiv.style.display = 'block'; | |
|       } | |
|       else { | |
|         this.manipulationDiv.style.display = 'none'; | |
|       } | |
|       this.canvas.frame.appendChild(this.manipulationDiv); | |
|     } | |
| 
 | |
|     // container for the edit button. | |
|     if (this.editModeDiv === undefined) { | |
|       this.editModeDiv = document.createElement('div'); | |
|       this.editModeDiv.className = 'vis-edit-mode'; | |
|       if (this.editMode === true) { | |
|         this.editModeDiv.style.display = 'none'; | |
|       } | |
|       else { | |
|         this.editModeDiv.style.display = 'block'; | |
|       } | |
|       this.canvas.frame.appendChild(this.editModeDiv); | |
|     } | |
| 
 | |
| 
 | |
|     // container for the close div button | |
|     if (this.closeDiv === undefined) { | |
|       this.closeDiv = document.createElement('div'); | |
|       this.closeDiv.className = 'vis-close'; | |
|       this.closeDiv.style.display = this.manipulationDiv.style.display; | |
|       this.canvas.frame.appendChild(this.closeDiv); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * generate a new target node. Used for creating new edges and editing edges | |
|    * @param x | |
|    * @param y | |
|    * @returns {*} | |
|    * @private | |
|    */ | |
|   _getNewTargetNode(x,y) { | |
|     let controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); | |
| 
 | |
|     controlNodeStyle.id = 'targetNode' + util.randomUUID(); | |
|     controlNodeStyle.hidden = false; | |
|     controlNodeStyle.physics = false; | |
|     controlNodeStyle.x = x; | |
|     controlNodeStyle.y = y; | |
| 
 | |
|     // we have to define the bounding box in order for the nodes to be drawn immediately | |
|     let node = this.body.functions.createNode(controlNodeStyle); | |
|     node.shape.boundingBox = {left: x, right:x, top:y, bottom:y}; | |
| 
 | |
|     return node; | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Create the edit button | |
|    */ | |
|   _createEditButton() { | |
|     // restore everything to it's original state (if applicable) | |
|     this._clean(); | |
| 
 | |
|     // reset the manipulationDOM | |
|     this.manipulationDOM = {}; | |
| 
 | |
|     // empty the editModeDiv | |
|     util.recursiveDOMDelete(this.editModeDiv); | |
| 
 | |
| 
 | |
|     // create the contents for the editMode button | |
|     let locale = this.options.locales[this.options.locale]; | |
|     let button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit']); | |
|     this.editModeDiv.appendChild(button); | |
| 
 | |
|     // bind a hammer listener to the button, calling the function toggleEditMode. | |
|     this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. | |
|    * @private | |
|    */ | |
|   _clean() { | |
|     // not in mode | |
|     this.inMode = false; | |
| 
 | |
|     // _clean the divs | |
|     if (this.guiEnabled === true) { | |
|       util.recursiveDOMDelete(this.editModeDiv); | |
|       util.recursiveDOMDelete(this.manipulationDiv); | |
| 
 | |
|       // removes all the bindings and overloads | |
|       this._cleanManipulatorHammers(); | |
|     } | |
| 
 | |
|     // remove temporary nodes and edges | |
|     this._cleanupTemporaryNodesAndEdges(); | |
| 
 | |
|     // restore overloaded UI functions | |
|     this._unbindTemporaryUIs(); | |
| 
 | |
|     // remove the temporaryEventFunctions | |
|     this._unbindTemporaryEvents(); | |
| 
 | |
|     // restore the physics if required | |
|     this.body.emitter.emit('restorePhysics'); | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. | |
|    * @private | |
|    */ | |
|   _cleanManipulatorHammers() { | |
|     // _clean hammer bindings | |
|     if (this.manipulationHammers.length != 0) { | |
|       for (let i = 0; i < this.manipulationHammers.length; i++) { | |
|         this.manipulationHammers[i].destroy(); | |
|       } | |
|       this.manipulationHammers = []; | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Remove all DOM elements created by this module. | |
|    * @private | |
|    */ | |
|   _removeManipulationDOM() { | |
|     // removes all the bindings and overloads | |
|     this._clean(); | |
| 
 | |
|     // empty the manipulation divs | |
|     util.recursiveDOMDelete(this.manipulationDiv); | |
|     util.recursiveDOMDelete(this.editModeDiv); | |
|     util.recursiveDOMDelete(this.closeDiv); | |
| 
 | |
|     // remove the manipulation divs | |
|     if (this.manipulationDiv) {this.canvas.frame.removeChild(this.manipulationDiv);} | |
|     if (this.editModeDiv)     {this.canvas.frame.removeChild(this.editModeDiv);} | |
|     if (this.closeDiv)        {this.canvas.frame.removeChild(this.manipulationDiv);} | |
| 
 | |
|     // set the references to undefined | |
|     this.manipulationDiv = undefined; | |
|     this.editModeDiv = undefined; | |
|     this.closeDiv = undefined; | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * create a seperator line. the index is to differentiate in the manipulation dom | |
|    * @param index | |
|    * @private | |
|    */ | |
|   _createSeperator(index = 1) { | |
|     this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); | |
|     this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; | |
|     this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); | |
|   } | |
| 
 | |
|   // ----------------------    DOM functions for buttons    --------------------------// | |
|  | |
|   _createAddNodeButton(locale) { | |
|     let button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode']); | |
|     this.manipulationDiv.appendChild(button); | |
|     this._bindHammerToDiv(button, this.addNodeMode.bind(this)); | |
|   } | |
| 
 | |
|   _createAddEdgeButton(locale) { | |
|     let button = this._createButton('addEdge', 'vis-button vis-connect',  locale['addEdge'] || this.options.locales['en']['addEdge']); | |
|     this.manipulationDiv.appendChild(button); | |
|     this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); | |
|   } | |
| 
 | |
|   _createEditNodeButton(locale) { | |
|     let button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); | |
|     this.manipulationDiv.appendChild(button); | |
|     this._bindHammerToDiv(button, this.editNode.bind(this)); | |
|   } | |
| 
 | |
|   _createEditEdgeButton(locale) { | |
|     let button = this._createButton('editEdge', 'vis-button vis-edit',  locale['editEdge'] || this.options.locales['en']['editEdge']); | |
|     this.manipulationDiv.appendChild(button); | |
|     this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); | |
|   } | |
| 
 | |
|   _createDeleteButton(locale) { | |
|     let button = this._createButton('delete', 'vis-button vis-delete', locale['del'] || this.options.locales['en']['del']); | |
|     this.manipulationDiv.appendChild(button); | |
|     this._bindHammerToDiv(button, this.deleteSelected.bind(this)); | |
|   } | |
| 
 | |
|   _createBackButton(locale) { | |
|     let button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back']); | |
|     this.manipulationDiv.appendChild(button); | |
|     this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); | |
|   } | |
| 
 | |
|   _createButton(id, className, label, labelClassName = 'vis-label') { | |
| 
 | |
|     this.manipulationDOM[id+'Div'] = document.createElement('div'); | |
|     this.manipulationDOM[id+'Div'].className = className; | |
|     this.manipulationDOM[id+'Label'] = document.createElement('div'); | |
|     this.manipulationDOM[id+'Label'].className = labelClassName; | |
|     this.manipulationDOM[id+'Label'].innerHTML = label; | |
|     this.manipulationDOM[id+'Div'].appendChild(this.manipulationDOM[id+'Label']); | |
|     return this.manipulationDOM[id+'Div']; | |
|   } | |
| 
 | |
|   _createDescription(label) { | |
|     this.manipulationDiv.appendChild( | |
|       this._createButton('description', 'vis-button vis-none', label) | |
|     ); | |
|   } | |
| 
 | |
|   // -------------------------- End of DOM functions for buttons ------------------------------// | |
|  | |
|   /** | |
|    * this binds an event until cleanup by the clean functions. | |
|    * @param event | |
|    * @param newFunction | |
|    * @private | |
|    */ | |
|   _temporaryBindEvent(event, newFunction) { | |
|     this.temporaryEventFunctions.push({event:event, boundFunction:newFunction}); | |
|     this.body.emitter.on(event, newFunction); | |
|   } | |
| 
 | |
|   /** | |
|    * this overrides an UI function until cleanup by the clean function | |
|    * @param UIfunctionName | |
|    * @param newFunction | |
|    * @private | |
|    */ | |
|   _temporaryBindUI(UIfunctionName, newFunction) { | |
|     if (this.body.eventListeners[UIfunctionName] !== undefined) { | |
|       this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; | |
|       this.body.eventListeners[UIfunctionName] = newFunction; | |
|     } | |
|     else { | |
|       throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); | |
|     } | |
|   } | |
| 
 | |
|   /** | |
|    * Restore the overridden UI functions to their original state. | |
|    * | |
|    * @private | |
|    */ | |
|   _unbindTemporaryUIs() { | |
|     for (let functionName in this.temporaryUIFunctions) { | |
|       if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { | |
|         this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; | |
|         delete this.temporaryUIFunctions[functionName]; | |
|       } | |
|     } | |
|     this.temporaryUIFunctions = {}; | |
|   } | |
| 
 | |
|   /** | |
|    * Unbind the events created by _temporaryBindEvent | |
|    * @private | |
|    */ | |
|   _unbindTemporaryEvents() { | |
|     for (let i = 0; i < this.temporaryEventFunctions.length; i++) { | |
|       let eventName = this.temporaryEventFunctions[i].event; | |
|       let boundFunction = this.temporaryEventFunctions[i].boundFunction; | |
|       this.body.emitter.off(eventName, boundFunction); | |
|     } | |
|     this.temporaryEventFunctions = []; | |
|   } | |
| 
 | |
|   /** | |
|    * Bind an hammer instance to a DOM element. | |
|    * @param domElement | |
|    * @param funct | |
|    */ | |
|   _bindHammerToDiv(domElement, boundFunction) { | |
|     let hammer = new Hammer(domElement, {}); | |
|     hammerUtil.onTouch(hammer, boundFunction); | |
|     this.manipulationHammers.push(hammer); | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Neatly clean up temporary edges and nodes | |
|    * @private | |
|    */ | |
|   _cleanupTemporaryNodesAndEdges() { | |
|     // _clean temporary edges | |
|     for (let i = 0; i < this.temporaryIds.edges.length; i++) { | |
|       this.body.edges[this.temporaryIds.edges[i]].disconnect(); | |
|       delete this.body.edges[this.temporaryIds.edges[i]]; | |
|       let indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); | |
|       if (indexTempEdge !== -1) {this.body.edgeIndices.splice(indexTempEdge,1);} | |
|     } | |
| 
 | |
|     // _clean temporary nodes | |
|     for (let i = 0; i < this.temporaryIds.nodes.length; i++) { | |
|       delete this.body.nodes[this.temporaryIds.nodes[i]]; | |
|       let indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); | |
|       if (indexTempNode !== -1) {this.body.nodeIndices.splice(indexTempNode,1);} | |
|     } | |
| 
 | |
|     this.temporaryIds = {nodes: [], edges: []}; | |
|   } | |
| 
 | |
|   // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// | |
|  | |
|   /** | |
|    * the touch is used to get the position of the initial click | |
|    * @param event | |
|    * @private | |
|    */ | |
|   _controlNodeTouch(event) { | |
|     this.selectionHandler.unselectAll(); | |
|     this.lastTouch = this.body.functions.getPointer(event.center); | |
|     this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * the drag start is used to mark one of the control nodes as selected. | |
|    * @param event | |
|    * @private | |
|    */ | |
|   _controlNodeDragStart(event) { | |
|     let pointer = this.lastTouch; | |
|     let pointerObj = this.selectionHandler._pointerToPositionObject(pointer); | |
|     let from = this.body.nodes[this.temporaryIds.nodes[0]]; | |
|     let to   = this.body.nodes[this.temporaryIds.nodes[1]]; | |
|     let edge = this.body.edges[this.edgeBeingEditedId]; | |
|     this.selectedControlNode = undefined; | |
| 
 | |
|     let fromSelect = from.isOverlappingWith(pointerObj); | |
|     let toSelect = to.isOverlappingWith(pointerObj); | |
| 
 | |
|     if (fromSelect === true) { | |
|       this.selectedControlNode = from; | |
|       edge.edgeType.from = from; | |
|     } | |
|     else if (toSelect === true) { | |
|       this.selectedControlNode = to; | |
|       edge.edgeType.to = to; | |
|     } | |
| 
 | |
|     this.body.emitter.emit('_redraw'); | |
|   } | |
| 
 | |
|   /** | |
|    * dragging the control nodes or the canvas | |
|    * @param event | |
|    * @private | |
|    */ | |
|   _controlNodeDrag(event) { | |
|     this.body.emitter.emit('disablePhysics'); | |
|     let pointer = this.body.functions.getPointer(event.center); | |
|     let pos = this.canvas.DOMtoCanvas(pointer); | |
| 
 | |
|     if (this.selectedControlNode !== undefined) { | |
|       this.selectedControlNode.x = pos.x; | |
|       this.selectedControlNode.y = pos.y; | |
|     } | |
|     else { | |
|       // if the drag was not started properly because the click started outside the network div, start it now. | |
|       let diffX = pointer.x - this.lastTouch.x; | |
|       let diffY = pointer.y - this.lastTouch.y; | |
|       this.body.view.translation = {x:this.lastTouch.translation.x + diffX, y:this.lastTouch.translation.y + diffY}; | |
|     } | |
|     this.body.emitter.emit('_redraw'); | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * connecting or restoring the control nodes. | |
|    * @param event | |
|    * @private | |
|    */ | |
|   _controlNodeDragEnd(event) { | |
|     let pointer = this.body.functions.getPointer(event.center); | |
|     let pointerObj = this.selectionHandler._pointerToPositionObject(pointer); | |
|     let edge = this.body.edges[this.edgeBeingEditedId]; | |
| 
 | |
|     // if the node that was dragged is not a control node, return | |
|     if (this.selectedControlNode === undefined) { | |
|       return; | |
|     } | |
| 
 | |
|     let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); | |
|     let node = undefined; | |
|     for (let i = overlappingNodeIds.length-1; i >= 0; i--) { | |
|       if (overlappingNodeIds[i] !== this.selectedControlNode.id) { | |
|         node = this.body.nodes[overlappingNodeIds[i]]; | |
|         break; | |
|       } | |
|     } | |
| 
 | |
|     // perform the connection | |
|     if (node !== undefined && this.selectedControlNode !== undefined) { | |
|       if (node.isCluster === true) { | |
|         alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']) | |
|       } | |
|       else { | |
|         let from = this.body.nodes[this.temporaryIds.nodes[0]]; | |
|         if (this.selectedControlNode.id === from.id) { | |
|           this._performEditEdge(node.id, edge.to.id); | |
|         } | |
|         else { | |
|           this._performEditEdge(edge.from.id, node.id); | |
|         } | |
|       } | |
|     } | |
|     else { | |
|       edge.updateEdgeType(); | |
|       this.body.emitter.emit('restorePhysics'); | |
|     } | |
|     this.body.emitter.emit('_redraw'); | |
|   } | |
| 
 | |
|   // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// | |
|  | |
| 
 | |
| 
 | |
|   // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// | |
|   /** | |
|    * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |
|    * to walk the user through the process. | |
|    * | |
|    * @private | |
|    */ | |
|   _handleConnect(event) { | |
|     // check to avoid double fireing of this function. | |
|     if (new Date().valueOf() - this.touchTime > 100) { | |
|       this.lastTouch = this.body.functions.getPointer(event.center); | |
|       this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object | |
|  | |
|       let pointer = this.lastTouch; | |
|       let node = this.selectionHandler.getNodeAt(pointer); | |
| 
 | |
|       if (node !== undefined) { | |
|         if (node.isCluster === true) { | |
|           alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']) | |
|         } | |
|         else { | |
|           // create a node the temporary line can look at | |
|           let targetNode = this._getNewTargetNode(node.x,node.y); | |
|           this.body.nodes[targetNode.id] = targetNode; | |
|           this.body.nodeIndices.push(targetNode.id); | |
| 
 | |
|           // create a temporary edge | |
|           let connectionEdge = this.body.functions.createEdge({ | |
|             id: 'connectionEdge' + util.randomUUID(), | |
|             from: node.id, | |
|             to: targetNode.id, | |
|             physics: false, | |
|             smooth: { | |
|               enabled: true, | |
|               type: 'continuous', | |
|               roundness: 0.5 | |
|             } | |
|           }); | |
|           this.body.edges[connectionEdge.id] = connectionEdge; | |
|           this.body.edgeIndices.push(connectionEdge.id); | |
| 
 | |
|           this.temporaryIds.nodes.push(targetNode.id); | |
|           this.temporaryIds.edges.push(connectionEdge.id); | |
|         } | |
|       } | |
|       this.touchTime = new Date().valueOf(); | |
|     } | |
|   } | |
| 
 | |
|   _dragControlNode(event) { | |
|     let pointer = this.body.functions.getPointer(event.center); | |
|     if (this.temporaryIds.nodes[0] !== undefined) { | |
|       let targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode. | |
|       targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x); | |
|       targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y); | |
|       this.body.emitter.emit('_redraw'); | |
|     } | |
|     else { | |
|       let diffX = pointer.x - this.lastTouch.x; | |
|       let diffY = pointer.y - this.lastTouch.y; | |
|       this.body.view.translation = {x:this.lastTouch.translation.x + diffX, y:this.lastTouch.translation.y + diffY}; | |
|     } | |
| 
 | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * Connect the new edge to the target if one exists, otherwise remove temp line | |
|    * @param event | |
|    * @private | |
|    */ | |
|   _finishConnect(event) { | |
|     let pointer = this.body.functions.getPointer(event.center); | |
|     let pointerObj = this.selectionHandler._pointerToPositionObject(pointer); | |
| 
 | |
|     // remember the edge id | |
|     let connectFromId = undefined; | |
|     if (this.temporaryIds.edges[0] !== undefined) { | |
|       connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; | |
|     } | |
| 
 | |
|     // get the overlapping node but NOT the temporary node; | |
|     let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); | |
|     let node = undefined; | |
|     for (let i = overlappingNodeIds.length-1; i >= 0; i--) { | |
|       // if the node id is NOT a temporary node, accept the node. | |
|       if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) { | |
|         node = this.body.nodes[overlappingNodeIds[i]]; | |
|         break; | |
|       } | |
|     } | |
| 
 | |
|     // clean temporary nodes and edges. | |
|     this._cleanupTemporaryNodesAndEdges(); | |
| 
 | |
|     // perform the connection | |
|     if (node !== undefined) { | |
|       if (node.isCluster === true) { | |
|         alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); | |
|       } | |
|       else { | |
|         if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { | |
|           this._performAddEdge(connectFromId, node.id); | |
|         } | |
|       } | |
|     } | |
|     this.body.emitter.emit('_redraw'); | |
|   } | |
| 
 | |
|   // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// | |
|  | |
| 
 | |
|   // ------------------------------ Performing all the actual data manipulation ------------------------// | |
|  | |
|   /** | |
|    * Adds a node on the specified location | |
|    */ | |
|   _performAddNode(clickData) { | |
|     let defaultData = { | |
|       id: util.randomUUID(), | |
|       x: clickData.pointer.canvas.x, | |
|       y: clickData.pointer.canvas.y, | |
|       label: 'new' | |
|     }; | |
| 
 | |
|     if (typeof this.options.addNode === 'function') { | |
|       if (this.options.addNode.length === 2) { | |
|         this.options.addNode(defaultData, (finalizedData) => { | |
|           if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'addNode') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback | |
|             this.body.data.nodes.getDataSet().add(finalizedData); | |
|             this.showManipulatorToolbar(); | |
|           } | |
|         }); | |
|       } | |
|       else { | |
|         throw new Error('The function for add does not support two arguments (data,callback)'); | |
|         this.showManipulatorToolbar(); | |
|       } | |
|     } | |
|     else { | |
|       this.body.data.nodes.getDataSet().add(defaultData); | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
|   /** | |
|    * connect two nodes with a new edge. | |
|    * | |
|    * @private | |
|    */ | |
|   _performAddEdge(sourceNodeId, targetNodeId) { | |
|     let defaultData = {from: sourceNodeId, to: targetNodeId}; | |
|     if (typeof this.options.addEdge === 'function') { | |
|       if (this.options.addEdge.length === 2) { | |
|         this.options.addEdge(defaultData, (finalizedData) => { | |
|           if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'addEdge') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback | |
|             this.body.data.edges.getDataSet().add(finalizedData); | |
|             this.selectionHandler.unselectAll(); | |
|             this.showManipulatorToolbar(); | |
|           } | |
|         }); | |
|       } | |
|       else { | |
|         throw new Error('The function for connect does not support two arguments (data,callback)'); | |
|       } | |
|     } | |
|     else { | |
|       this.body.data.edges.getDataSet().add(defaultData); | |
|       this.selectionHandler.unselectAll(); | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
|   /** | |
|    * connect two nodes with a new edge. | |
|    * | |
|    * @private | |
|    */ | |
|   _performEditEdge(sourceNodeId, targetNodeId) { | |
|     let defaultData = {id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId}; | |
|     if (typeof this.options.editEdge === 'function') { | |
|       if (this.options.editEdge.length === 2) { | |
|         this.options.editEdge(defaultData, (finalizedData) => { | |
|           if (finalizedData === null || finalizedData === undefined || this.inMode !== 'editEdge') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { | |
|             this.body.edges[defaultData.id].updateEdgeType(); | |
|             this.body.emitter.emit('_redraw'); | |
|           } | |
|           else { | |
|             this.body.data.edges.getDataSet().update(finalizedData); | |
|             this.selectionHandler.unselectAll(); | |
|             this.showManipulatorToolbar(); | |
|           } | |
|         }); | |
|       } | |
|       else { | |
|         throw new Error('The function for edit does not support two arguments (data, callback)'); | |
|       } | |
|     } | |
|     else { | |
|       this.body.data.edges.getDataSet().update(defaultData); | |
|       this.selectionHandler.unselectAll(); | |
|       this.showManipulatorToolbar(); | |
|     } | |
|   } | |
| 
 | |
| 
 | |
| } | |
| 
 | |
| export default ManipulationSystem; | |
|   
 |