StructureJS

0.15.2

A class based utility library for building modular and scalable web platform applications. Features opt-in classes and utilities which provide a solid foundation and toolset to build your next project.

File: ts/event/EventDispatcher.ts

import ObjectManager from '../ObjectManager';
import BaseEvent from './BaseEvent';
import Util from '../util/Util';

/**
 * EventDispatcher is the base class for all classes that dispatch events. It is the base class for the {{#crossLink "DisplayObjectContainer"}}{{/crossLink}} class.
 * EventDispatcher provides methods for managing prioritized queues of event listeners and dispatching events.
 *
 * @class EventDispatcher
 * @extends ObjectManager
 * @module StructureJS
 * @submodule event
 * @requires Extend
 * @requires ObjectManager
 * @requires BaseEvent
 * @constructor
 * @author Robert S. (www.codeBelt.com)
 * @example
 *      // Another way to use the EventDispatcher.
 *      let eventDispatcher = new EventDispatcher();
 *      eventDispatcher.addEventListener('change', this._handlerMethod, this);
 *      eventDispatcher.dispatchEvent('change');
 */
class EventDispatcher extends ObjectManager
{
    /**
     * Holds a reference to added listeners.
     *
     * @property _listeners
     * @type {any}
     * @protected
     */
    protected _listeners:any = {};

    /**
     * Indicates the object that contains a child object. Uses the parent property
     * to specify a relative path to display objects that are above the current display object in the display
     * list hierarchy and helps facilitate event bubbling.
     *
     * @property parent
     * @type {any}
     * @public
     */
    public parent:any = null;

    constructor()
    {
        super();
    }

    /**
     * Registers an event listener object with an EventDispatcher object so the listener receives notification of an event.
     *
     * @method addEventListener
     * @param type {String} The type of event.
     * @param callback {Function} The listener function that processes the event. This function must accept an Event object as its only parameter and must return nothing, as this example shows. @example function(event:Event):void
     * @param scope {any} Binds the scope to a particular object (scope is basically what "this" refers to in your function). This can be very useful in JavaScript because scope isn't generally maintained.
     * @param [priority=0] {int} Influences the order in which the listeners are called. Listeners with lower priorities are called after ones with higher priorities.
     * @public
     * @chainable
     * @example
     *      this.addEventListener(BaseEvent.CHANGE, this._handlerMethod, this);
     *
     *      _handlerMethod(event) {
     *          console.log(event.target + " sent the event.");
     *          console.log(event.type, event.data);
     *      }
     */
    public addEventListener(type:string, callback:Function, scope:any, priority:number = 0):EventDispatcher
    {
        // Get the list of event listeners by the associated type value that is passed in.
        let list = this._listeners[type];
        if (list == null)
        {
            // If a list of event listeners do not exist for the type value passed in then create a new empty array.
            this._listeners[type] = list = [];
        }
        let index:number = 0;
        let listener;
        let i:number = list.length;
        while (--i > -1)
        {
            listener = list[i];
            if (listener.callback === callback && listener.scope === scope)
            {
                // If the same callback and scope are found then remove it and add the current one below.
                list.splice(i, 1);
            }
            else if (index === 0 && listener.priority < priority)
            {
                index = i + 1;
            }
        }
        // Add the event listener to the list array at the index value.
        list.splice(index, 0, {callback: callback, scope: scope, priority: priority, once: false});

        return this;
    }

    /**
     * Registers an event listener object once with an EventDispatcher object so the listener will receive the notification of an event.
     *
     * @method addEventListenerOnce
     * @param type {String} The type of event.
     * @param callback {Function} The listener function that processes the event. This function must accept an Event object as its only parameter and must return nothing, as this example shows. @example function(event:Event):void
     * @param scope {any} Binds the scope to a particular object (scope is basically what "this" refers to in your function). This can be very useful in JavaScript because scope isn't generally maintained.
     * @param [priority=0] {int} Influences the order in which the listeners are called. Listeners with lower priorities are called after ones with higher priorities.
     * @public
     * @chainable
     * @example
     *      this.addEventListenerOnce(BaseEvent.CHANGE, this._handlerMethod, this);
     *
     *      _handlerMethod(event) {
     *          console.log(event.target + " sent the event.");
     *          console.log(event.type, event.data);
     *      }
     */
    public addEventListenerOnce(type:string, callback:Function, scope:any, priority:number = 0):EventDispatcher
    {
        // Add the event listener the normal way.
        this.addEventListener(type, callback, scope, priority);

        // Get the event listeners we just added.
        const list = this._listeners[type];
        const listener = list[0];

        // Change the value to true so it will be remove after dispatchEvent is called.
        listener.once = true;

        return this;
    }

    /**
     * Removes a specified listener from the EventDispatcher object.
     *
     * @method removeEventListener
     * @param type {String} The type of event.
     * @param callback {Function} The listener object to remove.
     * @param scope {any} The scope of the listener object to be removed.
     * @hide This was added because it was needed for the {{#crossLink "EventBroker"}}{{/crossLink}} class. To keep things consistent this parameter is required.
     * @public
     * @chainable
     * @example
     *      this.removeEventListener(BaseEvent.CHANGE, this._handlerMethod, this);
     */
    public removeEventListener(type:string, callback:Function, scope:any):EventDispatcher
    {
        // Get the list of event listeners by the associated type value that is passed in.
        const list:Array<any> = this._listeners[type];
        if (list !== void 0)
        {
            let i = list.length;
            while (--i > -1)
            {
                // If the callback and scope are the same then remove the event listener.
                if (list[i].callback === callback && list[i].scope === scope)
                {
                    list.splice(i, 1);
                    break;
                }
            }
        }

        return this;
    }

    /**
     * <p>Dispatches an event into the event flow. The event target is the EventDispatcher object upon which the dispatchEvent() method is called.</p>
     *
     * @method dispatchEvent
     * @param event {string|BaseEvent} The Event object or event type string you want to dispatch. You can create custom events, the only requirement is all events must extend {{#crossLink "BaseEvent"}}{{/crossLink}}.
     * @param [data=null] {any} The optional data you want to send with the event. Do not use this parameter if you are passing in a {{#crossLink "BaseEvent"}}{{/crossLink}}.
     * @public
     * @chainable
     * @example
     *      this.dispatchEvent('change');
     *
     *      // Example: Sending data with the event:
     *      this.dispatchEvent('change', {some: 'data'});
     *
     *      // Example: With an event object
     *      // (event type, bubbling set to true, cancelable set to true and passing data) :
     *      let event = new BaseEvent(BaseEvent.CHANGE, true, true, {some: 'data'});
     *      this.dispatchEvent(event);
     *
     *      // Here is a common inline event object being dispatched:
     *      this.dispatchEvent(new BaseEvent(BaseEvent.CHANGE));
     */
    public dispatchEvent(type:any, data:any = null):EventDispatcher
    {
        let event = type;

        if (typeof event === 'string')
        {
            event = new BaseEvent(type, false, true, data);
        }

        // If target is null then set it to the object that dispatched the event.
        if (event.target == null)
        {
            event.target = this;
            event.currentTarget = this;
        }

        // Get the list of event listener by the associated type value.
        const list:Array<any> = this._listeners[event.type];
        if (list !== void 0)
        {
            // Cache to prevent the edge case were another listener is added during the dispatch loop.
            const cachedList:Array<any> = list.slice();

            let i:number = cachedList.length;
            let listener:any;

            while (--i > -1)
            {
                // If cancelable and isImmediatePropagationStopped are true then break out of the while loop.
                if (event.cancelable === true && event.isImmediatePropagationStopped === true)
                {
                    break;
                }

                listener = cachedList[i];
                listener.callback.call(listener.scope, event);

                // If the once value is true we want to remove the listener right after this callback was called.
                if (listener.once === true)
                {
                    this.removeEventListener(event.type, listener.callback, listener.scope);
                }
            }
        }

        //Dispatches up the chain of classes that have a parent.
        if (this.parent != null && event.bubbles === true)
        {
            // If cancelable and isPropagationStopped are true then don't dispatch the event on the parent object.
            if (event.cancelable === true && event.isPropagationStopped === true)
            {
                return this;
            }

            // Assign the current object that is currently processing the event (i.e. event bubbling at).
            event.currentTarget = this;

            // Pass the event to the parent (event bubbling).
            this.parent.dispatchEvent(event);
        }

        return this;
    }

    /**
     * Check if an object has a specific event listener already added.
     *
     * @method hasEventListener
     * @param type {String} The type of event.
     * @param callback {Function} The listener method to call.
     * @param scope {any} The scope of the listener object.
     * @return {boolean}
     * @public
     * @example
     *      this.hasEventListener(BaseEvent.CHANGE, this._handlerMethod, this);
     */
    public hasEventListener(type:string, callback:Function, scope:any):boolean
    {
        if (this._listeners[type] !== void 0)
        {
            let listener:any;
            const numOfCallbacks:number = this._listeners[type].length;
            for (let i:number = 0; i < numOfCallbacks; i++)
            {
                listener = this._listeners[type][i];
                if (listener.callback === callback && listener.scope === scope)
                {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Returns and array of all current event types and there current listeners.
     *
     * @method getEventListeners
     * @return {Array<any>}
     * @public
     * @example
     *      this.getEventListeners();
     */
    public getEventListeners():Array<any>
    {
        return this._listeners;
    }

    /**
     * Prints out each event listener in the console.log
     *
     * @method print
     * @return {string}
     * @public
     * @example
     *      this.printEventListeners();
     *
     *      // [ClassName] is listening for the 'BaseEvent.change' event.
     *      // [AnotherClassName] is listening for the 'BaseEvent.refresh' event.
     */
    public printEventListeners():void
    {
        let numOfCallbacks:number;
        let listener:any;

        for (let type in this._listeners)
        {
            numOfCallbacks = this._listeners[type].length;
            for (let i:number = 0; i < numOfCallbacks; i++)
            {
                listener = this._listeners[type][i];

                let name;

                if (listener.scope)
                {
                    name = '[' + Util.getName(listener.scope) + ']';
                }
                else
                {
                    name ='[Unknown]';
                }

                console.log(`${name} is listen for "${type}" event.`, listener.scope);
            }
        }
    }

    /**
     * @overridden BaseObject.destroy
     */
    public destroy():void
    {
        this.disable();

        super.destroy();
    }

}

export default EventDispatcher;