index.js

/**
 * Signal class
 *
 * @class
 *
 * @license {@link https://opensource.org/licenses/MIT|MIT}
 *
 * @author Patrick Heng <hengpatrick.pro@gmail.com>
 * @author Fabien Motte <contact@fabienmotte.com>
 *
 * @example
 * const mySignal = new Signal()
 *
 * mySignal.add(bar => { console.log(bar) })
 * mySignal.dispatch('foo')
 */
class Signal {
  /**
   * Creates an instance of Signal
   *
   * @constructor
   */
  constructor () {
    /**
     * @type array
     * @private
     */
    this._listeners = []

   /**
     * @type number
     * @private
     */
    this._dispatchNb = 0
  }

  /**
   * Add a listener
   *
   * @param {listenerCallback} cb Callback
   * @param {Object} [options] Options
   * @param {string} [options.priority=0] Higher priority listener will be called earlier
   * @param {string} [options.once=false] Listener called once
   * @param {any} [options.context=this] Bind the listener with a specific context
   *
   * @throws {TypeError} First argument must be a Function
   * @throws {Error} Listener already exists
   *
   * @returns {this}
   */
  add (cb, options = { priority: 0, once: false, context: this }) {
    const listener = {
      context: this, // If options object is defined but no context precised
      ...options,
      function: cb
    }

    if (typeof listener.function !== 'function') {
      throw new TypeError('Signal.add() : First argument must be a Function')
    }

    if (this._getListernerIndex(listener.function, listener.context) !== -1) {
      throw new Error('Signal.add() : Listener already exists')
    }

    this._listeners.push(listener)

    // Sort listeners function by priority
    this._listeners.sort((a, b) => a.priority < b.priority)

    return this
  }

  /**
   * Add a listener once
   *
   * @param {listenerCallback} cb Callback
   * @param {Object} [options] Options
   * @param {string} [options.priority=0] Higher priority listener will be called earlier
   * @param {any} [options.context=this] Bind the listener with a specific context
   *
   * @returns {this}
   */
  once (cb, options = {}) {
    return this.add(cb, {
      ...options,
      once: true
    })
  }

  /**
   * Remove a listener
   *
   * @param {listenerCallback} cb Callback
   * @param {any} [context=this] Context specified when the listener was added
   *
   * @returns {this}
   *
   * @throws {TypeError} First argument must be a Function
   */
  remove (cb, context = this) {
    if (typeof cb !== 'function') {
      throw new TypeError('Signal.remove() : First argument must be a Function')
    }

    const listenerId = this._getListernerIndex(cb, context)

    if (listenerId !== -1) {
      this._listeners.splice(listenerId, 1)
    }

    return this
  }

  /**
   * Remove all listeners
   *
   * @returns {this}
   */
  removeAll () {
    this._listeners = []

    return this
  }

  /**
   * Dispatch a signal
   *
   * @param {...any} [args] Arguments to dispatch
   *
   * @returns {this}
   */
  dispatch (...args) {
    this._dispatchNb++

    for (let i = 0; i < this._listeners.length; i++) {
      const listener = this._listeners[i]

      if (listener.once) {
        this._listeners.splice(i)
      }

      const propagation = listener.function.call(listener.context, ...args)

      if (propagation === false) {
        break
      }
    }

    return this
  }

  /**
   * Check if a listener exists
   *
   * @param {listenerCallback} cb Callback
   * @param {any} [context=this] Context specified when the listener was added
   *
   * @returns {boolean} True if the listener exists, false otherwise
   */
  has (cb, context = this) {
    return (this._getListernerIndex(cb, context) !== -1)
  }

  /**
   * Get the number of listeners
   *
   * @returns {number} Listeners number
   */
  getListenersNb () {
    return this._listeners.length
  }

  /**
   * Get the number of dispatch
   *
   * @returns {number} Dispatch number
   */
  getDispatchNb () {
    return this._dispatchNb
  }

  /**
   * Get listener index
   *
   * @private
   *
   * @param {listenerCallback} cb Callback
   * @param {any} [context=this] Context
   *
   * @returns {number} Listener index, default to -1 if the listener is not found
   */
  _getListernerIndex (cb, context) {
    for (let i = 0, listenersLength = this._listeners.length; i < listenersLength; i++) {
      if (this._listeners[i].function === cb && this._listeners[i].context === context) {
        return i
      }
    }

    return -1
  }
}

/**
 * Callback called when a signal is dispatched
 *
 * @callback Signal~listenerCallback
 *
 * @param {...args} args Arguments dispatched
 */

export default Signal