events.js

/**
 * @module events
 * @description "Re-furbished" Backbone.Events
**/
'use strict';

const {assign} = Object;
const eventSplitter = /\s+/;

let idCounter = 0;
let uniqueId = prefix => (prefix + ++idCounter + '');
let once = function(fn) {
    let memo;
    let times = 2;
    return function() {
        if (--times > 0) {
            memo = fn.apply(this, arguments);
        }
        if (times <= 1) {fn = null;}
        return memo;
    };
};
let property = key => obj => (obj === null ? void 0 : obj[key]);
let isArrayLike = collection => {
    let length = property('length')(collection);
    return typeof length === 'number' && length >= 0 && length <= Number.MAX_SAFE_INTEGER;
};
let isObject = obj => {
    let type = typeof obj;
    return type === 'function' || type === 'object' && !!obj;
};
let isEmpty = obj => {
    if (obj === null) {return true;}
    if (isArrayLike(obj) && (Array.isArray(obj) || (typeof obj === 'string'))) {return obj.length === 0;}
    return Object.keys(obj).length === 0;
};
let tryCatchOn = (obj, name, callback, context) => {
    try {
        obj.on(name, callback, context);
    } catch (e) {
        return e;
    }
};

let _listening;
let onApi = (events, name, callback, options) => {
    if (callback) {
        let handlers = events[name] || (events[name] = []);
        let {context, ctx, listening} = options;
        ctx = context || ctx;
        if (listening) {listening.count++;}
        handlers.push({callback, context, ctx, listening});
    }
    return events;
};
let offApi = (events, name, callback, options) => {
    if (!events) {return;}
    let {context, listeners} = options;
    let names;
    if (!name && !context && !callback) {
        names = isObject(listeners) ? Object.keys(listeners) : [];
        names.forEach(name => (listeners[name].cleanup()));
        return;
    }
    names = name ? [name] : Object.keys(events);
    names.forEach(name => {
        let handlers = events[name];
        if (handlers) {
            let remaining = [];
            handlers.forEach(handler => {
                if (callback && callback !== handler.callback && callback !== handler.callback._callback || context && context !== handler.context) {
                    remaining.push(handler);
                } else {
                    let listening = handler.listening;
                    if (listening) {listening.off(name, callback);}
                }
            });
            if (remaining.length) {
                events[name] = remaining;
            } else {
                delete events[name];
            }
        }
    });
    return events;
};
let onceMap = function(map, name, callback, offer) {
    if (callback) {
        let _once = map[name] = once(function() {
            offer(name, _once);
            callback.apply(this, arguments);
        });
        _once._callback = callback;
    }
    return map;
};
let eventsApi = (iteratee, events, name, callback, opts) => {
    let isEventMap = (name && typeof name === 'object');
    if (isEventMap) {
        let hasContextNoCallback = (callback !== void 0 && 'context' in opts && opts.context === void 0);
        if (hasContextNoCallback) {
            opts.context = callback;
        }
        Object.keys(name).forEach(val => {
            events = eventsApi(iteratee, events, val, name[val], opts);
        });
    } else if (name && eventSplitter.test(name)) {// space-separated event names by delegating them individually
        name.split(eventSplitter).forEach(name => {
            events = iteratee(events, name, callback, opts);
        });
    } else {// standard events
        events = iteratee(events, name, callback, opts);
    }
    return events;
};
/* eslint-disable complexity */
let triggerEvents = (events, args) => {
    let ev;
    let i = -1;
    let l = events.length;
    let [a1, a2, a3] = args;
    switch (args.length) {
        case 0: while (++i < l) {(ev = events[i]).callback.call(ev.ctx);} break;
        case 1: while (++i < l) {(ev = events[i]).callback.call(ev.ctx, a1);} break;
        case 2: while (++i < l) {(ev = events[i]).callback.call(ev.ctx, a1, a2);} break;
        case 3: while (++i < l) {(ev = events[i]).callback.call(ev.ctx, a1, a2, a3);} break;
        default: while (++i < l) {(ev = events[i]).callback.apply(ev.ctx, args);}
    }
};
/* eslint-enable complexity */
let triggerApi = (objEvents, name, callback, args) => {
    if (objEvents) {
        let events = objEvents[name];
        let allEvents = objEvents.all;
        if (events && allEvents) {allEvents = allEvents.slice();}
        if (events) {triggerEvents(events, args);}
        if (allEvents) {triggerEvents(allEvents, [name].concat(args));}
    }
    return objEvents;
};

let Events = {
    on: function(name, callback, context) {
        this._events = eventsApi(onApi, this._events || {}, name, callback, {
            context: context,
            ctx: this,
            listening: _listening
        });
        if (_listening) {
            let listeners = this._listeners || (this._listeners = {});
            listeners[_listening.id] = _listening;
            _listening.interop = false;
        }
        return this;
    },
    off: function(name, callback, context) {
        if (!this._events) {return this;}
        this._events = eventsApi(offApi, this._events, name, callback, {
            context: context,
            listeners: this._listeners
        });
        return this;
    },
    once: function(name, callback, context) {
        let events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
        if (typeof name === 'string' && context === null) {callback = void 0;}
        return this.on(events, callback, context);
    },
    trigger: function(name) {
        if (this._events) {
            let length = Math.max(0, arguments.length - 1);
            let args = Array(length);
            for (var i = 0; i < length; i++) {args[i] = arguments[i + 1];}
            eventsApi(triggerApi, this._events, name, void 0, args);
        }
        return this;
    },
    /* eslint-disable complexity */
    listenTo: function(obj, name, callback) {
        if (obj) {
            let id = obj._listenId || (obj._listenId = uniqueId('l'));
            let listeningTo = this._listeningTo || (this._listeningTo = {});
            let listening = _listening = listeningTo[id];
            if (!listening) {
                this._listenId || (this._listenId = uniqueId('l'));
                listening = _listening = listeningTo[id] = new Listening(this, obj);
            }
            let error = tryCatchOn(obj, name, callback, this);
            _listening = void 0;
            if (error) {throw error;}
            // If the target obj is not Backbone.Events, track events manually.
            if (listening.interop) {listening.on(name, callback);}
        }
        return this;
    },
    /* eslint-enable complexity */
    listenToOnce: function(obj, name, callback) {
        let events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
        return this.listenTo(obj, events);
    },
    stopListening: function(obj, name, callback) {
        let listeningTo = this._listeningTo;
        if (listeningTo) {
            let ids = obj ? [obj._listenId] : Object.keys(listeningTo);
            ids.forEach(id => {
                let listening = listeningTo[id];
                if (listening) {
                    listening.obj.off(name, callback, this);
                    if (listening.interop) {listening.off(name, callback);}
                }
            });
            if (isEmpty(listeningTo)) {this._listeningTo = void 0;}
        }
        return this;
    }
};
function Listening(listener, obj) {
    this.id = listener._listenId;
    this.listener = listener;
    this.obj = obj;
    this.interop = true;
    this.count = 0;
    this._events = void 0;
}
assign(Listening.prototype, {
    on: Events.on,
    off: function(name, callback) {
        let cleanup;
        if (this.interop) {
            this._events = eventsApi(offApi, this._events, name, callback, {
                context: void 0,
                listeners: void 0
            });
            cleanup = !this._events;
        } else {
            this.count--;
            cleanup = this.count === 0;
        }
        if (cleanup) {this.cleanup();}
    },
    cleanup: function() {
        delete this.listener._listeningTo[this.obj._listenId];
        if (!this.interop) {delete this.obj._listeners[this.id];}
    }
});

module.exports = Events;