Home Reference Source

src/utils/Events.js

/**
 * Events emitter.
 *
 * @example
 * const events = new Events();
 * events.on('hello', wat => console.log(wat));
 * events.trigger('hello', 'world');
 * events.off('hello');
 * events.dispose();
 */
export default class Events {

  /**
   * Constructor.
   */
  constructor() {
    this._events = new Map();
    this._onQueue = [];
    this._offQueue = [];
    this._triggerDepth = 0;
  }

  /**
   * Destructor (disposes internal resources).
   *
   * @example
   * events.dispose();
   * events = null;
   */
  dispose() {
    this._events.clear();
    this._onQueue = [];
    this._offQueue = [];
    this._triggerDepth = 0;
  }

  /**
   * Listen for given event.
   *
   * @param {string}	name - Event name.
   * @param {Function}	callback - Event callback. Arguments are passed here from trigger methods.
   *
   * @example
   * events.on('hello', wat => console.log(wat));
   * events.trigger('hello', 'world');
   */
  on(name, callback) {
    if (typeof name !== 'string') {
      throw new Error('`name` is not type of String!');
    }
    if (!(callback instanceof Function)) {
      throw new Error('`callback` is not type of Function!');
    }

    const { _events, _triggerDepth, _onQueue } = this;

    if (_triggerDepth > 0) {
      _onQueue.push({ name, callback });
      return;
    }

    if (!_events.has(name)) {
     _events.set(name, []);
    }

    _events.get(name).push(callback);
  }

  /**
   * Remove given callback from listeners of given event.
   * If callback is omitted, then all callbacks of given event are removed.
   *
   * @param {string}	name - Event name.
   * @param {Function}	callback - Event callback.
   *
   * @example
   * const cb = wat => console.log(wat);
   * events.on('hello', cb);
   * events.off('hello', cb); // remove callback.
   * events.off('hello'); // remove all callbacks.
   */
  off(name, callback) {
    if (typeof name !== 'string') {
      throw new Error('`name` is not type of String!');
    }

    const { _events, _triggerDepth, _offQueue } = this;

    if (!callback) {
      _events.delete(name);
      return;
    }

    if (!(callback instanceof Function)) {
      throw new Error('`callback` is not type of Function!');
    }

    if (_triggerDepth > 0) {
      _offQueue.push({ name, callback });
      return;
    }

    const callbacks = _events.get(name);

    if (!callbacks) {
      return;
    }

    const found = callbacks.indexOf(callback);

    if (found >= 0) {
      callbacks.splice(found, 1);

      if (callbacks.length === 0) {
        _events.delete(name);
      }
    }
  }

  /**
   * Triggers given event with given callback parameters.
   *
   * @param {string}	name - Event name.
   * @param {...*}	args - Event parameters.
   *
   * @example
   * events.on('hello', wat => console.log(wat));
   * events.trigger('hello', 'world');
   */
  trigger(name, ...args) {
    this._trigger(name, false, ...args);
  }

  /**
   * Triggers given event with given callback parameters on next execution frame (delayed).
   *
   * @param {string}	name - Event name.
   * @param {...*}	args - Event parameters.
   *
   * @example
   * events.on('hello', wat => console.log(wat));
   * events.triggerLater('hello', 'world');
   */
  triggerLater(name, ...args) {
    this._trigger(name, true, ...args);
  }

  _trigger(name, delayed, ...args) {
    if (typeof name !== 'string') {
      throw new Error('`name` is not type of String!');
    }

    const { _events, _onQueue, _offQueue } = this;

    if (this._triggerDepth <= 0) {
      for (let i = 0, c = _onQueue.length; i < c; ++i) {
        const { name, callback } = _onQueue[i];

        this.on(name, callback);
      }

      for (let i = 0, c = _offQueue.length; i < c; ++i) {
        const { name, callback } = _offQueue[i];

        this.off(name, callback);
      }

      this._onQueue = [];
      this._offQueue = [];
    }

    const callbacks = _events.get(name);

    if (!callbacks) {
      return;
    }

    if (!!delayed) {
      for (let i = 0, c = callbacks.length; i < c; ++i) {
        setTimeout((callback) => {
          try {
            callback(...args);
          } catch (error) {
            console.error(error);
          }
        }, 0, callbacks[i]);
      }
    } else {
      ++this._triggerDepth;
      for (let i = 0, c = callbacks.length; i < c; ++i) {
        try {
          callbacks[i](...args);
        } catch (error) {
          console.error(error);
        }
      }
      --this._triggerDepth;
    }
  }

}