visjs-network
Version:
A dynamic, browser-based network visualization library.
1,390 lines (1,253 loc) • 42.5 kB
JavaScript
let util = require('../../util')
let Hammer = require('../../module/hammer')
let hammerUtil = require('../../hammerUtil')
/**
* Clears the toolbar div element of children
*
* @private
*/
class ManipulationSystem {
/**
* @param {Object} body
* @param {Canvas} canvas
* @param {SelectionHandler} selectionHandler
*/
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 {Object} options
* @param {Object} allOptions
* @param {Object} globalOptions
*/
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()
}
}
/**
* Enables Edit Mode
*/
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()
}
}
/**
* Disables Edit Mode
*/
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, false)
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._dragStartEdge.bind(this))
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 (
typeof this.options.editEdge === 'object' &&
typeof this.options.editEdge.editWithoutDrag === 'function'
) {
this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]
if (this.edgeBeingEditedId !== undefined) {
var edge = this.body.edges[this.edgeBeingEditedId]
this._performEditEdge(edge.from, edge.to)
return
}
}
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 {number} x
* @param {number} y
* @returns {Node}
* @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.closeDiv)
}
// 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 {number} [index=1]
* @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 --------------------------//
/**
*
* @param {Locale} locale
* @private
*/
_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))
}
/**
*
* @param {Locale} locale
* @private
*/
_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))
}
/**
*
* @param {Locale} locale
* @private
*/
_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))
}
/**
*
* @param {Locale} locale
* @private
*/
_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))
}
/**
*
* @param {Locale} locale
* @private
*/
_createDeleteButton(locale) {
var deleteBtnClass
if (this.options.rtl) {
deleteBtnClass = 'vis-button vis-delete-rtl'
} else {
deleteBtnClass = 'vis-button vis-delete'
}
let button = this._createButton(
'delete',
deleteBtnClass,
locale['del'] || this.options.locales['en']['del']
)
this.manipulationDiv.appendChild(button)
this._bindHammerToDiv(button, this.deleteSelected.bind(this))
}
/**
*
* @param {Locale} locale
* @private
*/
_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))
}
/**
*
* @param {number|string} id
* @param {string} className
* @param {label} label
* @param {string} labelClassName
* @returns {HTMLElement}
* @private
*/
_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']
}
/**
*
* @param {Label} label
* @private
*/
_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} event The event
* @param {function} 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 {string} UIfunctionName
* @param {function} 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 {Element} domElement
* @param {function} boundFunction
*/
_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} event The 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} event The event
* @private
*/
_controlNodeDragStart(/* event */) {
// eslint-disable-line no-unused-vars
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
}
// we use the selection to find the node that is being dragged. We explicitly select it here.
if (this.selectedControlNode !== undefined) {
this.selectionHandler.selectObject(this.selectedControlNode)
}
this.body.emitter.emit('_redraw')
}
/**
* dragging the control nodes or the canvas
* @param {Event} event The 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} event The 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
}
// we use the selection to find the node that is being dragged. We explicitly DEselect the control node here.
this.selectionHandler.unselectAll()
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.
*
* @param {Event} event
* @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()
}
}
/**
*
* @param {Event} event
* @private
*/
_dragControlNode(event) {
let pointer = this.body.functions.getPointer(event.center)
var pointerObj = this.selectionHandler._pointerToPositionObject(pointer)
// remember the edge id
var 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;
var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(
pointerObj
)
var node = undefined
for (var 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
}
}
event.controlEdge = { from: connectFromId, to: node ? node.id : undefined }
this.selectionHandler._generateClickEvent(
'controlNodeDragging',
event,
pointer
)
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} event The 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)
}
}
}
event.controlEdge = { from: connectFromId, to: node ? node.id : undefined }
this.selectionHandler._generateClickEvent(
'controlNodeDragEnd',
event,
pointer
)
// No need to do _generateclickevent('dragEnd') here, the regular dragEnd event fires.
this.body.emitter.emit('_redraw')
}
/**
*
* @param {Event} event
* @private
*/
_dragStartEdge(event) {
let pointer = this.lastTouch
this.selectionHandler._generateClickEvent(
'dragStart',
event,
pointer,
undefined,
true
)
}
// --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------//
// ------------------------------ Performing all the actual data manipulation ------------------------//
/**
* Adds a node on the specified location
*
* @param {Object} clickData
* @private
*/
_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 {
this.showManipulatorToolbar()
throw new Error(
'The function for add does not support two arguments (data,callback)'
)
}
} else {
this.body.data.nodes.getDataSet().add(defaultData)
this.showManipulatorToolbar()
}
}
/**
* connect two nodes with a new edge.
*
* @param {Node.id} sourceNodeId
* @param {Node.id} targetNodeId
* @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.
*
* @param {Node.id} sourceNodeId
* @param {Node.id} targetNodeId
* @private
*/
_performEditEdge(sourceNodeId, targetNodeId) {
let defaultData = {
id: this.edgeBeingEditedId,
from: sourceNodeId,
to: targetNodeId,
label: this.body.data.edges._data[this.edgeBeingEditedId].label
}
let eeFunct = this.options.editEdge
if (typeof eeFunct === 'object') {
eeFunct = eeFunct.editWithoutDrag
}
if (typeof eeFunct === 'function') {
if (eeFunct.length === 2) {
eeFunct(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')
this.showManipulatorToolbar()
} 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