UNPKG

visjs-network

Version:

A dynamic, browser-based network visualization library.

865 lines (791 loc) 23.8 kB
var Node = require('./components/Node').default var Edge = require('./components/Edge').default let util = require('../../util') /** * The handler for selections */ class SelectionHandler { /** * @param {Object} body * @param {Canvas} canvas */ constructor(body, canvas) { this.body = body this.canvas = canvas this.selectionObj = { nodes: [], edges: [] } this.hoverObj = { nodes: {}, edges: {} } this.options = {} this.defaultOptions = { multiselect: false, selectable: true, selectConnectedEdges: true, hoverConnectedEdges: true } util.extend(this.options, this.defaultOptions) this.body.emitter.on('_dataChanged', () => { this.updateSelection() }) } /** * * @param {Object} [options] */ setOptions(options) { if (options !== undefined) { let fields = [ 'multiselect', 'hoverConnectedEdges', 'selectable', 'selectConnectedEdges' ] util.selectiveDeepExtend(fields, this.options, options) } } /** * handles the selection part of the tap; * * @param {{x: number, y: number}} pointer * @returns {boolean} */ selectOnPoint(pointer) { let selected = false if (this.options.selectable === true) { let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer) // unselect after getting the objects in order to restore width and height. this.unselectAll() if (obj !== undefined) { selected = this.selectObject(obj) } this.body.emitter.emit('_requestRedraw') } return selected } /** * * @param {{x: number, y: number}} pointer * @returns {boolean} */ selectAdditionalOnPoint(pointer) { let selectionChanged = false if (this.options.selectable === true) { let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer) if (obj !== undefined) { selectionChanged = true if (obj.isSelected() === true) { this.deselectObject(obj) } else { this.selectObject(obj) } this.body.emitter.emit('_requestRedraw') } } return selectionChanged } /** * Create an object containing the standard fields for an event. * * @param {Event} event * @param {{x: number, y: number}} pointer Object with the x and y screen coordinates of the mouse * @returns {{}} * @private */ _initBaseEvent(event, pointer) { let properties = {} properties['pointer'] = { DOM: { x: pointer.x, y: pointer.y }, canvas: this.canvas.DOMtoCanvas(pointer) } properties['event'] = event return properties } /** * Generate an event which the user can catch. * * This adds some extra data to the event with respect to cursor position and * selected nodes and edges. * * @param {string} eventType Name of event to send * @param {Event} event * @param {{x: number, y: number}} pointer Object with the x and y screen coordinates of the mouse * @param {Object|undefined} oldSelection If present, selection state before event occured * @param {boolean|undefined} [emptySelection=false] Indicate if selection data should be passed */ _generateClickEvent( eventType, event, pointer, oldSelection, emptySelection = false ) { let properties = this._initBaseEvent(event, pointer) if (emptySelection === true) { properties.nodes = [] properties.edges = [] } else { let tmp = this.getSelection() properties.nodes = tmp.nodes properties.edges = tmp.edges } if (oldSelection !== undefined) { properties['previousSelection'] = oldSelection } if (eventType == 'click') { // For the time being, restrict this functionality to // just the click event. properties.items = this.getClickedItems(pointer) } if (event.controlEdge !== undefined) { properties.controlEdge = event.controlEdge } this.body.emitter.emit(eventType, properties) } /** * * @param {Object} obj * @param {boolean} [highlightEdges=this.options.selectConnectedEdges] * @returns {boolean} */ selectObject(obj, highlightEdges = this.options.selectConnectedEdges) { if (obj !== undefined) { if (obj instanceof Node) { if (highlightEdges === true) { this._selectConnectedEdges(obj) } } obj.select() this._addToSelection(obj) return true } return false } /** * * @param {Object} obj */ deselectObject(obj) { if (obj.isSelected() === true) { obj.selected = false this._removeFromSelection(obj) } } /** * retrieve all nodes overlapping with given object * @param {Object} object An object with parameters left, top, right, bottom * @return {number[]} An array with id's of the overlapping nodes * @private */ _getAllNodesOverlappingWith(object) { let overlappingNodes = [] let nodes = this.body.nodes for (let i = 0; i < this.body.nodeIndices.length; i++) { let nodeId = this.body.nodeIndices[i] if (nodes[nodeId].isOverlappingWith(object)) { overlappingNodes.push(nodeId) } } return overlappingNodes } /** * Return a position object in canvasspace from a single point in screenspace * * @param {{x: number, y: number}} pointer * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ _pointerToPositionObject(pointer) { let canvasPos = this.canvas.DOMtoCanvas(pointer) return { left: canvasPos.x - 1, top: canvasPos.y + 1, right: canvasPos.x + 1, bottom: canvasPos.y - 1 } } /** * Get the top node at the passed point (like a click) * * @param {{x: number, y: number}} pointer * @param {boolean} [returnNode=true] * @return {Node | undefined} node */ getNodeAt(pointer, returnNode = true) { // we first check if this is an navigation controls element let positionObject = this._pointerToPositionObject(pointer) let overlappingNodes = this._getAllNodesOverlappingWith(positionObject) // if there are overlapping nodes, select the last one, this is the // one which is drawn on top of the others if (overlappingNodes.length > 0) { if (returnNode === true) { return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]] } else { return overlappingNodes[overlappingNodes.length - 1] } } else { return undefined } } /** * retrieve all edges overlapping with given object, selector is around center * @param {Object} object An object with parameters left, top, right, bottom * @param {number[]} overlappingEdges An array with id's of the overlapping nodes * @private */ _getEdgesOverlappingWith(object, overlappingEdges) { let edges = this.body.edges for (let i = 0; i < this.body.edgeIndices.length; i++) { let edgeId = this.body.edgeIndices[i] if (edges[edgeId].isOverlappingWith(object)) { overlappingEdges.push(edgeId) } } } /** * retrieve all nodes overlapping with given object * @param {Object} object An object with parameters left, top, right, bottom * @return {number[]} An array with id's of the overlapping nodes * @private */ _getAllEdgesOverlappingWith(object) { let overlappingEdges = [] this._getEdgesOverlappingWith(object, overlappingEdges) return overlappingEdges } /** * Get the edges nearest to the passed point (like a click) * * @param {{x: number, y: number}} pointer * @param {boolean} [returnEdge=true] * @return {Edge | undefined} node */ getEdgeAt(pointer, returnEdge = true) { // Iterate over edges, pick closest within 10 var canvasPos = this.canvas.DOMtoCanvas(pointer) var mindist = 10 var overlappingEdge = null var edges = this.body.edges for (var i = 0; i < this.body.edgeIndices.length; i++) { var edgeId = this.body.edgeIndices[i] var edge = edges[edgeId] if (edge.connected) { var xFrom = edge.from.x var yFrom = edge.from.y var xTo = edge.to.x var yTo = edge.to.y var dist = edge.edgeType.getDistanceToEdge( xFrom, yFrom, xTo, yTo, canvasPos.x, canvasPos.y ) if (dist < mindist) { overlappingEdge = edgeId mindist = dist } } } if (overlappingEdge !== null) { if (returnEdge === true) { return this.body.edges[overlappingEdge] } else { return overlappingEdge } } else { return undefined } } /** * Add object to the selection array. * * @param {Object} obj * @private */ _addToSelection(obj) { if (obj instanceof Node) { this.selectionObj.nodes[obj.id] = obj } else { this.selectionObj.edges[obj.id] = obj } } /** * Add object to the selection array. * * @param {Object} obj * @private */ _addToHover(obj) { if (obj instanceof Node) { this.hoverObj.nodes[obj.id] = obj } else { this.hoverObj.edges[obj.id] = obj } } /** * Remove a single option from selection. * * @param {Object} obj * @private */ _removeFromSelection(obj) { if (obj instanceof Node) { delete this.selectionObj.nodes[obj.id] this._unselectConnectedEdges(obj) } else { delete this.selectionObj.edges[obj.id] } } /** * Unselect all. The selectionObj is useful for this. */ unselectAll() { for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { this.selectionObj.nodes[nodeId].unselect() } } for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { this.selectionObj.edges[edgeId].unselect() } } this.selectionObj = { nodes: {}, edges: {} } } /** * return the number of selected nodes * * @returns {number} * @private */ _getSelectedNodeCount() { let count = 0 for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { count += 1 } } return count } /** * return the selected node * * @returns {number} * @private */ _getSelectedNode() { for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { return this.selectionObj.nodes[nodeId] } } return undefined } /** * return the selected edge * * @returns {number} * @private */ _getSelectedEdge() { for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { return this.selectionObj.edges[edgeId] } } return undefined } /** * return the number of selected edges * * @returns {number} * @private */ _getSelectedEdgeCount() { let count = 0 for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { count += 1 } } return count } /** * return the number of selected objects. * * @returns {number} * @private */ _getSelectedObjectCount() { let count = 0 for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { count += 1 } } for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { count += 1 } } return count } /** * Check if anything is selected * * @returns {boolean} * @private */ _selectionIsEmpty() { for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { return false } } for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { return false } } return true } /** * check if one of the selected nodes is a cluster. * * @returns {boolean} * @private */ _clusterInSelection() { for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { if (this.selectionObj.nodes[nodeId].clusterSize > 1) { return true } } } return false } /** * select the edges connected to the node that is being selected * * @param {Node} node * @private */ _selectConnectedEdges(node) { for (let i = 0; i < node.edges.length; i++) { let edge = node.edges[i] edge.select() this._addToSelection(edge) } } /** * select the edges connected to the node that is being selected * * @param {Node} node * @private */ _hoverConnectedEdges(node) { for (let i = 0; i < node.edges.length; i++) { let edge = node.edges[i] edge.hover = true this._addToHover(edge) } } /** * unselect the edges connected to the node that is being selected * * @param {Node} node * @private */ _unselectConnectedEdges(node) { for (let i = 0; i < node.edges.length; i++) { let edge = node.edges[i] edge.unselect() this._removeFromSelection(edge) } } /** * Remove the highlight from a node or edge, in response to mouse movement * * @param {Event} event * @param {{x: number, y: number}} pointer object with the x and y screen coordinates of the mouse * @param {Node|vis.Edge} object * @private */ emitBlurEvent(event, pointer, object) { let properties = this._initBaseEvent(event, pointer) if (object.hover === true) { object.hover = false if (object instanceof Node) { properties.node = object.id this.body.emitter.emit('blurNode', properties) } else { properties.edge = object.id this.body.emitter.emit('blurEdge', properties) } } } /** * Create the highlight for a node or edge, in response to mouse movement * * @param {Event} event * @param {{x: number, y: number}} pointer object with the x and y screen coordinates of the mouse * @param {Node|vis.Edge} object * @returns {boolean} hoverChanged * @private */ emitHoverEvent(event, pointer, object) { let properties = this._initBaseEvent(event, pointer) let hoverChanged = false if (object.hover === false) { object.hover = true this._addToHover(object) hoverChanged = true if (object instanceof Node) { properties.node = object.id this.body.emitter.emit('hoverNode', properties) } else { properties.edge = object.id this.body.emitter.emit('hoverEdge', properties) } } return hoverChanged } /** * Perform actions in response to a mouse movement. * * @param {Event} event * @param {{x: number, y: number}} pointer | object with the x and y screen coordinates of the mouse */ hoverObject(event, pointer) { let object = this.getNodeAt(pointer) if (object === undefined) { object = this.getEdgeAt(pointer) } let hoverChanged = false // remove all node hover highlights for (let nodeId in this.hoverObj.nodes) { if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { if ( object === undefined || (object instanceof Node && object.id != nodeId) || object instanceof Edge ) { this.emitBlurEvent(event, pointer, this.hoverObj.nodes[nodeId]) delete this.hoverObj.nodes[nodeId] hoverChanged = true } } } // removing all edge hover highlights for (let edgeId in this.hoverObj.edges) { if (this.hoverObj.edges.hasOwnProperty(edgeId)) { // if the hover has been changed here it means that the node has been hovered over or off // we then do not use the emitBlurEvent method here. if (hoverChanged === true) { this.hoverObj.edges[edgeId].hover = false delete this.hoverObj.edges[edgeId] } // if the blur remains the same and the object is undefined (mouse off) or another // edge has been hovered, or another node has been hovered we blur the edge. else if ( object === undefined || (object instanceof Edge && object.id != edgeId) || (object instanceof Node && !object.hover) ) { this.emitBlurEvent(event, pointer, this.hoverObj.edges[edgeId]) delete this.hoverObj.edges[edgeId] hoverChanged = true } } } if (object !== undefined) { const hoveredEdgesCount = Object.keys(this.hoverObj.edges).length const hoveredNodesCount = Object.keys(this.hoverObj.nodes).length const newOnlyHoveredEdge = object instanceof Edge && hoveredEdgesCount === 0 && hoveredNodesCount === 0 const newOnlyHoveredNode = object instanceof Node && hoveredEdgesCount === 0 && hoveredNodesCount === 0 if (hoverChanged || newOnlyHoveredEdge || newOnlyHoveredNode) { hoverChanged = this.emitHoverEvent(event, pointer, object) } if (object instanceof Node && this.options.hoverConnectedEdges === true) { this._hoverConnectedEdges(object) } } if (hoverChanged === true) { this.body.emitter.emit('_requestRedraw') } } /** * * retrieve the currently selected objects * @return {{nodes: Array.<string>, edges: Array.<string>}} selection */ getSelection() { let nodeIds = this.getSelectedNodes() let edgeIds = this.getSelectedEdges() return { nodes: nodeIds, edges: edgeIds } } /** * * retrieve the currently selected nodes * @return {string[]} selection An array with the ids of the * selected nodes. */ getSelectedNodes() { let idArray = [] if (this.options.selectable === true) { for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { idArray.push(this.selectionObj.nodes[nodeId].id) } } } return idArray } /** * * retrieve the currently selected edges * @return {Array} selection An array with the ids of the * selected nodes. */ getSelectedEdges() { let idArray = [] if (this.options.selectable === true) { for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { idArray.push(this.selectionObj.edges[edgeId].id) } } } return idArray } /** * Updates the current selection * @param {{nodes: Array.<string>, edges: Array.<string>}} selection * @param {Object} options Options */ setSelection(selection, options = {}) { let i, id if (!selection || (!selection.nodes && !selection.edges)) throw 'Selection must be an object with nodes and/or edges properties' // first unselect any selected node, if option is true or undefined if (options.unselectAll || options.unselectAll === undefined) { this.unselectAll() } if (selection.nodes) { for (i = 0; i < selection.nodes.length; i++) { id = selection.nodes[i] let node = this.body.nodes[id] if (!node) { throw new RangeError('Node with id "' + id + '" not found') } // don't select edges with it this.selectObject(node, options.highlightEdges) } } if (selection.edges) { for (i = 0; i < selection.edges.length; i++) { id = selection.edges[i] let edge = this.body.edges[id] if (!edge) { throw new RangeError('Edge with id "' + id + '" not found') } this.selectObject(edge) } } this.body.emitter.emit('_requestRedraw') } /** * select zero or more nodes with the option to highlight edges * @param {number[] | string[]} selection An array with the ids of the * selected nodes. * @param {boolean} [highlightEdges] */ selectNodes(selection, highlightEdges = true) { if (!selection || selection.length === undefined) throw 'Selection must be an array with ids' this.setSelection({ nodes: selection }, { highlightEdges: highlightEdges }) } /** * select zero or more edges * @param {number[] | string[]} selection An array with the ids of the * selected nodes. */ selectEdges(selection) { if (!selection || selection.length === undefined) throw 'Selection must be an array with ids' this.setSelection({ edges: selection }) } /** * Validate the selection: remove ids of nodes which no longer exist * @private */ updateSelection() { for (let nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { if (!this.body.nodes.hasOwnProperty(nodeId)) { delete this.selectionObj.nodes[nodeId] } } } for (let edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { if (!this.body.edges.hasOwnProperty(edgeId)) { delete this.selectionObj.edges[edgeId] } } } } /** * Determine all the visual elements clicked which are on the given point. * * All elements are returned; this includes nodes, edges and their labels. * The order returned is from highest to lowest, i.e. element 0 of the return * value is the topmost item clicked on. * * The return value consists of an array of the following possible elements: * * - `{nodeId:number}` - node with given id clicked on * - `{nodeId:number, labelId:0}` - label of node with given id clicked on * - `{edgeId:number}` - edge with given id clicked on * - `{edge:number, labelId:0}` - label of edge with given id clicked on * * ## NOTES * * - Currently, there is only one label associated with a node or an edge, * but this is expected to change somewhere in the future. * - Since there is no z-indexing yet, it is not really possible to set the nodes and * edges in the correct order. For the time being, nodes come first. * * @param {point} pointer mouse position in screen coordinates * @returns {Array.<nodeClickItem|nodeLabelClickItem|edgeClickItem|edgeLabelClickItem>} * @private */ getClickedItems(pointer) { let point = this.canvas.DOMtoCanvas(pointer) var items = [] // Note reverse order; we want the topmost clicked items to be first in the array // Also note that selected nodes are disregarded here; these normally display on top let nodeIndices = this.body.nodeIndices let nodes = this.body.nodes for (let i = nodeIndices.length - 1; i >= 0; i--) { let node = nodes[nodeIndices[i]] let ret = node.getItemsOnPoint(point) items.push.apply(items, ret) // Append the return value to the running list. } let edgeIndices = this.body.edgeIndices let edges = this.body.edges for (let i = edgeIndices.length - 1; i >= 0; i--) { let edge = edges[edgeIndices[i]] let ret = edge.getItemsOnPoint(point) items.push.apply(items, ret) // Append the return value to the running list. } return items } } export default SelectionHandler