UNPKG

devoir

Version:

Lightweight Javascript library adding functionality used in everyday tasks

498 lines (391 loc) 12.9 kB
(function(factory) { module.exports = function(_root) { var root = _root || {}; return factory(root); }; })(function(root) { 'use strict'; /** @namespace {devoir} **/ /** @namespace {events} Devoir event functionality **/ function generateID() { return 'E' + (idCounter++); } function getEventName(eventName) { if (eventName instanceof root.Event) return eventName.type; var p = ('' + eventName).match(/^([^.]+)/); return (p) ? p[1] : undefined; } function getNamespace(eventName) { if (eventName instanceof root.Event) return eventName.namespace; var ns = (this && this._events) ? this._events._defaultEventNamespace : undefined; if (eventName) { var p = ('' + eventName).match(/\.(.*)$/); if (p) ns = p[1]; } return ns; } function getListeners(_eventName, skipCreate) { var eventName = getEventName.call(this, _eventName), events = this._events; if (!events) return; var bindings = events._eventBindings, listeners = bindings[eventName]; if (!listeners) { if (skipCreate) return; listeners = bindings[eventName] = []; } return listeners; } function getAllListeners(_eventName) { var eventName = getEventName.call(this, _eventName), namespace = getNamespace.call(this, _eventName), events = this._events; if (!events) return; var bindings = events._eventBindings, keys = Object.keys(bindings), allListeners = []; for (var i = 0, il = keys.length; i < il; i++) { var key = keys[i], listeners = bindings[eventName]; if (!listeners) continue; for (var j = 0, jl = listeners.length; j < jl; j++) { var listener = listeners[j]; if (!listener) continue; if (namespace && namespace !== listener.namespace) continue; allListeners.push(listener); } } return allListeners; } function emitToAllListeners(event, listeners, args) { var namespace = event.namespace, ret = []; for (var i = 0, il = listeners.length; i < il; i++) { var listenerObj = listeners[i]; //Should we halt event chain? if (event.halt === true) break; //If propagate is false, don't propagate to other namespaces if (event.propagate === false && listenerObj.namespace !== namespace) continue; //Call listeners ret.push(listenerObj.listener.bind(this, event).apply(this, args)); } return ret; } function doEmit(thisEvent, listeners, args) { //Call listeners emitToAllListeners.call(this, thisEvent, listeners, args); //Remove all "call-once" listeners var onceListeners = []; for (var i = 0, il = listeners.length; i < il; i++) { var listenerObj = listeners[i]; if (listenerObj.once) onceListeners.push(listenerObj.listener); } if (onceListeners.length > 0) this.removeListener(thisEvent, onceListeners); //Call proxies var events = this._events, proxies = events._eventProxies; if (thisEvent.proxy || proxies.length === 0) return true; for (var i = 0, il = proxies.length; i < il; i++) { var proxyObj = proxies[i], context = proxyObj.dst, filter = proxyObj.filter; if (!context) continue; if (filter) { if (typeof filter === 'string' || filter instanceof String) { var filterEventName = getEventName.call(this, filter), filterNamespace = getNamespace.call(this, filter); if (filterEventName && filterEventName !== event.type) continue; if (filterNamespace && event.namespace !== event.namespace) continue; } else if ((filter instanceof Function) && filter.call(this, event) !== true) continue; } if (proxyObj.namespace && proxyObj.namespace !== thisEvent.namespace) continue; context.emit.bind(context, thisEvent).apply(context, args); } return true; } /** * @class {Event} Event class to hold event information. An instance of this will be passed as "event" to all bound event listeners * @constructor * @param {event} * @type {String} Event name * @type {Event} Skip event creation and use this event instance instead * @param {[options]} * @type {Object} Options object for event * @property {Boolean} {bubbles=true} The event bubbles across namespaces * @property {Object} {origin} Object that emitted the event @end * @property {Boolean} {async=false} This event when emitted is asynchronous * @end **/ function Event(event, _opts) { if (event instanceof root.Event) return event; var opts = _opts || {}, bubbles = (opts.bubbles !== false), origin = opts.origin; /** @property {String} {id} Unique id of this event **/ this.id = generateID(); /** @property {String} {namespace} Namespace this event was emitted on **/ this.namespace = getNamespace.call(origin || this, event); /** @property {String} {type} Type name of event **/ this.type = getEventName.call(origin || this, event); /** @property {Boolean} {bubbles} Does this event bubble? **/ this.bubbles = !!bubbles; /** @property {Boolean} {propagate} If true then this event will be propagated to all namespaces **/ this.propagate = this.bubbles; this.preventDefault = false; this.halt = false; /** @property {Boolean} {async} If true then this event will be emitted on nextTick **/ if (opts.async !== undefined && opts.async !== null) this.async = !!opts.async; /** @property {Object} {origin} The originating object **/ if (origin) this.origin = origin; } Event.prototype = { constructor: Event, /** @function {stopImmediatePropagation} Stop all propagation immediately **/ stopImmediatePropagation: function() { this.halt = true; }, /** @function {stopPropagation} Stop propagation. Bound event callbacks in the current namespace will still be called **/ stopPropagation: function() { this.propagate = false; }, /** @function {preventDefault} Set event property "preventDefault" to **true**. This does nothing internally; it is intended to be used with any callbacks that have default actions. **/ preventDefault: function() { this.preventDefault = true; } }; root.Event = Event; /** * @@class {EventEmitter} Base class for all event emitting functionality **/ /** * @constructor * @param {[options]} Options object * @type {Object} @property {Boolean} {async=false} If *true* events will be asynchronous by default @property {Object} {eventsObj={}} Object used to hold bound event listeners. Only define this is you need fine grained control over the events @@property {Object} {proxyObj=[] Array used to hold proxies. Only define this is you need fine grained control over proxies @end **/ function EventEmitter(_opts) { var opts = _opts || {}, eventsObj = opts.eventsObj || {}, proxyObj = opts.proxyObj || []; if (!this.hasOwnProperty('_events')) { Object.defineProperty(this, '_events', { writable: false, enumerable: false, configurable: false, value: {} }); } var events = this._events; Object.defineProperty(events, '_async', { writable: false, enumerable: false, configurable: false, value: !!opts.async }); if (opts.defaultNamespace || typeof events._eventBindings === 'undefined') { Object.defineProperty(events, '_defaultEventNamespace', { writable: true, enumerable: false, configurable: false, value: opts.defaultNamespace }); } if (opts.eventsObj || typeof events._eventBindings === 'undefined') { Object.defineProperty(events, '_eventBindings', { writable: true, enumerable: false, configurable: false, value: eventsObj }); } if (opts.proxyObj || typeof events._eventProxies === 'undefined') { Object.defineProperty(events, '_eventProxies', { writable: true, enumerable: false, configurable: false, value: proxyObj }); } } EventEmitter.prototype = { constructor: EventEmitter, createEvent: function(eventName, _opts) { if (eventName instanceof root.Event) return eventName; var opts = Object.create(_opts || {}); if (!opts.origin) opts.origin = this; return new root.Event(eventName, opts); }, addListener: function(_eventName, listener, _opts) { var eventName = getEventName.call(this, _eventName), opts = _opts || {}, namespace = getNamespace.call(this, _eventName), listeners = getListeners.call(this, eventName); if (!eventName) return this; var e = this.createEvent("newListener", {proxy: false, async: false}); this.emit(e, listener); if (e.preventDefault) return this; listeners.push({ event: eventName, listener: listener, namespace: namespace, order: listeners.length, once: opts.once }); return this; }, removeListener: function(_eventName, _removeListeners) { var removeListeners = _removeListeners || [], eventName = getEventName.call(this, _eventName), listeners = getListeners.call(this, eventName, true), namespace = getNamespace.call(this, _eventName), events = this._events; if (!listeners || !events) return this; if (!(removeListeners instanceof Array)) removeListeners = [removeListeners]; var e = this.createEvent("removeListener", {proxy: false, async: false}), newListeners = []; for (var i = 0, il = listeners.length; i < il; i++) { var listenerObj = listeners[i]; if (removeListeners.length > 0 && removeListeners.indexOf(listenerObj.listener) < 0) { newListeners.push(listenerObj); continue; } if (namespace && listenerObj.namespace !== namespace) { newListeners.push(listenerObj); continue; } e.halt = false; e.preventDefault = false; e.propagate = true; this.emit(e, listenerObj.listener); if (e.preventDefault === true) { newListeners.push(listenerObj); continue; } } if (newListeners.length !== listeners.length) events._eventBindings[eventName] = newListeners; return this; }, removeAllListeners: function(event) { var events = this._events; if (!events) return this; if (arguments.length === 0) { var bindings = events._eventBindings, keys = Object.keys(bindings); for (var i = 0, il = keys.length; i < il; i++) this.removeListener(keys[i]); events._eventBindings = {}; events._eventProxies = {}; return this; } return this.removeListener(event); }, listeners: function(event) { return getAllListeners.call(this, event, true); }, on: function(_events, listener) { var events = _events; if (!events) return this; var isString = (typeof events === 'string' || events instanceof String); if (isString || events instanceof Array) { if (isString) events = events.split(/\s+/g); for (var i = 0, il = events.length; i < il; i++) { var thisEvent = events[i]; this.addListener(thisEvent, listener); } } else if ((events instanceof Object) && !(events instanceof String)) { var events = event, keys = Object.keys(events); for (var i = 0, il = keys.length; i < il; i++) { var thisEvent = keys[i], thisListener = events[key]; this.addListener(thisEvent, thisListener); } } return this; }, off: function(event, listener) { return this.removeListener(event, listener); }, once: function(event, listener) { return this.addListener(event, listener, {once: true}); }, emit: function(event) { if (arguments.length === 0) return false; var events = this._events; if (!events) return false; var thisEvent = this.createEvent(event), allListeners = getAllListeners.call(this, event, true); if (allListeners.length === 0) return false; var args = new Array(arguments.length - 1); for (var i = 1, il = arguments.length; i < il; i++) args[i - 1] = arguments[i]; var async = (thisEvent.async === undefined) ? events._async : thisEvent.async; if (async) { setImmediate(doEmit.bind(this, thisEvent, allListeners, args)); } else { doEmit.call(this, thisEvent, allListeners, args); } return true; }, proxy: function(dst, filter) { var events = this._events; if (!events) return this; if (!dst || !(dst['emit'] instanceof Function)) return this; events._eventProxies.push({ dst: dst, filter: filter }); return this; }, removeProxy: function(dst) { var events = this._events; if (!events) return this; var ep = events._eventProxies; if (!ep) return this; var newProxies = []; for (var i = 0, il = ep.length; i < il; i++) { var thisEP = ep[i]; if (thisEP.dst === dst) continue; newProxies.push(thisEP); } if (newProxies.length !== ep.length) events._eventProxies = newProxies; return this; } }; var idCounter = 0; root.EventEmitter = EventEmitter; return root; });