sigma.plugins.dragNodes.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /**
  2. * This plugin provides a method to drag & drop nodes. Check the
  3. * sigma.plugins.dragNodes function doc or the examples/basic.html &
  4. * examples/api-candy.html code samples to know more.
  5. */
  6. (function() {
  7. 'use strict';
  8. if (typeof sigma === 'undefined')
  9. throw 'sigma is not declared';
  10. sigma.utils.pkg('sigma.plugins');
  11. /**
  12. * This function will add `mousedown`, `mouseup` & `mousemove` events to the
  13. * nodes in the `overNode`event to perform drag & drop operations. It uses
  14. * `linear interpolation` [http://en.wikipedia.org/wiki/Linear_interpolation]
  15. * and `rotation matrix` [http://en.wikipedia.org/wiki/Rotation_matrix] to
  16. * calculate the X and Y coordinates from the `cam` or `renderer` node
  17. * attributes. These attributes represent the coordinates of the nodes in
  18. * the real container, not in canvas.
  19. *
  20. * Fired events:
  21. * *************
  22. * startdrag Fired at the beginning of the drag.
  23. * drag Fired while the node is dragged.
  24. * drop Fired at the end of the drag if the node has been dragged.
  25. * dragend Fired at the end of the drag.
  26. *
  27. * Recognized parameters:
  28. * **********************
  29. * @param {sigma} s The related sigma instance.
  30. * @param {renderer} renderer The related renderer instance.
  31. */
  32. function DragNodes(s, renderer) {
  33. sigma.classes.dispatcher.extend(this);
  34. // A quick hardcoded rule to prevent people from using this plugin with the
  35. // WebGL renderer (which is impossible at the moment):
  36. // if (
  37. // sigma.renderers.webgl &&
  38. // renderer instanceof sigma.renderers.webgl
  39. // )
  40. // throw new Error(
  41. // 'The sigma.plugins.dragNodes is not compatible with the WebGL renderer'
  42. // );
  43. // Init variables:
  44. var _self = this,
  45. _s = s,
  46. _body = document.body,
  47. _renderer = renderer,
  48. _mouse = renderer.container.lastChild,
  49. _camera = renderer.camera,
  50. _node = null,
  51. _prefix = '',
  52. _hoverStack = [],
  53. _hoverIndex = {},
  54. _isMouseDown = false,
  55. _isMouseOverCanvas = false,
  56. _drag = false;
  57. if (renderer instanceof sigma.renderers.svg) {
  58. _mouse = renderer.container.firstChild;
  59. }
  60. // It removes the initial substring ('read_') if it's a WegGL renderer.
  61. if (renderer instanceof sigma.renderers.webgl) {
  62. _prefix = renderer.options.prefix.substr(5);
  63. } else {
  64. _prefix = renderer.options.prefix;
  65. }
  66. renderer.bind('overNode', nodeMouseOver);
  67. renderer.bind('outNode', treatOutNode);
  68. renderer.bind('click', click);
  69. _s.bind('kill', function() {
  70. _self.unbindAll();
  71. });
  72. /**
  73. * Unbind all event listeners.
  74. */
  75. this.unbindAll = function() {
  76. _mouse.removeEventListener('mousedown', nodeMouseDown);
  77. _body.removeEventListener('mousemove', nodeMouseMove);
  78. _body.removeEventListener('mouseup', nodeMouseUp);
  79. _renderer.unbind('overNode', nodeMouseOver);
  80. _renderer.unbind('outNode', treatOutNode);
  81. }
  82. // Calculates the global offset of the given element more accurately than
  83. // element.offsetTop and element.offsetLeft.
  84. function calculateOffset(element) {
  85. var style = window.getComputedStyle(element);
  86. var getCssProperty = function(prop) {
  87. return parseInt(style.getPropertyValue(prop).replace('px', '')) || 0;
  88. };
  89. return {
  90. left: element.getBoundingClientRect().left + getCssProperty('padding-left'),
  91. top: element.getBoundingClientRect().top + getCssProperty('padding-top')
  92. };
  93. };
  94. function click(event) {
  95. // event triggered at the end of the click
  96. _isMouseDown = false;
  97. _body.removeEventListener('mousemove', nodeMouseMove);
  98. _body.removeEventListener('mouseup', nodeMouseUp);
  99. if (!_hoverStack.length) {
  100. _node = null;
  101. }
  102. };
  103. function nodeMouseOver(event) {
  104. // Don't treat the node if it is already registered
  105. if (_hoverIndex[event.data.node.id]) {
  106. return;
  107. }
  108. // Add node to array of current nodes over
  109. _hoverStack.push(event.data.node);
  110. _hoverIndex[event.data.node.id] = true;
  111. if(_hoverStack.length && ! _isMouseDown) {
  112. // Set the current node to be the last one in the array
  113. _node = _hoverStack[_hoverStack.length - 1];
  114. _mouse.addEventListener('mousedown', nodeMouseDown);
  115. }
  116. };
  117. function treatOutNode(event) {
  118. // Remove the node from the array
  119. var indexCheck = _hoverStack.map(function(e) { return e; }).indexOf(event.data.node);
  120. _hoverStack.splice(indexCheck, 1);
  121. delete _hoverIndex[event.data.node.id];
  122. if(_hoverStack.length && ! _isMouseDown) {
  123. // On out, set the current node to be the next stated in array
  124. _node = _hoverStack[_hoverStack.length - 1];
  125. } else {
  126. _mouse.removeEventListener('mousedown', nodeMouseDown);
  127. }
  128. };
  129. function nodeMouseDown(event) {
  130. _isMouseDown = true;
  131. var size = _s.graph.nodes().length;
  132. // when there is only node in the graph, the plugin cannot apply
  133. // linear interpolation. So treat it as if a user is dragging
  134. // the graph
  135. if (_node && size > 1) {
  136. _mouse.removeEventListener('mousedown', nodeMouseDown);
  137. _body.addEventListener('mousemove', nodeMouseMove);
  138. _body.addEventListener('mouseup', nodeMouseUp);
  139. // Do not refresh edgequadtree during drag:
  140. var k,
  141. c;
  142. for (k in _s.cameras) {
  143. c = _s.cameras[k];
  144. if (c.edgequadtree !== undefined) {
  145. c.edgequadtree._enabled = false;
  146. }
  147. }
  148. // Deactivate drag graph.
  149. _renderer.settings({mouseEnabled: false, enableHovering: false});
  150. _s.refresh();
  151. _self.dispatchEvent('startdrag', {
  152. node: _node,
  153. captor: event,
  154. renderer: _renderer
  155. });
  156. }
  157. };
  158. function nodeMouseUp(event) {
  159. _isMouseDown = false;
  160. _mouse.addEventListener('mousedown', nodeMouseDown);
  161. _body.removeEventListener('mousemove', nodeMouseMove);
  162. _body.removeEventListener('mouseup', nodeMouseUp);
  163. // Allow to refresh edgequadtree:
  164. var k,
  165. c;
  166. for (k in _s.cameras) {
  167. c = _s.cameras[k];
  168. if (c.edgequadtree !== undefined) {
  169. c.edgequadtree._enabled = true;
  170. }
  171. }
  172. // Activate drag graph.
  173. _renderer.settings({mouseEnabled: true, enableHovering: true});
  174. _s.refresh();
  175. if (_drag) {
  176. _self.dispatchEvent('drop', {
  177. node: _node,
  178. captor: event,
  179. renderer: _renderer
  180. });
  181. }
  182. _self.dispatchEvent('dragend', {
  183. node: _node,
  184. captor: event,
  185. renderer: _renderer
  186. });
  187. _drag = false;
  188. _node = null;
  189. };
  190. function nodeMouseMove(event) {
  191. if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
  192. clearTimeout(timeOut);
  193. var timeOut = setTimeout(executeNodeMouseMove, 0);
  194. } else {
  195. executeNodeMouseMove();
  196. }
  197. function executeNodeMouseMove() {
  198. var offset = calculateOffset(_renderer.container),
  199. x = event.clientX - offset.left,
  200. y = event.clientY - offset.top,
  201. cos = Math.cos(_camera.angle),
  202. sin = Math.sin(_camera.angle),
  203. nodes = _s.graph.nodes(),
  204. ref = [];
  205. // Getting and derotating the reference coordinates.
  206. for (var i = 0; i < 2; i++) {
  207. var n = nodes[i];
  208. var aux = {
  209. x: n.x * cos + n.y * sin,
  210. y: n.y * cos - n.x * sin,
  211. renX: n[_prefix + 'x'],
  212. renY: n[_prefix + 'y'],
  213. };
  214. ref.push(aux);
  215. }
  216. // Applying linear interpolation.
  217. // if the nodes are on top of each other, we use the camera ratio to interpolate
  218. if (ref[0].x === ref[1].x && ref[0].y === ref[1].y) {
  219. var xRatio = (ref[0].renX === 0) ? 1 : ref[0].renX;
  220. var yRatio = (ref[0].renY === 0) ? 1 : ref[0].renY;
  221. x = (ref[0].x / xRatio) * (x - ref[0].renX) + ref[0].x;
  222. y = (ref[0].y / yRatio) * (y - ref[0].renY) + ref[0].y;
  223. } else {
  224. var xRatio = (ref[1].renX - ref[0].renX) / (ref[1].x - ref[0].x);
  225. var yRatio = (ref[1].renY - ref[0].renY) / (ref[1].y - ref[0].y);
  226. // if the coordinates are the same, we use the other ratio to interpolate
  227. if (ref[1].x === ref[0].x) {
  228. xRatio = yRatio;
  229. }
  230. if (ref[1].y === ref[0].y) {
  231. yRatio = xRatio;
  232. }
  233. x = (x - ref[0].renX) / xRatio + ref[0].x;
  234. y = (y - ref[0].renY) / yRatio + ref[0].y;
  235. }
  236. // Rotating the coordinates.
  237. _node.x = x * cos - y * sin;
  238. _node.y = y * cos + x * sin;
  239. _s.refresh();
  240. _drag = true;
  241. _self.dispatchEvent('drag', {
  242. node: _node,
  243. captor: event,
  244. renderer: _renderer
  245. });
  246. }
  247. };
  248. };
  249. /**
  250. * Interface
  251. * ------------------
  252. *
  253. * > var dragNodesListener = sigma.plugins.dragNodes(s, s.renderers[0]);
  254. */
  255. var _instance = {};
  256. /**
  257. * @param {sigma} s The related sigma instance.
  258. * @param {renderer} renderer The related renderer instance.
  259. */
  260. sigma.plugins.dragNodes = function(s, renderer) {
  261. // Create object if undefined
  262. if (!_instance[s.id]) {
  263. _instance[s.id] = new DragNodes(s, renderer);
  264. }
  265. s.bind('kill', function() {
  266. sigma.plugins.killDragNodes(s);
  267. });
  268. return _instance[s.id];
  269. };
  270. /**
  271. * This method removes the event listeners and kills the dragNodes instance.
  272. *
  273. * @param {sigma} s The related sigma instance.
  274. */
  275. sigma.plugins.killDragNodes = function(s) {
  276. if (_instance[s.id] instanceof DragNodes) {
  277. _instance[s.id].unbindAll();
  278. delete _instance[s.id];
  279. }
  280. };
  281. }).call(window);