EventHandlerList.js

import recycle from 'recycle';
import EventHandler from './EventHandler';
import { arrayCache, greenSlice, greenSplice } from './utils/array';

/**
 * Maintains an ordered list of event handlers for a single event type.
 *
 * EventHandlerList supports:
 * - listener priorities
 * - deterministic insertion ordering
 * - safe mutation during dispatch
 * - one-time listeners
 * - recyclable pooled handlers
 *
 * Handlers are lazily sorted on first dispatch after modification.
 *
 * @memberof platypus
 * @class EventHandlerList
 */
const EventHandlerList = function () {
    /**
     * Whether the handler list requires resorting.
     *
     * @property dirty
     * @type Boolean
     * @default false
     */
    this.dirty = false;

    /**
     * The ordered list of EventHandler objects.
     *
     * @property handlers
     * @type Array
     */
    this.handlers = arrayCache.setUp();

    /**
     * Incrementing insertion index used to preserve
     * deterministic ordering between equal priorities.
     *
     * @property orderIndex
     * @type Number
     * @default 0
     */
    this.orderIndex = 0;
};

const { prototype } = EventHandlerList;

/**
 * Adds a new handler to the list.
 *
 * Handlers are not immediately sorted. Sorting occurs lazily
 * during the next dispatch.
 *
 * @method add
 * @param callback {Function}
 * The listener callback function.
 *
 * @param [context=null] {Object}
 * The callback execution context.
 *
 * @param [priority=0] {Number}
 * Lower numbers execute first.
 *
 * @return {platypus.EventHandler}
 * Returns the created EventHandler instance.
 */
prototype.add = function (callback, context, once, priority) {
    for (let i = 0; i < this.handlers.length; i++) {
        const existing = this.handlers[i];

        if (
            existing.callback === callback &&
            existing.context === context
        ) {
            return null;
        }
    }
    
    const
        eventHandler = EventHandler.setUp(
            callback,
            context,
            once,
            priority,
            this.orderIndex++
        );

    this.handlers.push(eventHandler);
    this.dirty = true;

    return eventHandler;
};

/**
 * Removes all handlers matching the supplied callback.
 *
 * @method remove
 * @param callback {Function}
 * @param context {Object}
 * The callback function to remove.
 */
prototype.remove = function (callback, context) {
    const
        {handlers} = this;
    let i = handlers.length;

    while (i--) {
        const
            handler = handlers[i];

        if (
            handler.callback === callback &&
            (
                typeof context === 'undefined' ||
                handler.context === context
            )
        ) {
            handler.recycle();
            greenSplice(handlers, i);
        }
    }
};

/**
 * Dispatches the event to all registered handlers.
 *
 * The handler list is shallow-copied before iteration
 * so listeners may safely remove themselves during dispatch.
 *
 * @method trigger
 * @param [args] {Array}
 * The arguments passed to listener callbacks.
 *
 * @return {Number}
 * Returns the number of handlers triggered.
 */
prototype.trigger = function (args) {
    if (this.dirty) {
        this.handlers.sort((a, b) => a.sortPriority(b));
        this.dirty = false;
    }

    const
        handlers = greenSlice(this.handlers),
        {length} = handlers;

    // This prevents event handlers from getting recycled in the handlers copy.
    for (let i = 0; i < length; i++) {
        handlers[i].recycleHold();
    }

    for (let i = 0; i < length; i++) {
        const
            {callback, context, once} = handlers[i];

        if (once) {
            this.remove(callback);
        }

        callback.apply(context, args);
    }

    for (let i = 0; i < length; i++) {
        handlers[i].recycleRelease();
    }

    arrayCache.recycle(handlers);

    return length;
};

/**
 * Returns EventHandlerList from cache or creates a new one.
 *
 * @method platypus.EventHandlerList.setUp
 * @return {platypus.EventHandlerList}
 */

/**
 * Returns EventHandlerList back to cache. If the instance has
 * outstanding holds, the recycle is deferred until all holds
 * are released.
 *
 * Prefer calling `eventHandlerList.recycle()`
 * instead of direct pool access.
 *
 * @method platypus.EventHandlerList.recycle
 * @param {platypus.EventHandlerList} eventHandlerList
 */

/**
 * Increments the hold count on the instance, preventing recycling
 * until all holds are released. Returns the instance for chaining.
 *
 * @method platypus.EventHandlerList.recycleHold
 * @param {platypus.EventHandlerList} eventHandlerList
 * @return {platypus.EventHandlerList}
 */

/**
 * Decrements the hold count. When it reaches zero, any pending
 * recycle is flushed. Returns the instance for chaining.
 *
 * @method platypus.EventHandlerList.recycleRelease
 * @param {platypus.EventHandlerList} eventHandlerList
 * @return {platypus.EventHandlerList}
 */

/**
 * Clears internal references and recycles the instance. If the
 * instance has outstanding holds, the recycle is deferred until
 * all holds are released.
 *
 * @method platypus.EventHandlerList#recycle
 */

/**
 * Increments the hold count on the instance, preventing recycling
 * until all holds are released. Returns the instance for chaining.
 *
 * @method platypus.EventHandlerList#recycleHold
 * @return {platypus.EventHandlerList}
 */

/**
 * Decrements the hold count. When it reaches zero, any pending
 * recycle is flushed. Returns the instance for chaining.
 *
 * @method platypus.EventHandlerList#recycleRelease
 * @return {platypus.EventHandlerList}
 */
recycle.add(
    EventHandlerList,
    'EventHandlerList',
    EventHandlerList,
    function () {
        this.handlers.forEach((handler) => handler.recycle());

        arrayCache.recycle(this.handlers);

        this.handlers = null;
        this.dirty = false;
        this.orderIndex = 0;
    },
    true
);

export default EventHandlerList;