flex-events
Version:
A powerful, flexible and highly customizable event system for JavaScript. Supports all browsers and Node.js.
725 lines (596 loc) • 17.6 kB
JavaScript
/*
flex-events
LICENSE (MIT):
Copyright (c) 2013 Atlantis Flight Development, Bret Copeland (except where indicated).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/***************************************************************
* Browser Compatibility Methods
***************************************************************/
if (!Function.prototype.bind)
{
Function.prototype.bind = function (scope)
{
var func = this;
var args = Array.prototype.slice.call(arguments, 1);
return function ()
{
args = Array.prototype.concat.apply(args, Array.prototype.slice.call(arguments));
func.apply(scope, args);
};
};
}
if (!Array.prototype.indexOf)
{
// indexOf implementation copied from: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf
// (c) 2005 - 2013 Mozilla Developer Network and individual contributors
Array.prototype.indexOf = function (searchElement /*, fromIndex */ )
{
"use strict";
if (this == null)
throw new TypeError();
var t = Object(this);
var len = t.length >>> 0;
if (len === 0)
return -1;
var n = 0;
if (arguments.length > 1)
{
n = Number(arguments[1]);
if (n != n) // shortcut for verifying if it's NaN
n = 0;
else if (n != 0 && n != Infinity && n != -Infinity)
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
if (n >= len)
return -1;
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
for (; k < len; k++)
{
if (k in t && t[k] === searchElement)
return k;
}
return -1;
};
// lastIndexOf implementation from: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/lastIndexOf
// (c) 2005 - 2013 Mozilla Developer Network and individual contributors
Array.prototype.lastIndexOf = function(searchElement /*, fromIndex*/)
{
"use strict";
if (this == null)
throw new TypeError();
var t = Object(this);
var len = t.length >>> 0;
if (len === 0)
return -1;
var n = len;
if (arguments.length > 1)
{
n = Number(arguments[1]);
if (n != n)
n = 0;
else if (n != 0 && n != (1 / 0) && n != -(1 / 0))
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
var k = n >= 0 ? Math.min(n, len - 1) : len - Math.abs(n);
for (; k >= 0; k--)
{
if (k in t && t[k] === searchElement)
return k;
}
return -1;
};
}
/***************************************************************
* Global Events Object
***************************************************************/
var flexEvents = new function ()
{
var _this = this;
var _managers = [];
var _missingParents = [];
var _globalManager;
var _setupCalled = false;
var _configureCalled = false;
/***************************************************************
* Configuration
***************************************************************/
//default config
var _config =
{
arbitraryEvents: true, // If true, any string can be passed as an event name. If false, eventList must be an array of strings, and each event name will be checked for its existence in the list.
arbitraryInvoke: true, // If false, events can only be invoked using the function returned by register. invoke and registerList methods will be disabled.
eventList: null, // An array of event names/
strict: false, // If true, only events which have been added to an object can be listened for or invoked.
methods: { attach: "attach", detach: "detach", hasEvent: "hasEvent", invoke: "invoke", register: false, registerList: false, deregister: false, destroy: false },
errorHandler: null,
warningHandler: null,
bubble: true
};
function mergeConfig (original, config)
{
if (!config)
return original;
var mod = {};
for (var i in original)
{
if (i in config)
{
if (i === 'methods')
mod[i] = mergeConfig(original[i], config[i]);
else
mod[i] = config[i];
}
else
{
mod[i] = original[i];
}
}
return mod;
}
/***************************************************************
* Private Helper Functions
***************************************************************/
function compareObjects (a, b)
{
for (var i in a)
{
if (a[i] !== b[i])
return false;
}
// this is not efficient to loop twice, but it's the only way I know which will support backward compatibility with all browsers.
// might revisit this later.
for (i in b)
{
if (a[i] !== b[i])
return false;
}
return true;
}
/***************************************************************
* Public Methods
***************************************************************/
this.configure = function (config)
{
if (_setupCalled)
{
_globalManager.createError('The configure method cannot be called after setup.');
}
else
{
if (!_configureCalled && config && config.methods)
{
var name;
for (var m in config.methods)
{
name = config.methods[m];
switch (m)
{
case 'destroy':
continue;
case 'setup':
case 'configure':
if (name)
_this[name] = _this[m];
delete _this[m];
break;
default:
if (m in _this)
delete _this[m];
if (name)
_this[name] = _globalManager.eventMethods[m];
break;
}
}
}
_config = mergeConfig(_config, config);
_configureCalled = true;
}
};
this.setup = function (obj, parent, config)
{
_setupCalled = true;
return new EventsManager(obj, parent, config).eventMethods;
};
/***************************************************************
* EventsMethods Class
***************************************************************/
function EventsMethods (mgr)
{
var methods = mgr.getConfig('methods');
var arbitraryInvoke = mgr.getConfig('arbitraryInvoke');
for (var m in methods)
{
if (!arbitraryInvoke && (m === 'registerList' || m === 'invoke')) // if arbitrary invoke is not enabled, we don't need these methods.
continue;
this[m] = mgr[m].bind(mgr); // need to call bind, otherwise the correct context can be lost when reassigned.
if (m === 'destroy')
this[m] = createUndo(this[m]);
if (methods[m])
mgr.obj[methods[m]] = this[m]; // this makes the method public on the object
}
function createUndo (destroy)
{
return function ()
{
//undo all public functions created on the object.
for (var m in methods)
{
if (!arbitraryInvoke && (m === 'registerList' || m === 'invoke')) // if arbitrary invoke is not enabled, we don't need these methods.
continue;
if (methods[m])
delete mgr.obj[methods[m]];
}
return destroy();
};
}
}
/***************************************************************
* EventsManager Class
***************************************************************/
function EventsManager (obj, parent, config)
{
this.id = _managers.push(this) - 1;
this.config = config;
this.obj = obj;
this.events = {};
obj.__flexEvents = true;
var i;
//try to locate our own parent
this.parentIndex = -1;
if (parent)
{
for (i in _managers)
{
if (_managers[i].obj === parent)
{
this.parentIndex = i;
break;
}
}
if (this.parentIndex === -1)
{
this.createError('Event parent has not been setup for events.');
_missingParents.push({ parent: parent, childMgr: this });
}
}
// check to see if this is the missing parent for any object(s)
var mgr, bubble;
for (i = 0; i < _missingParents.length; i++)
{
if (_missingParents[i].parent === obj)
{
mgr = _missingParents[i].childMgr;
mgr.parentIndex = this.id;
// propagate existing events
bubble = mgr.getConfig('bubble');
for (var e in mgr.events)
{
if (mgr.events[e].explicit)
this.registerInternal(e, false, mgr.id);
for (var b in mgr.events[e].fromBubble)
{
this.registerInternal(e, false, mgr.events[e].fromBubble[b]);
}
}
_missingParents.splice(i, 1);
i--;
}
}
this.eventMethods = new EventsMethods(this);
}
EventsManager.prototype.attach = function (event, options, callback)
{
if (!this.validateRegistered(event, true))
return;
if (arguments.length < 3)
{
callback = options;
options = {};
}
this.events[event].listeners.push({ callback: callback, options: options }); // this doesn't check for duplicates, I'll leave that up to the user's implementation.
};
EventsManager.prototype.createError = function (text, important)
{
var handler = this.getConfig(important ? 'errorHandler' : 'warningHandler');
var error = new Error(text);
if (handler)
handler(error);
else if (important) // if it's important, but we don't have an error handler, then throw the error.
throw error;
};
EventsManager.prototype.deregister = function (event)
{
this.deregisterInternal(event, this.id);
};
EventsManager.prototype.deregisterInternal = function (event, id)
{
if (this.validateRegistered(event) !== 2)
return;
if (id === this.id)
{
this.events[event].explicit = false;
var mgr = this;
while (mgr = mgr.getParentManager())
{
mgr.deregisterInternal(event, id);
}
}
else
{
var dex = this.events[event].fromBubble.indexOf(id);
if (dex !== -1)
this.events[event].fromBubble.splice(dex, 1);
}
// if the event is not explicit, and not bubbling up from any children, then we can delete it entirely.
if (!this.events[event].explicit && !this.events[event].fromBubble.length)
{
delete this.events[event];
}
};
EventsManager.prototype.destroy = function ()
{
delete this.obj.__flexEvents;
_managers[this.id] = null;
this.obj = null;
for (var i in this.events)
{
this.deregister(i);
delete this.events[i];
}
};
EventsManager.prototype.detach = function (event, options, callback)
{
if (this.validateRegistered(event) !== 2)
return false;
if (arguments.length < 3)
{
callback = options;
options = null;
}
if (!callback) // if no callback was provided, detach all listeners.
{
this.events[event].listeners = [];
return true;
}
var listeners = this.events[event].listeners;
for (var i in listeners)
{
if (listeners[i].callback === callback)
{
if (options)
{
if (!compareObjects(options, listeners[i].options))
continue;
}
// listener found, now detach
listeners.splice(i, 1);
return true;
}
}
return false; // listener was not found
};
EventsManager.prototype.hasEvent = function (event)
{
return event in this.events;
};
EventsManager.prototype.getConfig = function (key)
{
if (key === 'methods')
{
return this.config && this.config.methods ? mergeConfig(_config.methods, this.config.methods) : _config.methods;
}
return this.config && key in this.config ? this.config[key] : _config[key];
};
EventsManager.prototype.getParentManager = function ()
{
if (this.parentIndex !== -1)
{
var mgr = _managers[this.parentIndex];
if (mgr)
return mgr;
this.parentIndex = -1;
}
return this === _globalManager ? null : _globalManager; // if there's no parent, return the global parent. If we are the global parent, return null.
};
EventsManager.prototype.invoke = function (event)
{
if (!this.validateRegistered(event, true))
return false;
var args = Array.prototype.slice.call(arguments);
new EventInvocation(this, event, args);
return true;
};
EventsManager.prototype.register = function (event)
{
return this.registerInternal(event, true);
};
EventsManager.prototype.registerInternal = function (event, explicit, bubbleId)
{
// we don't need to validate an event name when registering it on a parent
if (typeof bubbleId === 'undefined' && !this.validateEventName(event))
return false;
if (event in this.events)
{
this.events[event].explicit = explicit || this.events[event].explicit;
}
else
{
this.events[event] =
{
listeners: [],
invoke: this.invoke.bind(this, event),
explicit: explicit,
fromBubble: []
};
}
if (bubbleId === undefined)
{
if (this.getConfig('bubble'))
{
var mgr = this;
while (mgr = mgr.getParentManager())
{
mgr.registerInternal(event, false, this.id);
}
}
}
else
{
if (this.events[event].fromBubble.indexOf(bubbleId) === -1)
this.events[event].fromBubble.push(bubbleId);
}
return this.events[event].invoke;
};
EventsManager.prototype.registerList = function ()
{
var arg;
for (var i = 0; i < arguments.length; i++)
{
arg = arguments[i];
switch (typeof arg)
{
case 'string':
this.registerInternal(arg, true);
break;
case 'object':
if (arg) // in case for some crazy reason someone sends null as an argument.
{
for (var x in arg)
this.registerInternal(arg[x], true);
}
break;
}
}
};
EventsManager.prototype.validateEventName = function (event)
{
if (!this.getConfig('arbitraryEvents'))
{
// if arbitrary events are disabled, we need make sure this event is on the list
var list = this.getConfig('eventList');
if (!list || list.indexOf(event) === -1)
{
this.createError('Arbitrary Events are disabled. ' + event + ' is not on the event list.', true);
return false;
}
}
return true;
};
EventsManager.prototype.validateRegistered = function (event, autoRegister)
{
if (event in this.events)
return 2;
if (this.getConfig('strict'))
{
this.createError('Events are configured with strict enabled, and ' + event + ' is not a registered event on this object.', true);
return 0;
}
if (autoRegister || event === '*')
return this.register(event) ? 1 : 0;
return 1;
};
/***************************************************************
* EventInvocation Class
***************************************************************/
function EventInvocation (mgr, event, args)
{
var _this = this;
var _mgr = mgr;
var _listenerIndex = 0;
var _stop = false; // for breaking immediately
var _continue = true; // for breaking at next level
var _pause = false;
var _running;
this.origin = mgr.obj;
this.obj = mgr.obj;
this.event = event;
args[0] = this; // the zero index is initially the same string as the event argument, so we just replace that.
this.stop = function (continueLevel)
{
_stop = true;
};
this.stopBubble = function ()
{
_continue = false;
};
this.pause = function ()
{
_pause = true;
};
this.resume = function ()
{
_pause = false;
if (!_running) // if resume is called asynchronously, we need to call run in order to resume.
run();
};
// The run function is designed so that it can return on a pause, then be called again on a resume and pick up where it left off.
// Be cautious when making changes to this function.
var run = function ()
{
_running = true;
var listeners, starListeners;
var l;
do
{
listeners = _mgr.events[event].listeners;
if (event !== '*' && _mgr.events['*'] && (starListeners = _mgr.events['*'].listeners))
{
listeners = listeners ? listeners.concat(starListeners) : starListeners;
}
if (listeners)
{
_this.obj = _mgr.obj;
while (_listenerIndex < listeners.length)
{
l = listeners[_listenerIndex];
//check listener options for originOnly and bubbleOnly
if ((!l.options.originOnly || _this.obj === _this.origin) &&
(!l.options.bubbleOnly || _this.obj !== _this.origin))
{
// actually call the listener's callback here
_this.options = l.options;
l.callback.apply(null, args);
}
if (l.options.once)
listeners.splice(_listenerIndex, 1);
else
_listenerIndex++;
if (_stop)
return;
if (_pause)
{
_running = false;
return;
}
}
}
_listenerIndex = 0;
}
while (_continue && (_mgr = _mgr.getParentManager()));
};
run();
}
/***************************************************************
* Initialization
***************************************************************/
this.setup(this);
_setupCalled = false;
_globalManager = _managers[0];
}();
// Node.js support.
if (typeof module !== 'undefined' && module.exports)
module.exports = flexEvents;