Source: functions/eventful.js


function addEventFunction(target, functionName, events, secret)
{
  var on = secret ? '$on' : 'on';
  var off = secret ? '$off' : 'off';

  addMethod( target, functionName, function(callback, context)
  {
    var subject = this;
    var unlistened = false;

    function listener()
    {
      var result = callback.apply( context || subject, arguments );

      if ( result === false )
      {
        unlistener();
      }
    }

    function unlistener()
    {
      if ( !unlistened )
      {
        subject[ off ]( events, listener );
        unlistened = true;
      }
    }

    subject[ on ]( events, listener );

    return unlistener;
  });
}

/**
 * Adds functions to the given object (or prototype) so you can listen for any
 * number of events on the given object, optionally once. Listeners can be
 * removed later.
 *
 * The following methods will be added to the given target:
 *
 * ```
 * target.on( events, callback, [context] )
 * target.once( events, callback, [context] )
 * target.after( events, callback, [context] )
 * target.off( events, callback )
 * target.trigger( events, [a, b, c...] )
 * ```
 *
 * Where...
 * - `events` is a string of space delimited events.
 * - `callback` is a function to invoke when the event is triggered.
 * - `context` is an object that should be the `this` when the callback is
 *   invoked. If no context is given the default value is the object which has
 *   the trigger function that was invoked.
 *
 * @memberof Rekord
 * @param {Object} [target] -
 *    The object to add `on`, `once`, `off`, and `trigger` functions to.
 * @param {Boolean} [secret=false] -
 *    If true - the functions will be prefixed with `$`.
 */
function addEventful(target, secret)
{

  var CALLBACK_FUNCTION = 0;
  var CALLBACK_CONTEXT = 1;
  var CALLBACK_GROUP = 2;

  var triggerId = 0;

  /**
   * A mixin which adds `on`, `once`, `after`, and `trigger` functions to
   * another object.
   *
   * @class Eventful
   * @memberof Rekord
   * @see Rekord.addEventful
   */

   /**
    * A mixin which adds `$on`, `$once`, `$after`, and `$trigger` functions to
    * another object.
    *
    * @class Eventful$
    * @memberof Rekord
    * @see Rekord.addEventful
    */

  // Adds a listener to $this
  function onListeners($this, property, eventsInput, callback, context)
  {
    if ( !isFunction( callback ) )
    {
      return noop;
    }

    var events = toArray( eventsInput, ' ' );
    var listeners = $this[ property ];

    if ( !listeners )
    {
      listeners = $this[ property ] = {};
    }

    for (var i = 0; i < events.length; i++)
    {
      var eventName = events[ i ];
      var eventListeners = listeners[ eventName ];

      if ( !eventListeners )
      {
        eventListeners = listeners[ eventName ] = [];
      }

      eventListeners.push( [ callback, context || $this, 0 ] );
    }

    return function ignore()
    {
      for (var i = 0; i < events.length; i++)
      {
        offListeners( listeners, events[ i ], callback );
      }
    };
  }

  /**
   * Listens for every occurrence of the given events and invokes the callback
   * each time any of them are triggered.
   *
   * @method on
   * @memberof Rekord.Eventful#
   * @param {String|Array} events -
   *    The event or events to listen to.
   * @param {Function} callback -
   *    The function to invoke when any of the events are invoked.
   * @param {Object} [context] -
   *    The value of `this` when the callback is invoked. If not specified, the
   *    reference of the object this function exists on will be `this`.
   * @return {Function} -
   *    A function to invoke to stop listening to all of the events given.
   */

  /**
   * Listens for every occurrence of the given events and invokes the callback
   * each time any of them are triggered.
   *
   * @method $on
   * @memberof Rekord.Eventful$#
   * @param {String|Array} events -
   *    The event or events to listen to.
   * @param {Function} callback -
   *    The function to invoke when any of the events are invoked.
   * @param {Object} [context] -
   *    The value of `this` when the callback is invoked. If not specified, the
   *    reference of the object this function exists on will be `this`.
   * @return {Function} -
   *    A function to invoke to stop listening to all of the events given.
   */

  function on(events, callback, context)
  {
    return onListeners( this, '$$on', events, callback, context );
  }

  /**
   * Listens for the first of the given events to be triggered and invokes the
   * callback once.
   *
   * @method once
   * @memberof Rekord.Eventful#
   * @param {String|Array} events -
   *    The event or events to listen to.
   * @param {Function} callback -
   *    The function to invoke when any of the events are invoked.
   * @param {Object} [context] -
   *    The value of `this` when the callback is invoked. If not specified, the
   *    reference of the object this function exists on will be `this`.
   * @return {Function} -
   *    A function to invoke to stop listening to all of the events given.
   */

  /**
   * Listens for the first of the given events to be triggered and invokes the
   * callback once.
   *
   * @method $once
   * @memberof Rekord.Eventful$#
   * @param {String|Array} events -
   *    The event or events to listen to.
   * @param {Function} callback -
   *    The function to invoke when any of the events are invoked.
   * @param {Object} [context] -
   *    The value of `this` when the callback is invoked. If not specified, the
   *    reference of the object this function exists on will be `this`.
   * @return {Function} -
   *    A function to invoke to stop listening to all of the events given.
   */

  function once(events, callback, context)
  {
    return onListeners( this, '$$once', events, callback, context );
  }

  function after(events, callback, context)
  {
    return onListeners( this, '$$after', events, callback, context );
  }

  // Removes a listener from an array of listeners.
  function offListeners(listeners, event, callback)
  {
    if (listeners && event in listeners)
    {
      var eventListeners = listeners[ event ];

      for (var k = eventListeners.length - 1; k >= 0; k--)
      {
        if (eventListeners[ k ][ CALLBACK_FUNCTION ] === callback)
        {
          eventListeners.splice( k, 1 );
        }
      }
    }
  }

  // Deletes a property from the given object if it exists
  function deleteProperty(obj, prop)
  {
    if ( obj && prop in obj )
    {
      delete obj[ prop ];
    }
  }

  /**
   * Stops listening for a given callback for a given set of events.
   *
   * **Examples:**
   *
   *     target.off();           // remove all listeners
   *     target.off('a b');      // remove all listeners on events a & b
   *     target.off(['a', 'b']); // remove all listeners on events a & b
   *     target.off('a', x);     // remove listener x from event a
   *
   * @method off
   * @for addEventful
   * @param {String|Array|Object} [eventsInput]
   * @param {Function} [callback]
   * @chainable
   */
  function off(eventsInput, callback)
  {
    // Remove ALL listeners
    if ( !isDefined( eventsInput ) )
    {
      deleteProperty( this, '$$on' );
      deleteProperty( this, '$$once' );
      deleteProperty( this, '$$after' );
    }
    else
    {
      var events = toArray( eventsInput, ' ' );

      // Remove listeners for given events
      if ( !isFunction( callback ) )
      {
        for (var i = 0; i < events.length; i++)
        {
          deleteProperty( this.$$on, events[i] );
          deleteProperty( this.$$once, events[i] );
          deleteProperty( this.$$after, events[i] );
        }
      }
      // Remove specific listener
      else
      {
        for (var i = 0; i < events.length; i++)
        {
          offListeners( this.$$on, events[i], callback );
          offListeners( this.$$once, events[i], callback );
          offListeners( this.$$after, events[i], callback );
        }
      }
    }

    return this;
  }

  // Triggers listeneers for the given event
  function triggerListeners(listeners, event, args, clear)
  {
    if (listeners && event in listeners)
    {
      var eventListeners = listeners[ event ];
      var triggerGroup = ++triggerId;

      for (var i = 0; i < eventListeners.length; i++)
      {
        var callback = eventListeners[ i ];

        if ( callback )
        {
          if ( callback[ CALLBACK_GROUP ] !== triggerGroup )
          {
            callback[ CALLBACK_GROUP ] = triggerGroup;
            callback[ CALLBACK_FUNCTION ].apply( callback[ CALLBACK_CONTEXT ], args );

            if ( callback !== eventListeners[ i ] )
            {
              i = -1;
            }
          }
        }
      }

      if ( clear )
      {
        delete listeners[ event ];
      }
    }
  }

  /**
   * Triggers a single event optionally passing an argument to any listeners.
   *
   * @method trigger
   * @for addEventful
   * @param {String} eventsInput
   * @param {Array} args
   * @chainable
   */
  function trigger(eventsInput, args)
  {
    var events = toArray( eventsInput, ' ' );

    for (var i = 0; i < events.length; i++)
    {
      var e = events[ i ];

      triggerListeners( this.$$on, e, args, false );
      triggerListeners( this.$$once, e, args, true );
      triggerListeners( this.$$after, e, args, false );
    }

    return this;
  }

  if ( secret )
  {
    addMethod( target, '$on', on );
    addMethod( target, '$once', once );
    addMethod( target, '$after', after );
    addMethod( target, '$off', off );
    addMethod( target, '$trigger', trigger );
  }
  else
  {
    addMethod( target, 'on', on );
    addMethod( target, 'once', once );
    addMethod( target, 'after', after );
    addMethod( target, 'off', off );
    addMethod( target, 'trigger', trigger );
  }
}