UNPKG

backbone-esnext-events

Version:

Separates 'events' support from Backbone in addition to adding TyphonJS extensions.

647 lines (566 loc) 22.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _typeof2 = require('babel-runtime/helpers/typeof'); var _typeof3 = _interopRequireDefault(_typeof2); var _keys = require('babel-runtime/core-js/object/keys'); var _keys2 = _interopRequireDefault(_keys); var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _createClass2 = require('babel-runtime/helpers/createClass'); var _createClass3 = _interopRequireDefault(_createClass2); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * backbone-esnext-events / Provides the ability to bind and trigger custom named events. * (http://backbonejs.org/#Events) * --------------- * * An important consideration of Backbone-ESNext is that Events are no longer an object literal, but a full blown ES6 * class. This is the biggest potential breaking change for Backbone-ESNext when compared to the original Backbone. * * Previously Events could be mixed in to any object. This is no longer possible with Backbone-ESNext when working from * source or the bundled versions. It should be noted that Events is also no longer mixed into Backbone itself, so * Backbone is not a Global events instance. * * Backbone-ESNext also separates Backbone into separate modules which may be used independently of Backbone itself. In * particular backbone-esnext-events is a standalone NPM module that has no other dependencies. Underscore is being * removed / minimized for Backbone-ESNext where possible. * * This class essentially implements the default Backbone events functionality and is extended by {@link TyphonEvents} * which provides additional trigger mechanisms. * * @example * One must now use ES6 extends syntax for Backbone.Events when inheriting events functionality from Backbone-ESNext: * import Backbone from 'backbone'; * * class MyClass extends Backbone.Events {} * * Or if importing this module directly use: * import Events from 'backbone-esnext-events'; * * class MyClass extends Events {} * * @example * A nice ES6 pattern for creating a named events instance is the following: * * import Backbone from 'backbone'; * * export default new Backbone.Events(); * * Or if importing this module directly use: * import Events from 'backbone-esnext-events'; * * export default new Events(); * * This module / Events instance can then be imported by full path or if consuming in a modular runtime by creating * a mapped path to it. * * backbone-esnext-events provides a default main eventbus implementation found in `src/mainEventbus.js`. */ var Events = function () { /** */ function Events() { (0, _classCallCheck3.default)(this, Events); } /** * Delegates to `on`. * * @returns {*} */ (0, _createClass3.default)(Events, [{ key: 'bind', value: function bind() { return this.on.apply(this, arguments); } /** * Tell an object to listen to a particular event on an other object. The advantage of using this form, instead of * other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can * be removed all at once later on. The callback will always be called with object as context. * * @example * view.listenTo(model, 'change', view.render); * * @see http://backbonejs.org/#Events-listenTo * * @param {object} obj - Event context * @param {string} name - Event name(s) * @param {function} callback - Event callback function * @param {object} [context] - Optional: event context * @returns {Events} */ }, { key: 'listenTo', value: function listenTo(obj, name, callback) { var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : this; if (!obj) { return this; } var id = obj._listenId || (obj._listenId = s_UNIQUE_ID('l')); var listeningTo = this._listeningTo || (this._listeningTo = {}); var listening = listeningTo[id]; // This object is not listening to any other events on `obj` yet. // Setup the necessary references to track the listening callbacks. if (!listening) { var thisId = this._listenId || (this._listenId = s_UNIQUE_ID('l')); listening = listeningTo[id] = { obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0 }; } // Bind callbacks on obj, and keep track of them on listening. s_INTERNAL_ON(obj, name, callback, context, listening); return this; } /** * Just like `listenTo`, but causes the bound callback to fire only once before being removed. * * @see http://backbonejs.org/#Events-listenToOnce * * @param {object} obj - Event context * @param {string} name - Event name(s) * @param {function} callback - Event callback function * @param {object} [context=this] - Optional: event context * @returns {Events} */ }, { key: 'listenToOnce', value: function listenToOnce(obj, name, callback) { var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : this; // Map the event into a `{event: once}` object. var events = s_EVENTS_API(s_ONCE_MAP, {}, name, callback, this.stopListening.bind(this, obj)); return this.listenTo(obj, events, void 0, context); } /** * Remove a previously-bound callback function from an object. If no context is specified, all of the versions of * the callback with different contexts will be removed. If no callback is specified, all callbacks for the event * will be removed. If no event is specified, callbacks for all events will be removed. * * Note that calling model.off(), for example, will indeed remove all events on the model — including events that * Backbone uses for internal bookkeeping. * * @example * // Removes just the `onChange` callback. * object.off("change", onChange); * * // Removes all "change" callbacks. * object.off("change"); * * // Removes the `onChange` callback for all events. * object.off(null, onChange); * * // Removes all callbacks for `context` for all events. * object.off(null, null, context); * * // Removes all callbacks on `object`. * object.off(); * * @see http://backbonejs.org/#Events-off * * @param {string} name - Event name(s) * @param {function} callback - Event callback function * @param {object} context - Event context * @returns {Events} */ }, { key: 'off', value: function off(name) { var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : void 0; var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : void 0; /* istanbul ignore if */ if (!this._events) { return this; } /** * @type {*} * @protected */ this._events = s_EVENTS_API(s_OFF_API, this._events, name, callback, { context: context, listeners: this._listeners }); return this; } /** * Bind a callback function to an object. The callback will be invoked whenever the event is fired. If you have a * large number of different events on a page, the convention is to use colons to namespace them: "poll:start", or * "change:selection". * * To supply a context value for this when the callback is invoked, pass the optional last argument: * model.on('change', this.render, this) or model.on({change: this.render}, this). * * @example * The event string may also be a space-delimited list of several events... * book.on("change:title change:author", ...); * * @example * Callbacks bound to the special "all" event will be triggered when any event occurs, and are passed the name of * the event as the first argument. For example, to proxy all events from one object to another: * proxy.on("all", function(eventName) { * object.trigger(eventName); * }); * * @example * All Backbone event methods also support an event map syntax, as an alternative to positional arguments: * book.on({ * "change:author": authorPane.update, * "change:title change:subtitle": titleView.update, * "destroy": bookView.remove * }); * * @see http://backbonejs.org/#Events-on * * @param {string} name - Event name(s) * @param {function} callback - Event callback function * @param {object} context - Event context * @returns {*} */ }, { key: 'on', value: function on(name, callback) { var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : void 0; return s_INTERNAL_ON(this, name, callback, context, void 0); } /** * Just like `on`, but causes the bound callback to fire only once before being removed. Handy for saying "the next * time that X happens, do this". When multiple events are passed in using the space separated syntax, the event * will fire once for every event you passed in, not once for a combination of all events * * @see http://backbonejs.org/#Events-once * * @param {string} name - Event name(s) * @param {function} callback - Event callback function * @param {object} context - Event context * @returns {*} */ }, { key: 'once', value: function once(name, callback) { var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : void 0; // Map the event into a `{event: once}` object. var events = s_EVENTS_API(s_ONCE_MAP, {}, name, callback, this.off.bind(this)); if (typeof name === 'string' && (context === null || typeof context === 'undefined')) { callback = void 0; } return this.on(events, callback, context); } /** * Tell an object to stop listening to events. Either call stopListening with no arguments to have the object remove * all of its registered callbacks ... or be more precise by telling it to remove just the events it's listening to * on a specific object, or a specific event, or just a specific callback. * * @example * view.stopListening(); * * view.stopListening(model); * * @see http://backbonejs.org/#Events-stopListening * * @param {object} obj - Event context * @param {string} name - Event name(s) * @param {function} callback - Event callback function * @param {object} [context=this] - Optional: event context * @returns {Events} */ }, { key: 'stopListening', value: function stopListening(obj) { var name = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : void 0; var callback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : void 0; var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : this; var listeningTo = this._listeningTo; if (!listeningTo) { return this; } var ids = obj ? [obj._listenId] : (0, _keys2.default)(listeningTo); for (var i = 0; i < ids.length; i++) { var listening = listeningTo[ids[i]]; // If listening doesn't exist, this object is not currently listening to obj. Break out early. if (!listening) { break; } listening.obj.off(name, callback, context); } return this; } /** * Trigger callbacks for the given event, or space-delimited list of events. Subsequent arguments to trigger will be * passed along to the event callbacks. * * @see http://backbonejs.org/#Events-trigger * * @param {string} name - Event name(s) * @returns {Events} */ }, { key: 'trigger', value: function trigger(name) { /* istanbul ignore if */ if (!this._events) { return this; } var length = Math.max(0, arguments.length - 1); var args = new Array(length); for (var i = 0; i < length; i++) { args[i] = arguments[i + 1]; } s_EVENTS_API(s_TRIGGER_API, this._events, name, void 0, args); return this; } /** * Delegates to `off`. * * @returns {*} */ }, { key: 'unbind', value: function unbind() { return this.off.apply(this, arguments); } }]); return Events; }(); // Private / internal methods --------------------------------------------------------------------------------------- /** * Regular expression used to split event strings. * @type {RegExp} */ exports.default = Events; var s_EVENT_SPLITTER = /\s+/; /** * Iterates over the standard `event, callback` (as well as the fancy multiple space-separated events `"change blur", * callback` and jQuery-style event maps `{event: callback}`). * * @param {function} iteratee - Event operation to invoke. * @param {Object.<{callback: function, context: object, ctx: object, listening:{}}>} events - Events object * @param {string|object} name - A single event name, compound event names, or a hash of event names. * @param {function} callback - Event callback function * @param {object} opts - Optional parameters * @returns {*} */ var s_EVENTS_API = function s_EVENTS_API(iteratee, events, name, callback, opts) { var i = 0, names = void 0; if (name && (typeof name === 'undefined' ? 'undefined' : (0, _typeof3.default)(name)) === 'object') { // Handle event maps. if (callback !== void 0 && 'context' in opts && opts.context === void 0) { opts.context = callback; } for (names = (0, _keys2.default)(name); i < names.length; i++) { events = s_EVENTS_API(iteratee, events, names[i], name[names[i]], opts); } } else if (name && s_EVENT_SPLITTER.test(name)) { // Handle space-separated event names by delegating them individually. for (names = name.split(s_EVENT_SPLITTER); i < names.length; i++) { events = iteratee(events, names[i], callback, opts); } } else { // Finally, standard events. events = iteratee(events, name, callback, opts); } return events; }; /** * Guard the `listening` argument from the public API. * * @param {Events} obj - The Events instance * @param {string} name - Event name * @param {function} callback - Event callback * @param {object} context - Event context * @param {Object.<{obj: object, objId: string, id: string, listeningTo: object, count: number}>} listening - * Listening object * @returns {*} */ var s_INTERNAL_ON = function s_INTERNAL_ON(obj, name, callback, context, listening) { obj._events = s_EVENTS_API(s_ON_API, obj._events || {}, name, callback, { context: context, ctx: obj, listening: listening }); if (listening) { var listeners = obj._listeners || (obj._listeners = {}); listeners[listening.id] = listening; } return obj; }; /** * The reducing API that removes a callback from the `events` object. * * @param {Object.<{callback: function, context: object, ctx: object, listening:{}}>} events - Events object * @param {string} name - Event name * @param {function} callback - Event callback * @param {object} options - Optional parameters * @returns {*} */ var s_OFF_API = function s_OFF_API(events, name, callback, options) { if (!events) { return; } var i = 0, listening = void 0; var context = options.context, listeners = options.listeners; // Delete all events listeners and "drop" events. if (!name && !callback && !context && listeners) { var ids = (0, _keys2.default)(listeners); for (; i < ids.length; i++) { listening = listeners[ids[i]]; delete listeners[listening.id]; delete listening.listeningTo[listening.objId]; } return; } var names = name ? [name] : (0, _keys2.default)(events); for (; i < names.length; i++) { name = names[i]; var handlers = events[name]; // Bail out if there are no events stored. /* istanbul ignore if */ if (!handlers) { break; } // Replace events if there are any remaining. Otherwise, clean up. var remaining = []; for (var j = 0; j < handlers.length; j++) { var handler = handlers[j]; if (callback && callback !== handler.callback && callback !== handler.callback._callback || context && context !== handler.context) { remaining.push(handler); } else { listening = handler.listening; if (listening && --listening.count === 0) { delete listeners[listening.id]; delete listening.listeningTo[listening.objId]; } } } // Update tail event if the list has any events. Otherwise, clean up. if (remaining.length) { events[name] = remaining; } else { delete events[name]; } } return events; }; /** * The reducing API that adds a callback to the `events` object. * * @param {Object.<{callback: function, context: object, ctx: object, listening:{}}>} events - Events object * @param {string} name - Event name * @param {function} callback - Event callback * @param {object} options - Optional parameters * @returns {*} */ var s_ON_API = function s_ON_API(events, name, callback, options) { if (callback) { var handlers = events[name] || (events[name] = []); var context = options.context, ctx = options.ctx, listening = options.listening; if (listening) { listening.count++; } handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); } return events; }; /** * Reduces the event callbacks into a map of `{event: onceWrapper}`. `offer` unbinds the `onceWrapper` after * it has been called. * * @param {Object.<{callback: function, context: object, ctx: object, listening:{}}>} map - Events object * @param {string} name - Event name * @param {function} callback - Event callback * @param {function} offer - Function to invoke after event has been triggered once; `off()` * @returns {*} */ var s_ONCE_MAP = function s_ONCE_MAP(map, name, callback, offer) { var _this = this, _arguments = arguments; if (callback) { var once = map[name] = function () { offer(name, once); return callback.apply(_this, _arguments); }; once._callback = callback; } return map; }; /** * Handles triggering the appropriate event callbacks. * * @param {Object.<{callback: function, context: object, ctx: object, listening:{}}>} objEvents - Events object * @param {string} name - Event name * @param {function} callback - Event callback * @param {Array<*>} args - Event arguments * @returns {*} */ var s_TRIGGER_API = function s_TRIGGER_API(objEvents, name, callback, args) { if (objEvents) { var events = objEvents[name]; var allEvents = objEvents.all; if (events && allEvents) { allEvents = allEvents.slice(); } if (events) { s_TRIGGER_EVENTS(events, args); } if (allEvents) { s_TRIGGER_EVENTS(allEvents, [name].concat(args)); } } return objEvents; }; /** * A difficult-to-believe, but optimized internal dispatch function for triggering events. Tries to keep the usual * cases speedy (most internal Backbone events have 3 arguments). * * @param {Object.<{callback: function, context: object, ctx: object, listening:{}}>} events - events array * @param {Array<*>} args - event argument array */ var s_TRIGGER_EVENTS = function s_TRIGGER_EVENTS(events, args) { // TODO: profile this for replacement. // let ev; // for (let i = -1, length = events.length; ++i < length;) { (ev = events[i]).callback.call(ev.ctx, ...args); } var ev = void 0, i = -1; var a1 = args[0], a2 = args[1], a3 = args[2], l = events.length; switch (args.length) { case 0: while (++i < l) { (ev = events[i]).callback.call(ev.ctx); } return; case 1: while (++i < l) { (ev = events[i]).callback.call(ev.ctx, a1); } return; case 2: while (++i < l) { (ev = events[i]).callback.call(ev.ctx, a1, a2); } return; case 3: while (++i < l) { (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); } return; default: while (++i < l) { (ev = events[i]).callback.apply(ev.ctx, args); } return; } }; /** * Generate a unique integer ID (unique within the entire client session). * * @type {number} - unique ID counter. */ var idCounter = 0; /** * Creates a new unique ID with a given prefix * * @param {string} prefix - An optional prefix to add to unique ID. * @returns {string} */ var s_UNIQUE_ID = function s_UNIQUE_ID() { var prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var id = '' + ++idCounter; return prefix ? '' + prefix + id : id; }; module.exports = exports['default'];