InteractionEvents.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. import {
  2. forEach,
  3. assign
  4. } from 'min-dash';
  5. import {
  6. delegate as domDelegate,
  7. query as domQuery,
  8. queryAll as domQueryAll
  9. } from 'min-dom';
  10. import {
  11. isPrimaryButton,
  12. isAuxiliaryButton
  13. } from '../../util/Mouse';
  14. import {
  15. append as svgAppend,
  16. attr as svgAttr,
  17. create as svgCreate,
  18. remove as svgRemove
  19. } from 'tiny-svg';
  20. import {
  21. createLine,
  22. updateLine
  23. } from '../../util/RenderUtil';
  24. function allowAll(event) { return true; }
  25. function allowPrimaryAndAuxiliary(event) {
  26. return isPrimaryButton(event) || isAuxiliaryButton(event);
  27. }
  28. var LOW_PRIORITY = 500;
  29. /**
  30. * A plugin that provides interaction events for diagram elements.
  31. *
  32. * It emits the following events:
  33. *
  34. * * element.click
  35. * * element.contextmenu
  36. * * element.dblclick
  37. * * element.hover
  38. * * element.mousedown
  39. * * element.mousemove
  40. * * element.mouseup
  41. * * element.out
  42. *
  43. * Each event is a tuple { element, gfx, originalEvent }.
  44. *
  45. * Canceling the event via Event#preventDefault()
  46. * prevents the original DOM operation.
  47. *
  48. * @param {EventBus} eventBus
  49. */
  50. export default function InteractionEvents(eventBus, elementRegistry, styles) {
  51. var self = this;
  52. /**
  53. * Fire an interaction event.
  54. *
  55. * @param {string} type local event name, e.g. element.click.
  56. * @param {DOMEvent} event native event
  57. * @param {djs.model.Base} [element] the diagram element to emit the event on;
  58. * defaults to the event target
  59. */
  60. function fire(type, event, element) {
  61. if (isIgnored(type, event)) {
  62. return;
  63. }
  64. var target, gfx, returnValue;
  65. if (!element) {
  66. target = event.delegateTarget || event.target;
  67. if (target) {
  68. gfx = target;
  69. element = elementRegistry.get(gfx);
  70. }
  71. } else {
  72. gfx = elementRegistry.getGraphics(element);
  73. }
  74. if (!gfx || !element) {
  75. return;
  76. }
  77. returnValue = eventBus.fire(type, {
  78. element: element,
  79. gfx: gfx,
  80. originalEvent: event
  81. });
  82. if (returnValue === false) {
  83. event.stopPropagation();
  84. event.preventDefault();
  85. }
  86. }
  87. // TODO(nikku): document this
  88. var handlers = {};
  89. function mouseHandler(localEventName) {
  90. return handlers[localEventName];
  91. }
  92. function isIgnored(localEventName, event) {
  93. var filter = ignoredFilters[localEventName] || isPrimaryButton;
  94. // only react on left mouse button interactions
  95. // except for interaction events that are enabled
  96. // for secundary mouse button
  97. return !filter(event);
  98. }
  99. var bindings = {
  100. click: 'element.click',
  101. contextmenu: 'element.contextmenu',
  102. dblclick: 'element.dblclick',
  103. mousedown: 'element.mousedown',
  104. mousemove: 'element.mousemove',
  105. mouseover: 'element.hover',
  106. mouseout: 'element.out',
  107. mouseup: 'element.mouseup',
  108. };
  109. var ignoredFilters = {
  110. 'element.contextmenu': allowAll,
  111. 'element.mousedown': allowPrimaryAndAuxiliary,
  112. 'element.mouseup': allowPrimaryAndAuxiliary,
  113. 'element.click': allowPrimaryAndAuxiliary,
  114. 'element.dblclick': allowPrimaryAndAuxiliary
  115. };
  116. // manual event trigger //////////
  117. /**
  118. * Trigger an interaction event (based on a native dom event)
  119. * on the target shape or connection.
  120. *
  121. * @param {string} eventName the name of the triggered DOM event
  122. * @param {MouseEvent} event
  123. * @param {djs.model.Base} targetElement
  124. */
  125. function triggerMouseEvent(eventName, event, targetElement) {
  126. // i.e. element.mousedown...
  127. var localEventName = bindings[eventName];
  128. if (!localEventName) {
  129. throw new Error('unmapped DOM event name <' + eventName + '>');
  130. }
  131. return fire(localEventName, event, targetElement);
  132. }
  133. var ELEMENT_SELECTOR = 'svg, .djs-element';
  134. // event handling ///////
  135. function registerEvent(node, event, localEvent, ignoredFilter) {
  136. var handler = handlers[localEvent] = function(event) {
  137. fire(localEvent, event);
  138. };
  139. if (ignoredFilter) {
  140. ignoredFilters[localEvent] = ignoredFilter;
  141. }
  142. handler.$delegate = domDelegate.bind(node, ELEMENT_SELECTOR, event, handler);
  143. }
  144. function unregisterEvent(node, event, localEvent) {
  145. var handler = mouseHandler(localEvent);
  146. if (!handler) {
  147. return;
  148. }
  149. domDelegate.unbind(node, event, handler.$delegate);
  150. }
  151. function registerEvents(svg) {
  152. forEach(bindings, function(val, key) {
  153. registerEvent(svg, key, val);
  154. });
  155. }
  156. function unregisterEvents(svg) {
  157. forEach(bindings, function(val, key) {
  158. unregisterEvent(svg, key, val);
  159. });
  160. }
  161. eventBus.on('canvas.destroy', function(event) {
  162. unregisterEvents(event.svg);
  163. });
  164. eventBus.on('canvas.init', function(event) {
  165. registerEvents(event.svg);
  166. });
  167. // hit box updating ////////////////
  168. eventBus.on([ 'shape.added', 'connection.added' ], function(event) {
  169. var element = event.element,
  170. gfx = event.gfx;
  171. eventBus.fire('interactionEvents.createHit', { element: element, gfx: gfx });
  172. });
  173. // Update djs-hit on change.
  174. // A low priortity is necessary, because djs-hit of labels has to be updated
  175. // after the label bounds have been updated in the renderer.
  176. eventBus.on([
  177. 'shape.changed',
  178. 'connection.changed'
  179. ], LOW_PRIORITY, function(event) {
  180. var element = event.element,
  181. gfx = event.gfx;
  182. eventBus.fire('interactionEvents.updateHit', { element: element, gfx: gfx });
  183. });
  184. eventBus.on('interactionEvents.createHit', LOW_PRIORITY, function(event) {
  185. var element = event.element,
  186. gfx = event.gfx;
  187. self.createDefaultHit(element, gfx);
  188. });
  189. eventBus.on('interactionEvents.updateHit', function(event) {
  190. var element = event.element,
  191. gfx = event.gfx;
  192. self.updateDefaultHit(element, gfx);
  193. });
  194. // hit styles ////////////
  195. var STROKE_HIT_STYLE = createHitStyle('djs-hit djs-hit-stroke');
  196. var CLICK_STROKE_HIT_STYLE = createHitStyle('djs-hit djs-hit-click-stroke');
  197. var ALL_HIT_STYLE = createHitStyle('djs-hit djs-hit-all');
  198. var NO_MOVE_HIT_STYLE = createHitStyle('djs-hit djs-hit-no-move');
  199. var HIT_TYPES = {
  200. 'all': ALL_HIT_STYLE,
  201. 'click-stroke': CLICK_STROKE_HIT_STYLE,
  202. 'stroke': STROKE_HIT_STYLE,
  203. 'no-move': NO_MOVE_HIT_STYLE
  204. };
  205. function createHitStyle(classNames, attrs) {
  206. attrs = assign({
  207. stroke: 'white',
  208. strokeWidth: 15
  209. }, attrs || {});
  210. return styles.cls(classNames, [ 'no-fill', 'no-border' ], attrs);
  211. }
  212. // style helpers ///////////////
  213. function applyStyle(hit, type) {
  214. var attrs = HIT_TYPES[type];
  215. if (!attrs) {
  216. throw new Error('invalid hit type <' + type + '>');
  217. }
  218. svgAttr(hit, attrs);
  219. return hit;
  220. }
  221. function appendHit(gfx, hit) {
  222. svgAppend(gfx, hit);
  223. }
  224. // API
  225. /**
  226. * Remove hints on the given graphics.
  227. *
  228. * @param {SVGElement} gfx
  229. */
  230. this.removeHits = function(gfx) {
  231. var hits = domQueryAll('.djs-hit', gfx);
  232. forEach(hits, svgRemove);
  233. };
  234. /**
  235. * Create default hit for the given element.
  236. *
  237. * @param {djs.model.Base} element
  238. * @param {SVGElement} gfx
  239. *
  240. * @return {SVGElement} created hit
  241. */
  242. this.createDefaultHit = function(element, gfx) {
  243. var waypoints = element.waypoints,
  244. isFrame = element.isFrame,
  245. boxType;
  246. if (waypoints) {
  247. return this.createWaypointsHit(gfx, waypoints);
  248. } else {
  249. boxType = isFrame ? 'stroke' : 'all';
  250. return this.createBoxHit(gfx, boxType, {
  251. width: element.width,
  252. height: element.height
  253. });
  254. }
  255. };
  256. /**
  257. * Create hits for the given waypoints.
  258. *
  259. * @param {SVGElement} gfx
  260. * @param {Array<Point>} waypoints
  261. *
  262. * @return {SVGElement}
  263. */
  264. this.createWaypointsHit = function(gfx, waypoints) {
  265. var hit = createLine(waypoints);
  266. applyStyle(hit, 'stroke');
  267. appendHit(gfx, hit);
  268. return hit;
  269. };
  270. /**
  271. * Create hits for a box.
  272. *
  273. * @param {SVGElement} gfx
  274. * @param {string} hitType
  275. * @param {Object} attrs
  276. *
  277. * @return {SVGElement}
  278. */
  279. this.createBoxHit = function(gfx, type, attrs) {
  280. attrs = assign({
  281. x: 0,
  282. y: 0
  283. }, attrs);
  284. var hit = svgCreate('rect');
  285. applyStyle(hit, type);
  286. svgAttr(hit, attrs);
  287. appendHit(gfx, hit);
  288. return hit;
  289. };
  290. /**
  291. * Update default hit of the element.
  292. *
  293. * @param {djs.model.Base} element
  294. * @param {SVGElement} gfx
  295. *
  296. * @return {SVGElement} updated hit
  297. */
  298. this.updateDefaultHit = function(element, gfx) {
  299. var hit = domQuery('.djs-hit', gfx);
  300. if (!hit) {
  301. return;
  302. }
  303. if (element.waypoints) {
  304. updateLine(hit, element.waypoints);
  305. } else {
  306. svgAttr(hit, {
  307. width: element.width,
  308. height: element.height
  309. });
  310. }
  311. return hit;
  312. };
  313. this.fire = fire;
  314. this.triggerMouseEvent = triggerMouseEvent;
  315. this.mouseHandler = mouseHandler;
  316. this.registerEvent = registerEvent;
  317. this.unregisterEvent = unregisterEvent;
  318. }
  319. InteractionEvents.$inject = [
  320. 'eventBus',
  321. 'elementRegistry',
  322. 'styles'
  323. ];
  324. /**
  325. * An event indicating that the mouse hovered over an element
  326. *
  327. * @event element.hover
  328. *
  329. * @type {Object}
  330. * @property {djs.model.Base} element
  331. * @property {SVGElement} gfx
  332. * @property {Event} originalEvent
  333. */
  334. /**
  335. * An event indicating that the mouse has left an element
  336. *
  337. * @event element.out
  338. *
  339. * @type {Object}
  340. * @property {djs.model.Base} element
  341. * @property {SVGElement} gfx
  342. * @property {Event} originalEvent
  343. */
  344. /**
  345. * An event indicating that the mouse has clicked an element
  346. *
  347. * @event element.click
  348. *
  349. * @type {Object}
  350. * @property {djs.model.Base} element
  351. * @property {SVGElement} gfx
  352. * @property {Event} originalEvent
  353. */
  354. /**
  355. * An event indicating that the mouse has double clicked an element
  356. *
  357. * @event element.dblclick
  358. *
  359. * @type {Object}
  360. * @property {djs.model.Base} element
  361. * @property {SVGElement} gfx
  362. * @property {Event} originalEvent
  363. */
  364. /**
  365. * An event indicating that the mouse has gone down on an element.
  366. *
  367. * @event element.mousedown
  368. *
  369. * @type {Object}
  370. * @property {djs.model.Base} element
  371. * @property {SVGElement} gfx
  372. * @property {Event} originalEvent
  373. */
  374. /**
  375. * An event indicating that the mouse has gone up on an element.
  376. *
  377. * @event element.mouseup
  378. *
  379. * @type {Object}
  380. * @property {djs.model.Base} element
  381. * @property {SVGElement} gfx
  382. * @property {Event} originalEvent
  383. */
  384. /**
  385. * An event indicating that the context menu action is triggered
  386. * via mouse or touch controls.
  387. *
  388. * @event element.contextmenu
  389. *
  390. * @type {Object}
  391. * @property {djs.model.Base} element
  392. * @property {SVGElement} gfx
  393. * @property {Event} originalEvent
  394. */