// 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" )
) );