EventBus.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import {
  2. isFunction,
  3. isArray,
  4. isNumber,
  5. bind,
  6. assign
  7. } from 'min-dash';
  8. var FN_REF = '__fn';
  9. var DEFAULT_PRIORITY = 1000;
  10. var slice = Array.prototype.slice;
  11. /**
  12. * A general purpose event bus.
  13. *
  14. * This component is used to communicate across a diagram instance.
  15. * Other parts of a diagram can use it to listen to and broadcast events.
  16. *
  17. *
  18. * ## Registering for Events
  19. *
  20. * The event bus provides the {@link EventBus#on} and {@link EventBus#once}
  21. * methods to register for events. {@link EventBus#off} can be used to
  22. * remove event registrations. Listeners receive an instance of {@link Event}
  23. * as the first argument. It allows them to hook into the event execution.
  24. *
  25. * ```javascript
  26. *
  27. * // listen for event
  28. * eventBus.on('foo', function(event) {
  29. *
  30. * // access event type
  31. * event.type; // 'foo'
  32. *
  33. * // stop propagation to other listeners
  34. * event.stopPropagation();
  35. *
  36. * // prevent event default
  37. * event.preventDefault();
  38. * });
  39. *
  40. * // listen for event with custom payload
  41. * eventBus.on('bar', function(event, payload) {
  42. * console.log(payload);
  43. * });
  44. *
  45. * // listen for event returning value
  46. * eventBus.on('foobar', function(event) {
  47. *
  48. * // stop event propagation + prevent default
  49. * return false;
  50. *
  51. * // stop event propagation + return custom result
  52. * return {
  53. * complex: 'listening result'
  54. * };
  55. * });
  56. *
  57. *
  58. * // listen with custom priority (default=1000, higher is better)
  59. * eventBus.on('priorityfoo', 1500, function(event) {
  60. * console.log('invoked first!');
  61. * });
  62. *
  63. *
  64. * // listen for event and pass the context (`this`)
  65. * eventBus.on('foobar', function(event) {
  66. * this.foo();
  67. * }, this);
  68. * ```
  69. *
  70. *
  71. * ## Emitting Events
  72. *
  73. * Events can be emitted via the event bus using {@link EventBus#fire}.
  74. *
  75. * ```javascript
  76. *
  77. * // false indicates that the default action
  78. * // was prevented by listeners
  79. * if (eventBus.fire('foo') === false) {
  80. * console.log('default has been prevented!');
  81. * };
  82. *
  83. *
  84. * // custom args + return value listener
  85. * eventBus.on('sum', function(event, a, b) {
  86. * return a + b;
  87. * });
  88. *
  89. * // you can pass custom arguments + retrieve result values.
  90. * var sum = eventBus.fire('sum', 1, 2);
  91. * console.log(sum); // 3
  92. * ```
  93. */
  94. export default function EventBus() {
  95. this._listeners = {};
  96. // cleanup on destroy on lowest priority to allow
  97. // message passing until the bitter end
  98. this.on('diagram.destroy', 1, this._destroy, this);
  99. }
  100. /**
  101. * Register an event listener for events with the given name.
  102. *
  103. * The callback will be invoked with `event, ...additionalArguments`
  104. * that have been passed to {@link EventBus#fire}.
  105. *
  106. * Returning false from a listener will prevent the events default action
  107. * (if any is specified). To stop an event from being processed further in
  108. * other listeners execute {@link Event#stopPropagation}.
  109. *
  110. * Returning anything but `undefined` from a listener will stop the listener propagation.
  111. *
  112. * @param {String|Array<String>} events
  113. * @param {Number} [priority=1000] the priority in which this listener is called, larger is higher
  114. * @param {Function} callback
  115. * @param {Object} [that] Pass context (`this`) to the callback
  116. */
  117. EventBus.prototype.on = function(events, priority, callback, that) {
  118. events = isArray(events) ? events : [ events ];
  119. if (isFunction(priority)) {
  120. that = callback;
  121. callback = priority;
  122. priority = DEFAULT_PRIORITY;
  123. }
  124. if (!isNumber(priority)) {
  125. throw new Error('priority must be a number');
  126. }
  127. var actualCallback = callback;
  128. if (that) {
  129. actualCallback = bind(callback, that);
  130. // make sure we remember and are able to remove
  131. // bound callbacks via {@link #off} using the original
  132. // callback
  133. actualCallback[FN_REF] = callback[FN_REF] || callback;
  134. }
  135. var self = this;
  136. events.forEach(function(e) {
  137. self._addListener(e, {
  138. priority: priority,
  139. callback: actualCallback,
  140. next: null
  141. });
  142. });
  143. };
  144. /**
  145. * Register an event listener that is executed only once.
  146. *
  147. * @param {String} event the event name to register for
  148. * @param {Function} callback the callback to execute
  149. * @param {Object} [that] Pass context (`this`) to the callback
  150. */
  151. EventBus.prototype.once = function(event, priority, callback, that) {
  152. var self = this;
  153. if (isFunction(priority)) {
  154. that = callback;
  155. callback = priority;
  156. priority = DEFAULT_PRIORITY;
  157. }
  158. if (!isNumber(priority)) {
  159. throw new Error('priority must be a number');
  160. }
  161. function wrappedCallback() {
  162. var result = callback.apply(that, arguments);
  163. self.off(event, wrappedCallback);
  164. return result;
  165. }
  166. // make sure we remember and are able to remove
  167. // bound callbacks via {@link #off} using the original
  168. // callback
  169. wrappedCallback[FN_REF] = callback;
  170. this.on(event, priority, wrappedCallback);
  171. };
  172. /**
  173. * Removes event listeners by event and callback.
  174. *
  175. * If no callback is given, all listeners for a given event name are being removed.
  176. *
  177. * @param {String|Array<String>} events
  178. * @param {Function} [callback]
  179. */
  180. EventBus.prototype.off = function(events, callback) {
  181. events = isArray(events) ? events : [ events ];
  182. var self = this;
  183. events.forEach(function(event) {
  184. self._removeListener(event, callback);
  185. });
  186. };
  187. /**
  188. * Create an EventBus event.
  189. *
  190. * @param {Object} data
  191. *
  192. * @return {Object} event, recognized by the eventBus
  193. */
  194. EventBus.prototype.createEvent = function(data) {
  195. var event = new InternalEvent();
  196. event.init(data);
  197. return event;
  198. };
  199. /**
  200. * Fires a named event.
  201. *
  202. * @example
  203. *
  204. * // fire event by name
  205. * events.fire('foo');
  206. *
  207. * // fire event object with nested type
  208. * var event = { type: 'foo' };
  209. * events.fire(event);
  210. *
  211. * // fire event with explicit type
  212. * var event = { x: 10, y: 20 };
  213. * events.fire('element.moved', event);
  214. *
  215. * // pass additional arguments to the event
  216. * events.on('foo', function(event, bar) {
  217. * alert(bar);
  218. * });
  219. *
  220. * events.fire({ type: 'foo' }, 'I am bar!');
  221. *
  222. * @param {String} [name] the optional event name
  223. * @param {Object} [event] the event object
  224. * @param {...Object} additional arguments to be passed to the callback functions
  225. *
  226. * @return {Boolean} the events return value, if specified or false if the
  227. * default action was prevented by listeners
  228. */
  229. EventBus.prototype.fire = function(type, data) {
  230. var event,
  231. firstListener,
  232. returnValue,
  233. args;
  234. args = slice.call(arguments);
  235. if (typeof type === 'object') {
  236. event = type;
  237. type = event.type;
  238. }
  239. if (!type) {
  240. throw new Error('no event type specified');
  241. }
  242. firstListener = this._listeners[type];
  243. if (!firstListener) {
  244. return;
  245. }
  246. // we make sure we fire instances of our home made
  247. // events here. We wrap them only once, though
  248. if (data instanceof InternalEvent) {
  249. // we are fine, we alread have an event
  250. event = data;
  251. } else {
  252. event = this.createEvent(data);
  253. }
  254. // ensure we pass the event as the first parameter
  255. args[0] = event;
  256. // original event type (in case we delegate)
  257. var originalType = event.type;
  258. // update event type before delegation
  259. if (type !== originalType) {
  260. event.type = type;
  261. }
  262. try {
  263. returnValue = this._invokeListeners(event, args, firstListener);
  264. } finally {
  265. // reset event type after delegation
  266. if (type !== originalType) {
  267. event.type = originalType;
  268. }
  269. }
  270. // set the return value to false if the event default
  271. // got prevented and no other return value exists
  272. if (returnValue === undefined && event.defaultPrevented) {
  273. returnValue = false;
  274. }
  275. return returnValue;
  276. };
  277. EventBus.prototype.handleError = function(error) {
  278. return this.fire('error', { error: error }) === false;
  279. };
  280. EventBus.prototype._destroy = function() {
  281. this._listeners = {};
  282. };
  283. EventBus.prototype._invokeListeners = function(event, args, listener) {
  284. var returnValue;
  285. while (listener) {
  286. // handle stopped propagation
  287. if (event.cancelBubble) {
  288. break;
  289. }
  290. returnValue = this._invokeListener(event, args, listener);
  291. listener = listener.next;
  292. }
  293. return returnValue;
  294. };
  295. EventBus.prototype._invokeListener = function(event, args, listener) {
  296. var returnValue;
  297. try {
  298. // returning false prevents the default action
  299. returnValue = invokeFunction(listener.callback, args);
  300. // stop propagation on return value
  301. if (returnValue !== undefined) {
  302. event.returnValue = returnValue;
  303. event.stopPropagation();
  304. }
  305. // prevent default on return false
  306. if (returnValue === false) {
  307. event.preventDefault();
  308. }
  309. } catch (e) {
  310. if (!this.handleError(e)) {
  311. console.error('unhandled error in event listener');
  312. console.error(e.stack);
  313. throw e;
  314. }
  315. }
  316. return returnValue;
  317. };
  318. /*
  319. * Add new listener with a certain priority to the list
  320. * of listeners (for the given event).
  321. *
  322. * The semantics of listener registration / listener execution are
  323. * first register, first serve: New listeners will always be inserted
  324. * after existing listeners with the same priority.
  325. *
  326. * Example: Inserting two listeners with priority 1000 and 1300
  327. *
  328. * * before: [ 1500, 1500, 1000, 1000 ]
  329. * * after: [ 1500, 1500, (new=1300), 1000, 1000, (new=1000) ]
  330. *
  331. * @param {String} event
  332. * @param {Object} listener { priority, callback }
  333. */
  334. EventBus.prototype._addListener = function(event, newListener) {
  335. var listener = this._getListeners(event),
  336. previousListener;
  337. // no prior listeners
  338. if (!listener) {
  339. this._setListeners(event, newListener);
  340. return;
  341. }
  342. // ensure we order listeners by priority from
  343. // 0 (high) to n > 0 (low)
  344. while (listener) {
  345. if (listener.priority < newListener.priority) {
  346. newListener.next = listener;
  347. if (previousListener) {
  348. previousListener.next = newListener;
  349. } else {
  350. this._setListeners(event, newListener);
  351. }
  352. return;
  353. }
  354. previousListener = listener;
  355. listener = listener.next;
  356. }
  357. // add new listener to back
  358. previousListener.next = newListener;
  359. };
  360. EventBus.prototype._getListeners = function(name) {
  361. return this._listeners[name];
  362. };
  363. EventBus.prototype._setListeners = function(name, listener) {
  364. this._listeners[name] = listener;
  365. };
  366. EventBus.prototype._removeListener = function(event, callback) {
  367. var listener = this._getListeners(event),
  368. nextListener,
  369. previousListener,
  370. listenerCallback;
  371. if (!callback) {
  372. // clear listeners
  373. this._setListeners(event, null);
  374. return;
  375. }
  376. while (listener) {
  377. nextListener = listener.next;
  378. listenerCallback = listener.callback;
  379. if (listenerCallback === callback || listenerCallback[FN_REF] === callback) {
  380. if (previousListener) {
  381. previousListener.next = nextListener;
  382. } else {
  383. // new first listener
  384. this._setListeners(event, nextListener);
  385. }
  386. }
  387. previousListener = listener;
  388. listener = nextListener;
  389. }
  390. };
  391. /**
  392. * A event that is emitted via the event bus.
  393. */
  394. function InternalEvent() { }
  395. InternalEvent.prototype.stopPropagation = function() {
  396. this.cancelBubble = true;
  397. };
  398. InternalEvent.prototype.preventDefault = function() {
  399. this.defaultPrevented = true;
  400. };
  401. InternalEvent.prototype.init = function(data) {
  402. assign(this, data || {});
  403. };
  404. /**
  405. * Invoke function. Be fast...
  406. *
  407. * @param {Function} fn
  408. * @param {Array<Object>} args
  409. *
  410. * @return {Any}
  411. */
  412. function invokeFunction(fn, args) {
  413. return fn.apply(null, args);
  414. }