/** * This plugin provides a method to drag & drop nodes. Check the * sigma.plugins.dragNodes function doc or the examples/basic.html & * examples/api-candy.html code samples to know more. */ (function() { 'use strict'; if (typeof sigma === 'undefined') throw 'sigma is not declared'; sigma.utils.pkg('sigma.plugins'); /** * This function will add `mousedown`, `mouseup` & `mousemove` events to the * nodes in the `overNode`event to perform drag & drop operations. It uses * `linear interpolation` [http://en.wikipedia.org/wiki/Linear_interpolation] * and `rotation matrix` [http://en.wikipedia.org/wiki/Rotation_matrix] to * calculate the X and Y coordinates from the `cam` or `renderer` node * attributes. These attributes represent the coordinates of the nodes in * the real container, not in canvas. * * Fired events: * ************* * startdrag Fired at the beginning of the drag. * drag Fired while the node is dragged. * drop Fired at the end of the drag if the node has been dragged. * dragend Fired at the end of the drag. * * Recognized parameters: * ********************** * @param {sigma} s The related sigma instance. * @param {renderer} renderer The related renderer instance. */ function DragNodes(s, renderer) { sigma.classes.dispatcher.extend(this); // A quick hardcoded rule to prevent people from using this plugin with the // WebGL renderer (which is impossible at the moment): // if ( // sigma.renderers.webgl && // renderer instanceof sigma.renderers.webgl // ) // throw new Error( // 'The sigma.plugins.dragNodes is not compatible with the WebGL renderer' // ); // Init variables: var _self = this, _s = s, _body = document.body, _renderer = renderer, _mouse = renderer.container.lastChild, _camera = renderer.camera, _node = null, _prefix = '', _hoverStack = [], _hoverIndex = {}, _isMouseDown = false, _isMouseOverCanvas = false, _drag = false; if (renderer instanceof sigma.renderers.svg) { _mouse = renderer.container.firstChild; } // It removes the initial substring ('read_') if it's a WegGL renderer. if (renderer instanceof sigma.renderers.webgl) { _prefix = renderer.options.prefix.substr(5); } else { _prefix = renderer.options.prefix; } renderer.bind('overNode', nodeMouseOver); renderer.bind('outNode', treatOutNode); renderer.bind('click', click); _s.bind('kill', function() { _self.unbindAll(); }); /** * Unbind all event listeners. */ this.unbindAll = function() { _mouse.removeEventListener('mousedown', nodeMouseDown); _body.removeEventListener('mousemove', nodeMouseMove); _body.removeEventListener('mouseup', nodeMouseUp); _renderer.unbind('overNode', nodeMouseOver); _renderer.unbind('outNode', treatOutNode); } // Calculates the global offset of the given element more accurately than // element.offsetTop and element.offsetLeft. function calculateOffset(element) { var style = window.getComputedStyle(element); var getCssProperty = function(prop) { return parseInt(style.getPropertyValue(prop).replace('px', '')) || 0; }; return { left: element.getBoundingClientRect().left + getCssProperty('padding-left'), top: element.getBoundingClientRect().top + getCssProperty('padding-top') }; }; function click(event) { // event triggered at the end of the click _isMouseDown = false; _body.removeEventListener('mousemove', nodeMouseMove); _body.removeEventListener('mouseup', nodeMouseUp); if (!_hoverStack.length) { _node = null; } }; function nodeMouseOver(event) { // Don't treat the node if it is already registered if (_hoverIndex[event.data.node.id]) { return; } // Add node to array of current nodes over _hoverStack.push(event.data.node); _hoverIndex[event.data.node.id] = true; if(_hoverStack.length && ! _isMouseDown) { // Set the current node to be the last one in the array _node = _hoverStack[_hoverStack.length - 1]; _mouse.addEventListener('mousedown', nodeMouseDown); } }; function treatOutNode(event) { // Remove the node from the array var indexCheck = _hoverStack.map(function(e) { return e; }).indexOf(event.data.node); _hoverStack.splice(indexCheck, 1); delete _hoverIndex[event.data.node.id]; if(_hoverStack.length && ! _isMouseDown) { // On out, set the current node to be the next stated in array _node = _hoverStack[_hoverStack.length - 1]; } else { _mouse.removeEventListener('mousedown', nodeMouseDown); } }; function nodeMouseDown(event) { _isMouseDown = true; var size = _s.graph.nodes().length; // when there is only node in the graph, the plugin cannot apply // linear interpolation. So treat it as if a user is dragging // the graph if (_node && size > 1) { _mouse.removeEventListener('mousedown', nodeMouseDown); _body.addEventListener('mousemove', nodeMouseMove); _body.addEventListener('mouseup', nodeMouseUp); // Do not refresh edgequadtree during drag: var k, c; for (k in _s.cameras) { c = _s.cameras[k]; if (c.edgequadtree !== undefined) { c.edgequadtree._enabled = false; } } // Deactivate drag graph. _renderer.settings({mouseEnabled: false, enableHovering: false}); _s.refresh(); _self.dispatchEvent('startdrag', { node: _node, captor: event, renderer: _renderer }); } }; function nodeMouseUp(event) { _isMouseDown = false; _mouse.addEventListener('mousedown', nodeMouseDown); _body.removeEventListener('mousemove', nodeMouseMove); _body.removeEventListener('mouseup', nodeMouseUp); // Allow to refresh edgequadtree: var k, c; for (k in _s.cameras) { c = _s.cameras[k]; if (c.edgequadtree !== undefined) { c.edgequadtree._enabled = true; } } // Activate drag graph. _renderer.settings({mouseEnabled: true, enableHovering: true}); _s.refresh(); if (_drag) { _self.dispatchEvent('drop', { node: _node, captor: event, renderer: _renderer }); } _self.dispatchEvent('dragend', { node: _node, captor: event, renderer: _renderer }); _drag = false; _node = null; }; function nodeMouseMove(event) { if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { clearTimeout(timeOut); var timeOut = setTimeout(executeNodeMouseMove, 0); } else { executeNodeMouseMove(); } function executeNodeMouseMove() { var offset = calculateOffset(_renderer.container), x = event.clientX - offset.left, y = event.clientY - offset.top, cos = Math.cos(_camera.angle), sin = Math.sin(_camera.angle), nodes = _s.graph.nodes(), ref = []; // Getting and derotating the reference coordinates. for (var i = 0; i < 2; i++) { var n = nodes[i]; var aux = { x: n.x * cos + n.y * sin, y: n.y * cos - n.x * sin, renX: n[_prefix + 'x'], renY: n[_prefix + 'y'], }; ref.push(aux); } // Applying linear interpolation. // if the nodes are on top of each other, we use the camera ratio to interpolate if (ref[0].x === ref[1].x && ref[0].y === ref[1].y) { var xRatio = (ref[0].renX === 0) ? 1 : ref[0].renX; var yRatio = (ref[0].renY === 0) ? 1 : ref[0].renY; x = (ref[0].x / xRatio) * (x - ref[0].renX) + ref[0].x; y = (ref[0].y / yRatio) * (y - ref[0].renY) + ref[0].y; } else { var xRatio = (ref[1].renX - ref[0].renX) / (ref[1].x - ref[0].x); var yRatio = (ref[1].renY - ref[0].renY) / (ref[1].y - ref[0].y); // if the coordinates are the same, we use the other ratio to interpolate if (ref[1].x === ref[0].x) { xRatio = yRatio; } if (ref[1].y === ref[0].y) { yRatio = xRatio; } x = (x - ref[0].renX) / xRatio + ref[0].x; y = (y - ref[0].renY) / yRatio + ref[0].y; } // Rotating the coordinates. _node.x = x * cos - y * sin; _node.y = y * cos + x * sin; _s.refresh(); _drag = true; _self.dispatchEvent('drag', { node: _node, captor: event, renderer: _renderer }); } }; }; /** * Interface * ------------------ * * > var dragNodesListener = sigma.plugins.dragNodes(s, s.renderers[0]); */ var _instance = {}; /** * @param {sigma} s The related sigma instance. * @param {renderer} renderer The related renderer instance. */ sigma.plugins.dragNodes = function(s, renderer) { // Create object if undefined if (!_instance[s.id]) { _instance[s.id] = new DragNodes(s, renderer); } s.bind('kill', function() { sigma.plugins.killDragNodes(s); }); return _instance[s.id]; }; /** * This method removes the event listeners and kills the dragNodes instance. * * @param {sigma} s The related sigma instance. */ sigma.plugins.killDragNodes = function(s) { if (_instance[s.id] instanceof DragNodes) { _instance[s.id].unbindAll(); delete _instance[s.id]; } }; }).call(window);