EventBus.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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 {number} [priority=1000] the priority in which this listener is called, larger is higher
  149. * @param {Function} callback the callback to execute
  150. * @param {Object} [that] Pass context (`this`) to the callback
  151. */
  152. EventBus.prototype.once = function(event, priority, callback, that) {
  153. var self = this;
  154. if (isFunction(priority)) {
  155. that = callback;
  156. callback = priority;
  157. priority = DEFAULT_PRIORITY;
  158. }
  159. if (!isNumber(priority)) {
  160. throw new Error('priority must be a number');
  161. }
  162. function wrappedCallback() {
  163. wrappedCallback.__isTomb = true;
  164. var result = callback.apply(that, arguments);
  165. self.off(event, wrappedCallback);
  166. return result;
  167. }
  168. // make sure we remember and are able to remove
  169. // bound callbacks via {@link #off} using the original
  170. // callback
  171. wrappedCallback[FN_REF] = callback;
  172. this.on(event, priority, wrappedCallback);
  173. };
  174. /**
  175. * Removes event listeners by event and callback.
  176. *
  177. * If no callback is given, all listeners for a given event name are being removed.
  178. *
  179. * @param {string|Array<string>} events
  180. * @param {Function} [callback]
  181. */
  182. EventBus.prototype.off = function(events, callback) {
  183. events = isArray(events) ? events : [ events ];
  184. var self = this;
  185. events.forEach(function(event) {
  186. self._removeListener(event, callback);
  187. });
  188. };
  189. /**
  190. * Create an EventBus event.
  191. *
  192. * @param {Object} data
  193. *
  194. * @return {Object} event, recognized by the eventBus
  195. */
  196. EventBus.prototype.createEvent = function(data) {
  197. var event = new InternalEvent();
  198. event.init(data);
  199. return event;
  200. };
  201. /**
  202. * Fires a named event.
  203. *
  204. * @example
  205. *
  206. * // fire event by name
  207. * events.fire('foo');
  208. *
  209. * // fire event object with nested type
  210. * var event = { type: 'foo' };
  211. * events.fire(event);
  212. *
  213. * // fire event with explicit type
  214. * var event = { x: 10, y: 20 };
  215. * events.fire('element.moved', event);
  216. *
  217. * // pass additional arguments to the event
  218. * events.on('foo', function(event, bar) {
  219. * alert(bar);
  220. * });
  221. *
  222. * events.fire({ type: 'foo' }, 'I am bar!');
  223. *
  224. * @param {string} [name] the optional event name
  225. * @param {Object} [event] the event object
  226. * @param {...Object} additional arguments to be passed to the callback functions
  227. *
  228. * @return {boolean} the events return value, if specified or false if the
  229. * default action was prevented by listeners
  230. */
  231. EventBus.prototype.fire = function(type, data) {
  232. var event,
  233. firstListener,
  234. returnValue,
  235. args;
  236. args = slice.call(arguments);
  237. if (typeof type === 'object') {
  238. data = type;
  239. type = data.type;
  240. }
  241. if (!type) {
  242. throw new Error('no event type specified');
  243. }
  244. firstListener = this._listeners[type];
  245. if (!firstListener) {
  246. return;
  247. }
  248. // we make sure we fire instances of our home made
  249. // events here. We wrap them only once, though
  250. if (data instanceof InternalEvent) {
  251. // we are fine, we alread have an event
  252. event = data;
  253. } else {
  254. event = this.createEvent(data);
  255. }
  256. // ensure we pass the event as the first parameter
  257. args[0] = event;
  258. // original event type (in case we delegate)
  259. var originalType = event.type;
  260. // update event type before delegation
  261. if (type !== originalType) {
  262. event.type = type;
  263. }
  264. try {
  265. returnValue = this._invokeListeners(event, args, firstListener);
  266. } finally {
  267. // reset event type after delegation
  268. if (type !== originalType) {
  269. event.type = originalType;
  270. }
  271. }
  272. // set the return value to false if the event default
  273. // got prevented and no other return value exists
  274. if (returnValue === undefined && event.defaultPrevented) {
  275. returnValue = false;
  276. }
  277. return returnValue;
  278. };
  279. EventBus.prototype.handleError = function(error) {
  280. return this.fire('error', { error: error }) === false;
  281. };
  282. EventBus.prototype._destroy = function() {
  283. this._listeners = {};
  284. };
  285. EventBus.prototype._invokeListeners = function(event, args, listener) {
  286. var returnValue;
  287. while (listener) {
  288. // handle stopped propagation
  289. if (event.cancelBubble) {
  290. break;
  291. }
  292. returnValue = this._invokeListener(event, args, listener);
  293. listener = listener.next;
  294. }
  295. return returnValue;
  296. };
  297. EventBus.prototype._invokeListener = function(event, args, listener) {
  298. var returnValue;
  299. if (listener.callback.__isTomb) {
  300. return returnValue;
  301. }
  302. try {
  303. // returning false prevents the default action
  304. returnValue = invokeFunction(listener.callback, args);
  305. // stop propagation on return value
  306. if (returnValue !== undefined) {
  307. event.returnValue = returnValue;
  308. event.stopPropagation();
  309. }
  310. // prevent default on return false
  311. if (returnValue === false) {
  312. event.preventDefault();
  313. }
  314. } catch (error) {
  315. if (!this.handleError(error)) {
  316. console.error('unhandled error in event listener', error);
  317. throw error;
  318. }
  319. }
  320. return returnValue;
  321. };
  322. /*
  323. * Add new listener with a certain priority to the list
  324. * of listeners (for the given event).
  325. *
  326. * The semantics of listener registration / listener execution are
  327. * first register, first serve: New listeners will always be inserted
  328. * after existing listeners with the same priority.
  329. *
  330. * Example: Inserting two listeners with priority 1000 and 1300
  331. *
  332. * * before: [ 1500, 1500, 1000, 1000 ]
  333. * * after: [ 1500, 1500, (new=1300), 1000, 1000, (new=1000) ]
  334. *
  335. * @param {string} event
  336. * @param {Object} listener { priority, callback }
  337. */
  338. EventBus.prototype._addListener = function(event, newListener) {
  339. var listener = this._getListeners(event),
  340. previousListener;
  341. // no prior listeners
  342. if (!listener) {
  343. this._setListeners(event, newListener);
  344. return;
  345. }
  346. // ensure we order listeners by priority from
  347. // 0 (high) to n > 0 (low)
  348. while (listener) {
  349. if (listener.priority < newListener.priority) {
  350. newListener.next = listener;
  351. if (previousListener) {
  352. previousListener.next = newListener;
  353. } else {
  354. this._setListeners(event, newListener);
  355. }
  356. return;
  357. }
  358. previousListener = listener;
  359. listener = listener.next;
  360. }
  361. // add new listener to back
  362. previousListener.next = newListener;
  363. };
  364. EventBus.prototype._getListeners = function(name) {
  365. return this._listeners[name];
  366. };
  367. EventBus.prototype._setListeners = function(name, listener) {
  368. this._listeners[name] = listener;
  369. };
  370. EventBus.prototype._removeListener = function(event, callback) {
  371. var listener = this._getListeners(event),
  372. nextListener,
  373. previousListener,
  374. listenerCallback;
  375. if (!callback) {
  376. // clear listeners
  377. this._setListeners(event, null);
  378. return;
  379. }
  380. while (listener) {
  381. nextListener = listener.next;
  382. listenerCallback = listener.callback;
  383. if (listenerCallback === callback || listenerCallback[FN_REF] === callback) {
  384. if (previousListener) {
  385. previousListener.next = nextListener;
  386. } else {
  387. // new first listener
  388. this._setListeners(event, nextListener);
  389. }
  390. }
  391. previousListener = listener;
  392. listener = nextListener;
  393. }
  394. };
  395. /**
  396. * A event that is emitted via the event bus.
  397. */
  398. function InternalEvent() { }
  399. InternalEvent.prototype.stopPropagation = function() {
  400. this.cancelBubble = true;
  401. };
  402. InternalEvent.prototype.preventDefault = function() {
  403. this.defaultPrevented = true;
  404. };
  405. InternalEvent.prototype.init = function(data) {
  406. assign(this, data || {});
  407. };
  408. /**
  409. * Invoke function. Be fast...
  410. *
  411. * @param {Function} fn
  412. * @param {Array<Object>} args
  413. *
  414. * @return {Any}
  415. */
  416. function invokeFunction(fn, args) {
  417. return fn.apply(null, args);
  418. }