Source: Evented.js

// Licensed Materials - Property of IBM
//
// IBM Watson Analytics
//
// (C) Copyright IBM Corp. 2015
//
// US Government Users Restricted Rights - Use, duplication or
// disclosure restricted by GSA ADP Schedule Contract with IBM Corp.

module.exports = ( function(
        Error,
        ObjectPolyfill,
        decl,
        Destroyable,
        EventArgs,
        WeakMap
    )
{
"use strict";

/*global console*/

var DEFAULT_ARGS = decl.freeze( new EventArgs() );
// listenersRef is used by Evented to register and unregister itself.
var g_listenersRef = new WeakMap();
// Using a WeakMap prevents circular references and hides the listeners from the object.
var g_eventHandles = new WeakMap();

var ensureCallable = ObjectPolyfill.ensureCallable;

/**
 * Create a new EventedHandle.
 * @class module:barejs.Evented~EventedHandle
 * @classdesc EventedHandle is the handle returned by {@link module:barejs.Evented#on Evented::on} and  {@link module:barejs.Evented#once Evented::once}.
 */
function EventedHandle( _listeners, _eventName, _listener )
{
    Destroyable.call( this );

    g_listenersRef.set( this, _listeners );
    decl.defineProperties( this,
    {
        eventName: { enumerable: true, value: _eventName },
        listener: { configurable: true, enumerable: true, value: _listener },
        _attached: { writable: true, value: false }
    } );

    this.attach();
}

decl.declareClass( EventedHandle, Destroyable,
/** @lends module:barejs.Evented~EventedHandle# */
{
    eventName: null,
    listener: null,
    _attached: null,

    /**
     * Destroy the handle.
     */
    destroy: function destroy()
    {
        this.remove();

        delete this.listener;
        g_listenersRef["delete"]( this );

        Destroyable.prototype.destroy.call( this );
    },

    /**
     * Remove the handle. A removed handle can be re-attached using {@link module:barejs.Evented~EventedHandle#attach attach}.
     * @returns {boolean} True if the handle got detached.
     */
    remove: function remove()
    {
        var listeners;
        if ( this._attached && this.listener && ( listeners = g_listenersRef.get( this ) ) )
        {
            // Look up event listeners backwards, since destroy iterates backwards
            var idx = listeners.lastIndexOf( this );

            // If the last listener is removed, use pop, since it doesn't return an Array.
            if ( idx >= ( listeners.length - 1 ) )
                listeners.pop();
            /*istanbul ignore else: this is purely a sanity check, else path should never occur*/
            else if ( idx >= 0 )
                listeners.splice( idx, 1 );


            this._attached = false;
            return true;
        }

        return false;
    },

    /**
     * Attach the handle. A handle is attached by default, this method should only be used after the handle has been detached
     * using {@link module:barejs.Evented~EventedHandle#remove remove}.
     * @returns {boolean} True if the handle is attached.
     */
    attach: function attach()
    {
        var listeners;
        if ( !this._attached && this.listener && ( listeners = g_listenersRef.get( this ) ) )
        {
            listeners.push( this );
            this._attached = true;
        }

        return this._attached;
    },

    /**
     * Check if the handle is attached.
     * @returns {boolean} True if the handle is attached.
     */
    isAttached: function isAttached()
    {
        return this._attached;
    }
} );

/**
 * The evented constructor will invoke the Destroyable constructor to ensure the object is initialized correctly.
 * @class module:barejs.Evented
 * @extends module:barejs.Destroyable
 * @classdesc Evented is a base class that adds Eventing to JavaScript Objects.
 * Extends Destroyable to automatically remove listeners if the object is destroyed.
 * If handles given to the listener are {@link module:barejs.Destroyable#own own}ed (or manually destroyed at the appropiate time),
 * the event link between Evented and its listener will be removed as soon as either party is destroyed.
 */
function Evented()
{
    Destroyable.call( this );
}

decl.preventCast( Evented );

return decl.declareClass( Evented, Destroyable,
/** @lends module:barejs.Evented# */
{
    /**
     * Destroy the Evented object. This will clean up the object, removing any links to listeners.
     */
    destroy: function destroy()
    {
        var handles = g_eventHandles.get( this ) || null;

        if ( handles )
        {
            g_eventHandles["delete"]( this );

            // Note: handles remove themselves from the array, so iterate backwards.
            // Do NOT use Destroyable.destroyAll for this!
            for ( var i = handles.length - 1; i >= 0; --i )
                handles[i].destroy();
        }

        Destroyable.prototype.destroy.call( this );
    },

    /**
     * Listen to an event. To stop listening, call destroy the returned handle. Listeners should {@link module:barejs.Destroyable#own own}
     * the handle returned by this method, so it is automatically disconnected when the listener is destroyed.
     * @param {string} _eventName The event to listen to.
     * @param {module:barejs.Evented~EventListener} _listener The callback to call if the event occurs.
     * @returns {module:barejs.Evented~EventedHandle}
     */
    on: function on( _eventName, _listener )
    {
        var handlers;

        // Note: inline check to see _listener is callable is performed first.
        if ( Destroyable.isDestroyed( ensureCallable( _listener, this )) )
            throw new Error( "The target object has been destroyed, cannot attach an event listener to it" );

        if ( !( ( "on" + _eventName ) in this ) )
            throw new Error( "The target object does not have a(n) " + _eventName + " event" );

        if ( !( handlers = g_eventHandles.get( this ) ) )
            g_eventHandles.set( this, handlers = [] );

        // No need to own the handle; handles are automatically destroyed
        return new EventedHandle( handlers, _eventName, _listener );
    },

    /**
     * Listen for an event, automatically detaching after one invocation of the listener.
     * @param {string} _eventName The event to listen to.
     * @param {module:barejs.Evented~EventListener} _listener The callback to call (once) if the event occurs.
     * @returns {module:barejs.Evented~EventedHandle}
     */
    once: function once( _eventName, _listener )
    {
        // Inline validation of _listener
        var handle = ensureCallable( _listener, null );

        return ( handle = this.on( _eventName, function()
        {
            /*istanbul ignore else: this is purely a sanity check, else path should never occur*/
            if ( handle )
            {
                handle.destroy();
                handle = null;
            }

            // Forward call
            return _listener.apply( this, arguments );
        } ) );
    },

    /**
     * Emit the event with _eventName.
     * @param {string} _eventName The name of the event to emit.
     * @param {module:barejs.EventArgs} _eventArgs the event args to emit.
     * @returns {module:barejs.EventArgs} _eventArgs (or null if not specified).
     */
    emit: function emit( _eventName, _eventArgs )
    {
        if ( Destroyable.isDestroyed( this ) )
            throw new Error( "The target object has been destroyed, cannot emit an event from it" );

        var args = _eventArgs || DEFAULT_ARGS,
            evtDef = "on" + _eventName,
            handles;

        if ( !( evtDef in this ) )
            throw new Error( "The " + _eventName + " event being emitted is not defined. Is this class missing an " + evtDef + " event definition?" );
        if ( !__RELEASE__ && !( args instanceof EventArgs ) && ( typeof console !== "undefined" ) )
            console.warn( ( this.constructor.name || "Evented" ) + " is emitting the \"" + _eventName + "\" event with arguments that are not EventArgs" );

        // First, call the local method (event definition)
        this[evtDef]( args, this );

        // If there are no handles, there are no listeners, so bail out
        if ( ( handles = g_eventHandles.get( this ) ) && ( handles.length > 0 ) )
        {
            // Get the listeners for this event; using filter gives us a temporary array which is protected
            // from the handles array being modified as a side effect of handler execution.
            var listeners = handles.filter( function( _handle )
            {
                return _handle.eventName === _eventName;
            } );

            // Invoke the event handlers
            for ( var idx = 0, len = listeners.length, handle; idx < len; idx++ )
            {
                // It is possible a handle got removed as a side effect of a previous handler; if so, ignore it.
                if ( ( handle = listeners[idx] )._attached && handle.listener )
                    handle.listener( args, this );
            }
        }

        return _eventArgs || null;
    },

    /**
     * Check if there is at least one listener for _eventName. It is highly recommended to just
     * emit an event instead of checking if there are listeners. This method is provided for edge
     * cases where building the event metadata is an expensive process, which should be avoided if
     * there are no listeners.
     * @param {string} _eventName The name of the event to check.
     * @returns {Boolean} True if there is at least one listener, false otherwise.
     */
    hasListener: function( _eventName )
    {
        var handles = g_eventHandles.get( this );

        return ( !!handles ) && handles.some( function( _handle )
        {
            return _handle.eventName === _eventName;
        } );
    }
} );

/**
 * Event listeners are called with two arguments; the EventArgs and the sender.
 * @callback module:barejs.Evented~EventListener
 * @param {module:barejs.EventArgs} _eventArgs The EventArgs to the event.
 * @param {module:barejs.Evented} _sender The Evented object that emitted the event.
 */

// End of module
}(
    Error,
    require( "./polyfill/Object" ),
    require( "./decl" ),
    require( "./Destroyable" ),
    require( "./EventArgs" ),
    require( "./WeakMap" )
) );