123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- import {
- isFunction,
- isArray,
- isNumber,
- bind,
- assign
- } from 'min-dash';
- var FN_REF = '__fn';
- var DEFAULT_PRIORITY = 1000;
- var slice = Array.prototype.slice;
- /**
- * A general purpose event bus.
- *
- * This component is used to communicate across a diagram instance.
- * Other parts of a diagram can use it to listen to and broadcast events.
- *
- *
- * ## Registering for Events
- *
- * The event bus provides the {@link EventBus#on} and {@link EventBus#once}
- * methods to register for events. {@link EventBus#off} can be used to
- * remove event registrations. Listeners receive an instance of {@link Event}
- * as the first argument. It allows them to hook into the event execution.
- *
- * ```javascript
- *
- * // listen for event
- * eventBus.on('foo', function(event) {
- *
- * // access event type
- * event.type; // 'foo'
- *
- * // stop propagation to other listeners
- * event.stopPropagation();
- *
- * // prevent event default
- * event.preventDefault();
- * });
- *
- * // listen for event with custom payload
- * eventBus.on('bar', function(event, payload) {
- * console.log(payload);
- * });
- *
- * // listen for event returning value
- * eventBus.on('foobar', function(event) {
- *
- * // stop event propagation + prevent default
- * return false;
- *
- * // stop event propagation + return custom result
- * return {
- * complex: 'listening result'
- * };
- * });
- *
- *
- * // listen with custom priority (default=1000, higher is better)
- * eventBus.on('priorityfoo', 1500, function(event) {
- * console.log('invoked first!');
- * });
- *
- *
- * // listen for event and pass the context (`this`)
- * eventBus.on('foobar', function(event) {
- * this.foo();
- * }, this);
- * ```
- *
- *
- * ## Emitting Events
- *
- * Events can be emitted via the event bus using {@link EventBus#fire}.
- *
- * ```javascript
- *
- * // false indicates that the default action
- * // was prevented by listeners
- * if (eventBus.fire('foo') === false) {
- * console.log('default has been prevented!');
- * };
- *
- *
- * // custom args + return value listener
- * eventBus.on('sum', function(event, a, b) {
- * return a + b;
- * });
- *
- * // you can pass custom arguments + retrieve result values.
- * var sum = eventBus.fire('sum', 1, 2);
- * console.log(sum); // 3
- * ```
- */
- export default function EventBus() {
- this._listeners = {};
- // cleanup on destroy on lowest priority to allow
- // message passing until the bitter end
- this.on('diagram.destroy', 1, this._destroy, this);
- }
- /**
- * Register an event listener for events with the given name.
- *
- * The callback will be invoked with `event, ...additionalArguments`
- * that have been passed to {@link EventBus#fire}.
- *
- * Returning false from a listener will prevent the events default action
- * (if any is specified). To stop an event from being processed further in
- * other listeners execute {@link Event#stopPropagation}.
- *
- * Returning anything but `undefined` from a listener will stop the listener propagation.
- *
- * @param {String|Array<String>} events
- * @param {Number} [priority=1000] the priority in which this listener is called, larger is higher
- * @param {Function} callback
- * @param {Object} [that] Pass context (`this`) to the callback
- */
- EventBus.prototype.on = function(events, priority, callback, that) {
- events = isArray(events) ? events : [ events ];
- if (isFunction(priority)) {
- that = callback;
- callback = priority;
- priority = DEFAULT_PRIORITY;
- }
- if (!isNumber(priority)) {
- throw new Error('priority must be a number');
- }
- var actualCallback = callback;
- if (that) {
- actualCallback = bind(callback, that);
- // make sure we remember and are able to remove
- // bound callbacks via {@link #off} using the original
- // callback
- actualCallback[FN_REF] = callback[FN_REF] || callback;
- }
- var self = this;
- events.forEach(function(e) {
- self._addListener(e, {
- priority: priority,
- callback: actualCallback,
- next: null
- });
- });
- };
- /**
- * Register an event listener that is executed only once.
- *
- * @param {String} event the event name to register for
- * @param {Function} callback the callback to execute
- * @param {Object} [that] Pass context (`this`) to the callback
- */
- EventBus.prototype.once = function(event, priority, callback, that) {
- var self = this;
- if (isFunction(priority)) {
- that = callback;
- callback = priority;
- priority = DEFAULT_PRIORITY;
- }
- if (!isNumber(priority)) {
- throw new Error('priority must be a number');
- }
- function wrappedCallback() {
- var result = callback.apply(that, arguments);
- self.off(event, wrappedCallback);
- return result;
- }
- // make sure we remember and are able to remove
- // bound callbacks via {@link #off} using the original
- // callback
- wrappedCallback[FN_REF] = callback;
- this.on(event, priority, wrappedCallback);
- };
- /**
- * Removes event listeners by event and callback.
- *
- * If no callback is given, all listeners for a given event name are being removed.
- *
- * @param {String|Array<String>} events
- * @param {Function} [callback]
- */
- EventBus.prototype.off = function(events, callback) {
- events = isArray(events) ? events : [ events ];
- var self = this;
- events.forEach(function(event) {
- self._removeListener(event, callback);
- });
- };
- /**
- * Create an EventBus event.
- *
- * @param {Object} data
- *
- * @return {Object} event, recognized by the eventBus
- */
- EventBus.prototype.createEvent = function(data) {
- var event = new InternalEvent();
- event.init(data);
- return event;
- };
- /**
- * Fires a named event.
- *
- * @example
- *
- * // fire event by name
- * events.fire('foo');
- *
- * // fire event object with nested type
- * var event = { type: 'foo' };
- * events.fire(event);
- *
- * // fire event with explicit type
- * var event = { x: 10, y: 20 };
- * events.fire('element.moved', event);
- *
- * // pass additional arguments to the event
- * events.on('foo', function(event, bar) {
- * alert(bar);
- * });
- *
- * events.fire({ type: 'foo' }, 'I am bar!');
- *
- * @param {String} [name] the optional event name
- * @param {Object} [event] the event object
- * @param {...Object} additional arguments to be passed to the callback functions
- *
- * @return {Boolean} the events return value, if specified or false if the
- * default action was prevented by listeners
- */
- EventBus.prototype.fire = function(type, data) {
- var event,
- firstListener,
- returnValue,
- args;
- args = slice.call(arguments);
- if (typeof type === 'object') {
- event = type;
- type = event.type;
- }
- if (!type) {
- throw new Error('no event type specified');
- }
- firstListener = this._listeners[type];
- if (!firstListener) {
- return;
- }
- // we make sure we fire instances of our home made
- // events here. We wrap them only once, though
- if (data instanceof InternalEvent) {
- // we are fine, we alread have an event
- event = data;
- } else {
- event = this.createEvent(data);
- }
- // ensure we pass the event as the first parameter
- args[0] = event;
- // original event type (in case we delegate)
- var originalType = event.type;
- // update event type before delegation
- if (type !== originalType) {
- event.type = type;
- }
- try {
- returnValue = this._invokeListeners(event, args, firstListener);
- } finally {
- // reset event type after delegation
- if (type !== originalType) {
- event.type = originalType;
- }
- }
- // set the return value to false if the event default
- // got prevented and no other return value exists
- if (returnValue === undefined && event.defaultPrevented) {
- returnValue = false;
- }
- return returnValue;
- };
- EventBus.prototype.handleError = function(error) {
- return this.fire('error', { error: error }) === false;
- };
- EventBus.prototype._destroy = function() {
- this._listeners = {};
- };
- EventBus.prototype._invokeListeners = function(event, args, listener) {
- var returnValue;
- while (listener) {
- // handle stopped propagation
- if (event.cancelBubble) {
- break;
- }
- returnValue = this._invokeListener(event, args, listener);
- listener = listener.next;
- }
- return returnValue;
- };
- EventBus.prototype._invokeListener = function(event, args, listener) {
- var returnValue;
- try {
- // returning false prevents the default action
- returnValue = invokeFunction(listener.callback, args);
- // stop propagation on return value
- if (returnValue !== undefined) {
- event.returnValue = returnValue;
- event.stopPropagation();
- }
- // prevent default on return false
- if (returnValue === false) {
- event.preventDefault();
- }
- } catch (e) {
- if (!this.handleError(e)) {
- console.error('unhandled error in event listener');
- console.error(e.stack);
- throw e;
- }
- }
- return returnValue;
- };
- /*
- * Add new listener with a certain priority to the list
- * of listeners (for the given event).
- *
- * The semantics of listener registration / listener execution are
- * first register, first serve: New listeners will always be inserted
- * after existing listeners with the same priority.
- *
- * Example: Inserting two listeners with priority 1000 and 1300
- *
- * * before: [ 1500, 1500, 1000, 1000 ]
- * * after: [ 1500, 1500, (new=1300), 1000, 1000, (new=1000) ]
- *
- * @param {String} event
- * @param {Object} listener { priority, callback }
- */
- EventBus.prototype._addListener = function(event, newListener) {
- var listener = this._getListeners(event),
- previousListener;
- // no prior listeners
- if (!listener) {
- this._setListeners(event, newListener);
- return;
- }
- // ensure we order listeners by priority from
- // 0 (high) to n > 0 (low)
- while (listener) {
- if (listener.priority < newListener.priority) {
- newListener.next = listener;
- if (previousListener) {
- previousListener.next = newListener;
- } else {
- this._setListeners(event, newListener);
- }
- return;
- }
- previousListener = listener;
- listener = listener.next;
- }
- // add new listener to back
- previousListener.next = newListener;
- };
- EventBus.prototype._getListeners = function(name) {
- return this._listeners[name];
- };
- EventBus.prototype._setListeners = function(name, listener) {
- this._listeners[name] = listener;
- };
- EventBus.prototype._removeListener = function(event, callback) {
- var listener = this._getListeners(event),
- nextListener,
- previousListener,
- listenerCallback;
- if (!callback) {
- // clear listeners
- this._setListeners(event, null);
- return;
- }
- while (listener) {
- nextListener = listener.next;
- listenerCallback = listener.callback;
- if (listenerCallback === callback || listenerCallback[FN_REF] === callback) {
- if (previousListener) {
- previousListener.next = nextListener;
- } else {
- // new first listener
- this._setListeners(event, nextListener);
- }
- }
- previousListener = listener;
- listener = nextListener;
- }
- };
- /**
- * A event that is emitted via the event bus.
- */
- function InternalEvent() { }
- InternalEvent.prototype.stopPropagation = function() {
- this.cancelBubble = true;
- };
- InternalEvent.prototype.preventDefault = function() {
- this.defaultPrevented = true;
- };
- InternalEvent.prototype.init = function(data) {
- assign(this, data || {});
- };
- /**
- * Invoke function. Be fast...
- *
- * @param {Function} fn
- * @param {Array<Object>} args
- *
- * @return {Any}
- */
- function invokeFunction(fn, args) {
- return fn.apply(null, args);
- }
|