Source: conbo.js

(function(global, factory, undefined)
{
	var doc;
	try { global = window; doc = document; } catch(e) {}

    // AMD (recommended)
    if (typeof define == 'function' && define.amd) 
	{
		define('conbo', function()
		{
			return factory(global, doc);
		});
	}
	// Common.js & Node.js
	else if (typeof module != 'undefined' && module.exports)
	{
   		module.exports = factory(global, doc);
		exports["default"] = module.exports;
		exports.__esModule = true;
    }
	// Global
	else
	{
		global.conbo = factory(global, doc);
	}
	
})(this, function(window, document, undefined)
{
	'use strict';
	
/*! 
 * ConboJS: Lightweight MVx application framework for JavaScript
 * http://conbo.mesmotronic.com/
 * 
 * Copyright (c) 2019 Mesmotronic Limited
 * Released under the MIT license
 * http://www.mesmotronic.com/legal/mit
 */

/**
 * @private
 */
var __namespaces = {};

/**
 * ConboJS is a lightweight MVx application framework for JavaScript featuring 
 * dependency injection, context and encapsulation, data binding, command 
 * pattern and an event model which enables callback scoping and consistent 
 * event handling
 * 
 * All ConboJS classes, methods and properties live within the conbo namespace
 * 
 * @namespace 	conbo
 */

/**
 * Create or access a ConboJS namespace
 * 
 * @variation	2
 * @function	conbo
 * @param		{string}	namespace - The selected namespace
 * @param		{...*}		[globals] - Globals to minify followed by function to execute, with each of the globals as parameters
 * @returns		{conbo.Namespace}
 * 			
 * @example
 * // Conbo can replace the standard minification pattern with modular namespace definitions
 * // If an Object is returned, its contents will be added to the namespace
 * conbo('com.example.namespace', window, document, conbo, function(window, document, conbo, undefined)
 * {
 *  // The executed function is scoped to the namespace
 * 	var ns = this;
 * 	
 * 	// ... Your code here ...
 * 
 * 	// Optionally, return an Object containing values to be added to the namespace
 *  return { MyApp, MyView };
 * });  
 * 
 * @example
 * // Retrieve a namespace and import classes defined elsewhere
 * var ns = conbo('com.example.namespace');
 * ns.import({ MyApp, MyView });
 */
var conbo = function(namespace)
{
	if (!namespace || !conbo.isString(namespace))
	{
		namespace = 'default';
	}

	if (!__namespaces[namespace])
	{
		__namespaces[namespace] = new conbo.Namespace();
	}
	
	var ns = __namespaces[namespace],
		params = conbo.rest(arguments),
		func = params.pop()
		;
	
	if (arguments.length == 1 && conbo.isFunction(arguments[0]))
	{
		func = arguments[0];
	}

	if (conbo.isFunction(func))
	{
		var obj = func.apply(ns, params);
		
		if (conbo.isObject(obj) && !conbo.isArray(obj))
		{
			ns.import(obj);
		}
	}
	
	return ns;
};

/**
 * Internal reference to self for use with ES2015 import statements
 * 
 * @memberof	conbo
 * @type		{conbo}
 * 
 * @example 
 * import { conbo } from 'conbo';
 */
conbo.conbo = conbo;

/**
 * The current ConboJS version number in the format major.minor.build
 * @memberof	conbo
 * @type	 	{string}
 */
conbo.VERSION = '4.3.27';
	
/**
 * A string containing the framework name and version number, e.g. "ConboJS v1.2.3"
 * @memberof	conbo
 * @returns 	{string}
 */
conbo.toString = function() 
{ 
	return 'ConboJS '+this.VERSION; 
};

/**
 * Lightweight Promise polyfill
 */
(function() 
{
	!('Promise' in window) && (function()
	{
		function Promise(fn) 
		{
			if (!(this instanceof Promise)) throw new TypeError('Promises must be constructed via new');
			if (typeof fn !== 'function') throw new TypeError('Parameter must be a function');

			this._state = 0;
			this._handled = false;
			this._value = undefined;
			this._deferreds = [];
		
			doResolve(fn, this);
		}
		
		function handle(self, deferred) 
		{
			while (self._state === 3) 
			{
				self = self._value;
			}

			if (self._state === 0) 
			{
				self._deferreds.push(deferred);
				return;
			}

			self._handled = true;

			setTimeout(function() 
			{
				var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;

				if (cb === null) 
				{
					(self._state === 1 ? resolve : reject)(deferred.promise, self._value);
					return;
				}

				var ret;
				
				try 
				{
					ret = cb(self._value);
				}
				catch (e)
				{
					reject(deferred.promise, e);
					return;
				}

				resolve(deferred.promise, ret);
			}, 0);
		}
		
		function resolve(self, newValue) 
		{
			try 
			{
				if (newValue === self)
				{
					throw new TypeError('A promise cannot be resolved with itself.');
				}

				if (newValue && (typeof newValue === 'object' || typeof newValue === 'function'))
				{
					var then = newValue.then;

					if (newValue instanceof Promise) 
					{
						self._state = 3;
						self._value = newValue;
						finale(self);
						return;
					}
					else if (typeof then === 'function') 
					{
						doResolve(conbo.bind(then, newValue), self);
						return;
					}
				}

				self._state = 1;
				self._value = newValue;
				finale(self);
			}
			catch (e)
			{
				reject(self, e);
			}
		}
		
		function reject(self, newValue) 
		{
			self._state = 2;
			self._value = newValue;
			finale(self);
		}
		
		function finale(self) 
		{
			if (self._state === 2 && self._deferreds.length === 0) 
			{
				// Ignore unhandled errors for now
			}
		
			for (var i = 0, len = self._deferreds.length; i < len; i++) 
			{
				handle(self, self._deferreds[i]);
			}

			self._deferreds = null;
		}
		
		function Handler(onFulfilled, onRejected, promise) 
		{
			this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
			this.onRejected = typeof onRejected === 'function' ? onRejected : null;
			this.promise = promise;
		}
		
		function doResolve(fn, self) 
		{
			var done = false;

			try 
			{
				fn(
					function(value) 
					{
						if (done) return;
						done = true;
						resolve(self, value);
					},

					function(reason) 
					{
						if (done) return;
						done = true;
						reject(self, reason);
					}
				);
			}
			catch (ex)
			{
				if (done) return;
				done = true;
				reject(self, ex);
			}
		}
		
		Promise.prototype['catch'] = function(onRejected) 
		{
			return this.then(null, onRejected);
		};
		
		Promise.prototype.then = function(onFulfilled, onRejected) 
		{
			var prom = new this.constructor(conbo.noop);
		
			handle(this, new Handler(onFulfilled, onRejected, prom));
			return prom;
		};
				
		Promise.all = function(arr) 
		{
			return new Promise(function(resolve, reject) 
			{
				if (!conbo.isArray(arr))
				{
					return reject(new TypeError('Promise.all accepts an array'));
				}
			
				var args = Array.prototype.slice.call(arr);
				if (args.length === 0) return resolve([]);
				var remaining = args.length;
			
				function res(i, val) 
				{
					try 
					{
						if (val && (typeof val === 'object' || typeof val === 'function')) 
						{
							var then = val.then;

							if (typeof then === 'function') 
							{
								then.call
								(
									val,
									function(val) {
									res(i, val);
									},
									reject
								);

								return;
							}
						}

						args[i] = val;

						if (--remaining === 0) {
							resolve(args);
						}
					}
					catch (ex) 
					{
						reject(ex);
					}
				}
			
				for (var i = 0; i < args.length; i++) {
					res(i, args[i]);
				}
			});
		};
		
		Promise.resolve = function(value) 
		{
			if (value && typeof value === 'object' && value.constructor === Promise) 
			{
				return value;
			}
		
			return new Promise(function(resolve) 
			{
				resolve(value);
			});
		};
		
		Promise.reject = function(value) 
		{
			return new Promise(function(resolve, reject) 
			{
				reject(value);
			});
		};
		
		Promise.race = function(arr) 
		{
			return new Promise(function(resolve, reject) 
			{
				if (!conbo.isArray(arr)) 
				{
					return reject(new TypeError('Promise.race accepts an array'));
				}
			
				for (var i = 0, len = arr.length; i < len; i++) 
				{
					Promise.resolve(arr[i]).then(resolve, reject);
				}
			});
		};
		
		window.Promise = Promise;

	})();

	!('finally' in window.Promise.prototype) && (function()
	{
		window.Promise.prototype['finally'] = function(callback) 
		{
			var constructor = this.constructor;
	
			return this.then
			(
				function(value) 
				{
					return constructor.resolve(callback()).then(function() 
					{
						return value;
					});
				},
	
				function(reason) 
				{
					return constructor.resolve(callback()).then(function() 
					{
						return constructor.reject(reason);
					});
				}
			);
		}
	})();

})();

conbo.Promise = window.Promise;

/**
 * Constant for JSON content type
 * 
 * @memberof	conbo
 * @constant
 * @type		{string}
 */
conbo.CONTENT_TYPE_JSON = 'application/json';

/**
 * Constant for form URL-encoded content type
 * 
 * @memberof	conbo
 * @constant
 * @type		{string}
 */
conbo.CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded';

/**
 * Constant for JSON data type
 * 
 * @memberof	conbo
 * @constant
 * @type		{string}
 */
conbo.DATA_TYPE_JSON = 'json';

/**
 * Constant for script data type type
 * 
 * @memberof	conbo
 * @constant
 * @type		{string}
 */
conbo.DATA_TYPE_SCRIPT = 'script';

/**
 * Constant for text data type type
 * 
 * @memberof	conbo
 * @constant
 * @type		{string}
 */
conbo.DATA_TYPE_TEXT = 'text';

/**
 * Default application namespace
 * 
 * @memberof	conbo
 * @constant
 * @type		{string}
 */
conbo.NAMESPACE_DEFAULT = 'default';

/*
 * Internal utility methods
 */

/**
 * Dispatch a property change event from the specified object
 * @private
 */
var __dispatchChange = function(obj, propName)
{
	if (obj instanceof conbo.EventDispatcher)
	{
		var options = {property:propName, value:obj[propName]};
		
		obj.dispatchEvent(new conbo.ConboEvent('change:'+propName, options));
		obj.dispatchEvent(new conbo.ConboEvent('change', options));
	}
};

/**
 * Creates a property which can be bound to DOM elements and others
 * 
 * @param	{Object}	obj	- The EventDispatcher object on which the property will be defined
 * @param	{string}	propName - The name of the property to be defined
 * @param	{*}			[value] - The initial value of the property (optional)
 * @private
 */
var __defineBindableProperty = function(obj, propName, value)
{
	if (conbo.isAccessor(obj, propName)) return;
	if (arguments.length < 3) value = obj[propName];
	
	var enumerable = propName.indexOf('_') != 0;
	var internalName = '__'+propName;
	
	__definePrivateProperty(obj, internalName, value);

	var getter = function()
	{
		return this[internalName];
	};

	var setter = function(newValue)
	{
		if (!conbo.isEqual(newValue, this[internalName])) 
		{
			this[internalName] = newValue;
			__dispatchChange(this, propName);
		}
	};
	
	Object.defineProperty(obj, propName, {enumerable:enumerable, configurable:true, get:getter, set:setter});
};

/**
 * Used by ConboJS to define private and internal properties (usually prefixed 
 * with an underscore) that can't be enumerated
 * 
 * @private
 */
var __definePrivateProperty = function(obj, propName, value)
{
	if (arguments.length == 2)
	{
		value = obj[propName];
	}
	
	Object.defineProperty(obj, propName, {enumerable:false, configurable:true, writable:true, value:value});
};

/**
 * Define properties that can't be enumerated
 * @private
 */
var __definePrivateProperties = function(obj, values)
{
	for (var key in values)
	{
		__definePrivateProperty(obj, key, values[key]);
	}
}

/**
 * Convert enumerable properties of the specified object into non-enumerable ones
 * @private
 */
var __denumerate = function(obj)
{
	var regExp = arguments[1];
	
	var keys = regExp instanceof RegExp
		? conbo.filter(conbo.keys(obj), function(key) { return regExp.test(key); })
		: (arguments.length > 1 ? conbo.rest(arguments) : conbo.keys(obj));
	
	keys.forEach(function(key)
	{
		var descriptor = Object.getOwnPropertyDescriptor(obj, key) 
			|| {value:obj[key], configurable:true, writable:true};
		
		descriptor.enumerable = false;
		Object.defineProperty(obj, key, descriptor);
	});
};

/**
 * Warn developers that the method they are using is deprecated
 * @private
 */
var __deprecated = function(deprecatedMethod, newMethod)
{
	conbo.warn('Deprecation warning: '+deprecatedMethod+' is deprecated, please use '+newMethod);
};

/**
 * Shortcut for new conbo.ElementProxy(el);
 * @private
 */
var __ep = function(el)
{
	return new conbo.ElementProxy(el);
};

/*
 * Utility methods: a modified subset of Underscore.js methods and loads of our own
 */

// TODO Remove methods that are now available natively in all target browsers

(function() 
{
	// Establish the object that gets returned to break out of a loop iteration.
	var breaker = false;

	// Save bytes in the minified (but not gzipped) version:
	var
		ArrayProto = Array.prototype, 
		ObjProto = Object.prototype
		;

	// Create quick reference variables for speed access to core prototypes.
	var
		push			= ArrayProto.push,
		slice			= ArrayProto.slice,
		concat			= ArrayProto.concat,
		toString		= ObjProto.toString,
		hasOwnProperty	= ObjProto.hasOwnProperty
		;

	// All ECMAScript 5 native function implementations that we hope to use
	// are declared here.
	var
		nativeIndexOf		= ArrayProto.indexOf,
		nativeLastIndexOf	= ArrayProto.lastIndexOf,
		nativeMap			= ArrayProto.map,
		nativeReduce		= ArrayProto.reduce,
		nativeReduceRight	= ArrayProto.reduceRight,
		nativeFilter		= ArrayProto.filter,
		nativeEvery			= ArrayProto.every,
		nativeSome			= ArrayProto.some,
		nativeIsArray		= Array.isArray,
		nativeKeys			= Object.keys
		;
	
	// Collection Functions
	// --------------------

	/**
	 * Handles objects, arrays, lists and raw objects using a for loop (because 
	 * tests show that a for loop can be twice as fast as a native forEach).
	 * 
	 * Return `false` to break the loop.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	iterator - Iterator function with parameters: item, index, list
	 * @param		{Object}	[scope] - The scope the iterator function should run in
	 * @returns		{void}
	 */
	 conbo.forEach = function(obj, iterator, scope) 
	 {
		if (obj == undefined) return;
		
		var i, length;
		
		if (conbo.isIterable(obj)) 
		{
			for (i=0, length=obj.length; i<length; ++i) 
			{
				if (iterator.call(scope, obj[i], i, obj) === breaker) return;
			}
		}
		else
		{
			var keys = conbo.keys(obj);
			
			for (i=0, length=keys.length; i<length; i++) 
			{
				if (iterator.call(scope, obj[keys[i]], keys[i], obj) === breaker) return;
			}
		}
		
		return obj;
	};
	
	var forEach = conbo.forEach;
	
	/**
	 * Return the results of applying the iterator to each element.
	 * Delegates to native `map` if available.
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Array.prototype.map
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	iterator - Iterator function with parameters: item, index, list
	 * @param		{Object}	[scope] - The scope the iterator function should run in
	 * @returns		{Array}
	 */
	conbo.map = function(obj, iterator, scope) 
	{
		return nativeMap.call(obj || [], iterator, scope);
	};
	
	/**
	 * Returns the index of the first instance of the specified item in the list
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Array.prototype.indexOf
	 * @param		{Object}	obj - The list to search
	 * @param		{Object}	item - The value to find the index of
	 * @returns		{number}
	 */
	conbo.indexOf = function(obj, item)
	{
		return nativeIndexOf.call(obj || [], item);
	};
	
	/**
	 * Returns the index of the last instance of the specified item in the list
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Array.prototype.lastIndexOf
	 * @param		{Object}	obj - The list to search
	 * @param		{Object}	item - The value to find the index of
	 * @returns		{number}
	 */
	conbo.lastIndexOf = function(obj, item)
	{
		return nativeLastIndexOf.call(obj || [], item);
	};
	
	/**
	 * Return the first value which passes a truth test
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	predicate - Function that tests each value, returning true or false
	 * @param		{Object}	[scope] - The scope the predicate function should run in
	 * @returns		{*}
	 */
	conbo.find = function(obj, predicate, scope) 
	{
		var result;
		
		conbo.some(obj, function(value, index, list) 
		{
			if (predicate.call(scope, value, index, list)) 
			{
				result = value;
				return true;
			}
		});
		
		return result;
	};
	
	/**
	 * Return the index of the first value which passes a truth test
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	predicate - Function that tests each value, returning true or false
	 * @param		{Object}	[scope] - The scope the predicate function should run in
	 * @returns		{number}
	 */
	conbo.findIndex = function(obj, predicate, scope) 
	{
		var value = conbo.find(obj, predicate, scope);
		return nativeIndexOf.call(obj, value);
	};
	
	/**
	 * Return all the elements that pass a truth test.
	 * Delegates to native `filter` if available.
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Array.prototype.filter
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	predicate - Function that tests each value, returning true or false
	 * @param		{Object}	[scope] - The scope the predicate function should run in
	 * @returns		{Array}
	 */
	conbo.filter = function(obj, predicate, scope) 
	{
		return nativeFilter.call(obj || [], predicate, scope);
	};

	/**
	 * Return all the elements for which a truth test fails.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	predicate - Function that tests each value, returning true or false
	 * @param		{Object}	[scope] - The scope the predicate function should run in
	 * @returns		{Array}
	 */
	conbo.reject = function(obj, predicate, scope) 
	{
		return conbo.filter(obj, function(value, index, list) 
		{
			return !predicate.call(scope, value, index, list);
		},
		scope);
	};
	
	/**
	 * Determine whether all of the elements match a truth test.
	 * Delegates to native `every` if available.
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Array.prototype.every
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	predicate - Function that tests each value, returning true or false
	 * @param		{Object}	[scope] - The scope the predicate function should run in
	 * @returns		{boolean}
	 */
	conbo.every = function(obj, predicate, scope) 
	{
		return nativeEvery.call(obj || [], predicate || conbo.identity, scope);
	};

	/**
	 * Determine if at least one element in the object matches a truth test.
	 * Delegates to native `some` if available.
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Array.prototype.some
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	predicate - Function that tests each value, returning true or false
	 * @param		{Object}	[scope] - The scope the predicate function should run in
	 * @returns		{Array}
	 */
	conbo.some = function(obj, predicate, scope) 
	{
		return nativeSome.call(obj || [], predicate || conbo.identity, scope);
	};
	
	var some = conbo.some;
	
	/**
	 * Determine if the array or object contains a given value (using `===`).
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	target - The value to match
	 * @returns		{boolean}
	 */
	conbo.contains = function(obj, target) 
	{
		return obj && 'indexOf' in obj
			? obj.indexOf(target) != -1
			: nativeIndexOf.call(obj || [], target) != -1
			;
	};

	/**
	 * Invoke a method (with arguments) on every item in a collection.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	method - Function to invoke on every item
	 * @returns		{Array}
	 */
	conbo.invoke = function(obj, method) 
	{
		var args = slice.call(arguments, 2);
		var isFunc = conbo.isFunction(method);
		
		return conbo.map(obj, function(value) 
		{
			return (isFunc ? method : value[method]).apply(value, args);
		});
	};
	
	/**
	 * Convenience version of a common use case of `map`: fetching a property.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Array of Objects
	 * @param		{string}	key - Property name
	 * @returns		{Array}
	 */
	conbo.pluck = function(obj, key) 
	{
		return conbo.map(obj, conbo.property(key));
	};

	/**
	 * Return the maximum element or (element-based computation).
	 * Can't optimize arrays of integers longer than 65,535 elements.
	 * 
	 * @see https://bugs.webkit.org/show_bug.cgi?id=80797
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	[iterator] - Function that tests each value
	 * @param		{Object}	[scope] - The scope the iterator function should run in
	 * @returns		{Object}
	 */
	conbo.max = function(obj, iterator, scope) 
	{
		if (!iterator && conbo.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) 
		{
			return Math.max.apply(Math, obj);
		}
		
		var result = -Infinity, lastComputed = -Infinity;
		
		forEach(obj, function(value, index, list) 
		{
			var computed = iterator ? iterator.call(scope, value, index, list) : value;
			if (computed > lastComputed) {
				result = value;
				lastComputed = computed;
			}
		});
		
		return result;
	};

	/**
	 * Return the minimum element (or element-based computation).
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to iterate
	 * @param		{Function}	[iterator] - Function that tests each value
	 * @param		{Object}	[scope] - The scope the iterator function should run in
	 * @returns		{Object}
	 */
	conbo.min = function(obj, iterator, scope) 
	{
		if (!iterator && conbo.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535)
		{
			return Math.min.apply(Math, obj);
		}
		
		var result = Infinity, lastComputed = Infinity;
		
		forEach(obj, function(value, index, list) 
		{
			var computed = iterator ? iterator.call(scope, value, index, list) : value;
			
			if (computed < lastComputed) 
			{
				result = value;
				lastComputed = computed;
			}
		});
		
		return result;
	};

	/**
	 * Shuffle an array, using the modern version of the Fisher-Yates shuffle
	 * @see http://en.wikipedia.org/wiki/Fisher–Yates_shuffle
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The list to shuffle
	 * @returns		{Array}
	 */
	conbo.shuffle = function(obj) 
	{
		var rand;
		var index = 0;
		var shuffled = [];
		
		forEach(obj, function(value) 
		{
			rand = conbo.random(index++);
			shuffled[index - 1] = shuffled[rand];
			shuffled[rand] = value;
		});
		
		return shuffled;
	};

	/**
	 * Returns the sum of all of the values in an array
	 * @memberof	conbo
	 * @param 		{*} 		obj 
	 * @returns		{Number}
	 */
	conbo.sum = function(obj)
	{
		return ArrayProto.reduce.call(obj || [], function(a,c) { return a+c; }, 0);
	}

	/**
	 * An internal function to generate lookup iterators.
	 * @private
	 */
	var lookupIterator = function(value) 
	{
		if (value == undefined) return conbo.identity;
		if (conbo.isFunction(value)) return value;
		return conbo.property(value);
	};
	
	/**
	 * Convert anything iterable into an Array
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to convert into an Array 
	 * @returns		{Array}
	 */
	conbo.toArray = function(obj) 
	{
		if (!obj) return [];
		if (conbo.isArray(obj)) return slice.call(obj);
		if (conbo.isIterable(obj)) return conbo.map(obj, conbo.identity);
		return conbo.values(obj);
	};
	
	/**
	 * Return the number of elements in an object.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to count the keys of
	 * @returns		{number}
	 */
	conbo.size = function(obj) 
	{
		if (!obj) return 0;
		
		return conbo.isIterable(obj)
			? obj.length 
			: conbo.keys(obj).length
			;
	};
	
	// Array Functions
	// ---------------

	/**
	 * Get the last element of an array. Passing n will return the last N
	 * values in the array. The guard check allows it to work with `conbo.map`.
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to slice
	 * @param		{Function}	n - The number of elements to return (default: 1)
	 * @param		{Object}	[guard] - Optional
	 * @returns		{Object}
	 */
	conbo.last = function(array, n, guard) 
	{
		if (array == undefined) return undefined;
		if (n == undefined || guard) return array[array.length - 1];
		return slice.call(array, Math.max(array.length - n, 0));
	};

	/**
	 * Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
	 * Especially useful on the arguments object. Passing an n will return
	 * the rest N values in the array. The guard
	 * check allows it to work with `conbo.map`.
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to slice
	 * @param		{Function}	n - The number of elements to return (default: 1)
	 * @param		{Object}	[guard] - Optional
	 * @returns		{Array}
	 */
	conbo.rest = function(array, n, guard) 
	{
		return slice.call(array, (n == undefined) || guard ? 1 : n);
	};

	/**
	 * Trim out all falsy values from an array.
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to trim
	 * @returns		{Array}
	 */
	conbo.compact = function(array) 
	{
		return conbo.filter(array, conbo.identity);
	};

	/**
	 * Internal implementation of a recursive `flatten` function.
	 * @private
	 */
	var flatten = function(input, shallow, output) 
	{
		if (shallow && conbo.every(input, conbo.isArray)) 
		{
			return concat.apply(output, input);
		}
		
		forEach(input, function(value) 
		{
			if (conbo.isArray(value) || conbo.isArguments(value)) 
			{
				shallow ? push.apply(output, value) : flatten(value, shallow, output);
			}
			else 
			{
				output.push(value);
			}
		});
		
		return output;
	};

	/**
	 * Flatten out an array, either recursively (by default), or just one level.
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to flatten
	 * @returns		{Array}
	 */
	conbo.flatten = function(array, shallow) 
	{
		return flatten(array, shallow, []);
	};

	/**
	 * Return a version of the array that does not contain the specified value(s).
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to remove the specified values from
	 * @param		{...*}		Items to remove from the array
	 * @returns		{Array}
	 */
	conbo.without = function(array) 
	{
		return conbo.difference(array, slice.call(arguments, 1));
	};

	/**
	 * Split an array into two arrays: one whose elements all satisfy the given
	 * predicate, and one whose elements all do not satisfy the predicate.
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to split
	 * @param		{Function}	predicate - Function to determine a match, returning true or false
	 * @returns		{Array}
	 */
	conbo.partition = function(array, predicate) 
	{
		var pass = [], fail = [];
		
		forEach(array, function(elem) 
		{
			(predicate(elem) ? pass : fail).push(elem);
		});
		
		return [pass, fail];
	};

	/**
	 * Produce a duplicate-free version of the array. If the array has already
	 * been sorted, you have the option of using a faster algorithm.
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The array to filter
	 * @param		{boolean}	isSorted - Should the returned array be sorted?
	 * @param		{Object}	iterator - Iterator function
	 * @param		{Object}	[scope] - The scope the iterator function should run in
	 * @returns		{Array}
	 */
	conbo.uniq = function(array, isSorted, iterator, scope) 
	{
		if (conbo.isFunction(isSorted)) 
		{
			scope = iterator;
			iterator = isSorted;
			isSorted = false;
		}
		
		var initial = iterator ? conbo.map(array, iterator, scope) : array;
		var results = [];
		var seen = [];
		
		forEach(initial, function(value, index) 
		{
			if (isSorted ? (!index || seen[seen.length - 1] !== value) : !conbo.contains(seen, value)) 
			{
				seen.push(value);
				results.push(array[index]);
			}
		});
		
		return results;
	};

	/**
	 * Produce an array that contains the union: each distinct element from all of
	 * the passed-in arrays.
	 * 
	 * @memberof	conbo
	 * @param		{...array}	array - Arrays to merge
	 * @returns		{Array}
	 */
	conbo.union = function() 
	{
		return conbo.uniq(conbo.flatten(arguments, true));
	};

	/**
	 * Produce an array that contains every item shared between all the
	 * passed-in arrays.
	 * 
	 * @memberof	conbo
	 * @param		{...Array}	array - Arrays of values
	 * @returns		{Array}
	 */
	conbo.intersection = function(array) 
	{
		var rest = slice.call(arguments, 1);
		
		return conbo.filter(conbo.uniq(array), function(item) 
		{
			return conbo.every(rest, function(other) 
			{
				return conbo.contains(other, item);
			});
		});
	};

	/**
	 * Take the difference between one array and a number of other arrays.
	 * Only the elements present in just the first array will remain.
	 * 
	 * @memberof	conbo
	 * @param		{...array}	array - Arrays of compare
	 * @returns		{Array}
	 */
	conbo.difference = function(array) 
	{
		var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
		return conbo.filter(array, function(value){ return !conbo.contains(rest, value); });
	};

	/**
	 * Converts lists into objects. Pass either a single array of `[key, value]`
	 * pairs, or two parallel arrays of the same length -- one of keys, and one of
	 * the corresponding values.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	list - List of keys
	 * @param		{Object}	values - List of values
	 * @returns		{Array}
	 */
	conbo.object = function(list, values) 
	{
		if (list == undefined) return {};
		
		var result = {};
		
		for (var i = 0, length = list.length; i < length; i++) 
		{
			if (values) 
			{
				result[list[i]] = values[i];
			}
			else 
			{
				result[list[i][0]] = list[i][1];
			}
		}
		return result;
	};
	
	/**
	 * Generate an integer Array containing an arithmetic progression. A port of
	 * the native Python `range()` function.
	 * 
	 * @see 		http://docs.python.org/library/functions.html#range
	 * @memberof	conbo
	 * @param		{number}	start - Start
	 * @param		{number}	stop - Stop
	 * @param		{number}	stop - Step
	 * @returns		{Array}
	 */
	conbo.range = function(start, stop, step) 
	{
		if (arguments.length <= 1) 
		{
			stop = start || 0;
			start = 0;
		}
		
		step = arguments[2] || 1;

		var length = Math.max(Math.ceil((stop - start) / step), 0);
		var idx = 0;
		var range = new Array(length);

		while(idx < length) 
		{
			range[idx++] = start;
			start += step;
		}

		return range;
	};

	// Function (ahem) Functions
	// ------------------

	// Reusable constructor function for prototype setting.
	var ctor = function(){};

	/**
	 * Bind one or more of an object's methods to that object. Remaining arguments
	 * are the method names to be bound. If no additional arguments are passed,
	 * all of the objects methods that are not native or accessors are bound to it.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to bind methods to
	 * @returns		{Object}
	 */
	conbo.bindAll = function(obj)
	{
		var funcs;
		
		if (arguments.length > 1)
		{
			funcs = conbo.rest(arguments);
		}
		else
		{
			funcs = conbo.filter(conbo.getFunctionNames(obj, true), function(func)
			{
				return !conbo.isNative(obj[func]);
			});
		}
		
		funcs.forEach(function(func)
		{
			obj[func] = obj[func].bind(obj);
		});
		
		return obj;
	};

	/**
	 * Partially apply a function by creating a version that has had some of its
	 * arguments pre-filled, without changing its dynamic `this` scope.
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - Method to partially pre-fill
	 * @param		{...*}		args - Arguments to pass to specified method
	 * @returns		{Function}
	 */
	conbo.partial = function(func) 
	{
		var boundArgs = slice.call(arguments, 1);
		
		return function() 
		{
			var position = 0;
			var args = boundArgs.slice();
			
			for (var i = 0, length = args.length; i < length; i++) 
			{
				if (args[i] === conbo) args[i] = arguments[position++];
			}
			
			while (position < arguments.length) args.push(arguments[position++]);
			return func.apply(this, args);
		};
	};	
	
	var ready__domContentLoaded = !document || ['complete', 'loaded'].indexOf(document.readyState) != -1;
	
	/**
	 * Calls the specified function as soon as the DOM is ready, if it is not already,
	 * otherwise call it at the end of the current callstack
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - The function to call
	 * @param		{Object}	[scope] - The scope in which to run the specified function
	 * @returns		{conbo}
	 */
	conbo.ready = function(func, scope)
	{
		var args = conbo.toArray(arguments);
		
		var readyHandler = function()
		{
			if (document)
			{
				document.removeEventListener('DOMContentLoaded', readyHandler);
			}
			
			ready__domContentLoaded = true;
			conbo.defer.apply(conbo, args);
		};
		
		ready__domContentLoaded
			? readyHandler()
			: document.addEventListener('DOMContentLoaded', readyHandler);
			
		return conbo;
	};
	
	/**
	 * Defers a function, scheduling it to run after the current call stack has
	 * cleared.
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - Function to call
	 * @param		{Object}	[scope] - The scope in which to call the function
	 * @returns		{number}	ID that can be used with clearInterval
	 */
	conbo.defer = function(func, scope) 
	{
		if (scope)
		{
			func = func.bind(scope);
		}
		
		return setTimeout.apply(undefined, [func, 0].concat(conbo.rest(arguments, 2)));
	};

	var callLater__tasks = [];
	
	var callLater__run = function()
	{
		var task;
		
		while (task = callLater__tasks.shift()) 
		{
			task();
		}
	};
	
	/**
	 * Calls a function at the start of the next animation frame, useful when 
	 * updating multiple elements in the DOM
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - Function to call
	 * @param		{Object}	[scope] - The scope in which to call the function
	 * @returns		{conbo}
	 */
	conbo.callLater = function(func, scope)
	{
		if (callLater__run.length === 0)
		{
			window.requestAnimationFrame(callLater__run);
		}
		
		var task = function()
		{
			func.apply(scope, conbo.rest(arguments, 2));
		}
		
		callLater__tasks.push(task);
		
		return conbo;
	};
	
	/**
	 * Returns a function that will be executed at most one time, no matter how
	 * often you call it. Useful for lazy initialization.
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - Function to call
	 * @returns		{Function}
	 */
	conbo.once = function(func) 
	{
		var ran = false, memo;
		
		return function() 
		{
			if (ran) return memo;
			ran = true;
			memo = func.apply(this, arguments);
			func = undefined;
			return memo;
		};
	};

	/**
	 * Returns the first function passed as an argument to the second,
	 * allowing you to adjust arguments, run code before and after, and
	 * conditionally execute the original function.
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - Function to wrap
	 * @param		{Function}	wrapper - Function to call
	 * @returns		{Function}
	 */
	conbo.wrap = function(func, wrapper) 
	{
		return conbo.partial(wrapper, func);
	};
	
	// Object Functions
	// ----------------

	/**
	 * Extends Object.keys to retrieve the names of an object's 
	 * enumerable properties
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @returns		{Array}
	 */
	conbo.keys = function(obj, deep)
	{
		if (!obj) return [];
		
		if (deep)
		{
			var keys = [];
			for (var key in obj) { keys.push(key); }
			return keys;
		}
		
		return nativeKeys(obj);
	};
	
	/**
	 * Extends Object.keys to retrieve the names of an object's 
	 * enumerable functions
	 * 
	 * @memberof	conbo
	 * @see			#keys
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @param		{boolean}	includeAccessors - Whether or not to include accessors that contain functions (default: false)
	 * @returns		{Array}
	 */
	conbo.functions = function(obj, deep, includeAccessors)
	{
		return conbo.filter(conbo.keys(obj, deep), function(name) 
		{
			return includeAccessors ? conbo.isFunction(obj[name]) : conbo.isFunc(obj, name);
		});
	};
	
	/**
	 * Extends Object.keys to retrieve the names of an object's enumerable 
	 * variables
	 * 
	 * @memberof	conbo
	 * @see			#keys
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @returns		{Array}
	 */
	conbo.variables = function(obj, deep)
	{
		return conbo.difference(conbo.keys(obj, deep), conbo.functions(obj, deep));
	};
	
	/**
	 * Extends Object.getOwnPropertyNames to retrieve the names of every 
	 * property of an object, regardless of whether it's enumerable or 
	 * unenumerable
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @returns		{Array}
	 */
	conbo.getPropertyNames = function(obj, deep)
	{
		if (!obj) return [];
		
		if (deep)
		{
			var names = [];
			do { names = names.concat(Object.getOwnPropertyNames(obj)); }
			while (obj = Object.getPrototypeOf(obj));
			return conbo.uniq(names);
		}
		
		return Object.getOwnPropertyNames(obj);
	};
	
	/**
	 * Extends Object.getOwnPropertyNames to retrieves the names of every 
	 * function of an object, regardless of whether it's enumerable or 
	 * unenumerable
	 * 
	 * @memberof	conbo
	 * @see			#getPropertyNames
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @param		{boolean}	includeAccessors - Whether or not to include accessors that contain functions (default: false)
	 * @returns		{Array}
	 */
	conbo.getFunctionNames = function(obj, deep, includeAccessors)
	{
		return conbo.filter(conbo.getPropertyNames(obj, deep), function(name) 
		{
			return includeAccessors ? conbo.isFunction(obj[name]) : conbo.isFunc(obj, name);
		});
	},
	
	/**
	 * Extends Object.getOwnPropertyNames to retrieves the names of every 
	 * variable of an object, regardless of whether it's enumerable or 
	 * unenumerable
	 * 
	 * @memberof	conbo
	 * @see			#getPropertyNames
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @returns		{Array}
	 */
	conbo.getVariableNames = function(obj, deep)
	{
		return conbo.difference(conbo.getPropertyNames(obj, deep), conbo.getFunctionNames(obj, deep));
	};
	
	/**
	 * Extends Object.getOwnPropertyNames to retrieves the names of every 
	 * public variable of an object, regardless of whether it's enumerable or 
	 * unenumerable
	 * 
	 * @memberof	conbo
	 * @see			#getPropertyNames
	 * @param		{Object}	obj - Object to get keys from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @returns		{Array}
	 */
	conbo.getPublicVariableNames = function(obj, deep)
	{
		return conbo
			.getVariableNames(obj, deep)
			.filter(function(name) { return name.indexOf('_') != 0; })
			;
	};
	
	/**
	 * Extends Object.getOwnPropertyDescriptor to return a property descriptor 
	 * for a property of a given object, regardless of where it is in the 
	 * prototype chain
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object containing the property
	 * @param		{string}	propName - Name of the property
	 * @returns		{Object}
	 */
	conbo.getPropertyDescriptor = function(obj, propName)
	{
		if (!obj) return;
		
		do
		{
			var descriptor = Object.getOwnPropertyDescriptor(obj, propName);
			if (descriptor) return descriptor;
		}
		while (obj = Object.getPrototypeOf(obj))
	};
	
	/**
	 * Retrieve the values of an object's enumerable properties, optionally 
	 * including values further up the prototype chain
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to get values from
	 * @param		{boolean}	[deep] - Retrieve keys from further up the prototype chain?
	 * @returns		{Array}
	 */
	conbo.values = function(obj, deep) 
	{
		var keys = conbo.keys(obj, deep);
		var length = keys.length;
		var values = new Array(length);
		
		for (var i=0; i<length; i++)
		{
			values[i] = obj[keys[i]];
		}
		
		return values;
	};

	/**
	 * Define the values of the given object by cloning all of the properties 
	 * of the passed-in object(s), destroying and overwriting the target's 
	 * property descriptors and values in the process
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to define properties on
	 * @param		{...*}		source - Objects containing properties to define 
	 * @returns		{Object}
	 * @see			conbo.setValues
	 */
	conbo.defineValues = function(target, source) 
	{
		forEach(slice.call(arguments, 1), function(source) 
		{
			if (!source) return;
			
			for (var propName in source) 
			{
				conbo.cloneProperty(source, propName, target);
			}
		});
		
		return target;
	};
	
	/**
	 * Define bindable values on the given object using the property names and
	 * of the passed-in object(s), destroying and overwriting the target's 
	 * property descriptors and values in the process
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to define properties on
	 * @param		{...*}		source - Objects containing properties to defined
	 * @returns		{Object}
	 */
	conbo.defineBindableValues = function(target, source) 
	{
		forEach(slice.call(arguments, 1), function(source) 
		{
			if (!source) return;
			
			for (var propName in source) 
			{
				delete target[propName];
				__defineBindableProperty(target, propName, source[propName]);
			}
		});
		
		return target;
	};
	
	/**
	 * Return an object containing the values of each of whitelisted properties.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Objects to copy properties from
	 * @param		{...string}	propName - Property names to copy 
	 * @returns		{Object}
	 */
	conbo.pick = function(obj) 
	{
		var copy = {};
		var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
		
		forEach(keys, function(key) 
		{
			if (key in obj)
			{
				copy[key] = obj[key];
			}
		});
		
		return copy;
	};
	
	/**
	 * Return an object containing all of the values from the source except the specified blacklisted properties.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to copy
	 * @param		{...string}	propNames - Names of properties to omit
	 * @returns		{Object}
	 */
	conbo.omit = function(obj) 
	{
		var copy = {};
		var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
		
		for (var key in obj) 
		{
			if (!conbo.contains(keys, key))
			{
				copy[key] = obj[key];
			}
		}
		
		return copy;
	};

	/**
	 * Fill in an object's missing properties by cloning the properties of the 
	 * source object(s) onto the target object, overwriting the target's
	 * property descriptors
	 * 
	 * @memberof	conbo
	 * @param		{Object}	target - Object to populate
	 * @param		{...Object}	obj - Objects containing default values
	 * @returns		{Object}
	 * @see			conbo.setDefaults
	 */
	conbo.defineDefaults = function(target) 
	{
		forEach(slice.call(arguments, 1), function(source) 
		{
			if (source) 
			{
				for (var propName in source) 
				{
					if (target[propName] !== undefined) continue;
					conbo.cloneProperty(source, propName, target);
				}
			}
		});
		
		return target;
	};
	
	/**
	 * Fill in missing values on an object by setting the property values on 
	 * the target object, without affecting the target's property descriptors
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to populate
	 * @param		{...Object}	source - Objects containging default values
	 * @returns		{Object}
	 */
	conbo.setDefaults = function(obj) 
	{
		forEach(slice.call(arguments, 1), function(source) 
		{
			if (source) 
			{
				for (var propName in source) 
				{
					if (obj[propName] !== undefined) continue;
					obj[propName] = source[propName];
				}
			}
		});
		
		return obj;
	};
	
	/**
	 * Fill in missing values on an object by setting the property values on 
	 * the target object, without affecting the target's property descriptors
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to populate
	 * @param		{...Object}	source - Objects containging default values
	 * @returns		{Object}
	 */
	conbo.implement = function(obj)
	{
		return conbo.setDefaults.apply(conbo, arguments);
	};

	/**
	 * Create a (shallow-cloned) duplicate of an object.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Object to clone
	 * @returns		{Object}
	 */
	conbo.clone = function(obj) 
	{
		if (!conbo.isObject(obj)) return obj;
		return conbo.isArray(obj) ? obj.slice() : conbo.defineValues({}, obj);
	};
	
	// Internal recursive comparison function for `isEqual`.
	var eq = function(a, b, aStack, bStack) {
		// Identical objects are equal. `0 === -0`, but they aren't identical.
		// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
		if (a === b) return a !== 0 || 1 / a == 1 / b;
		// A strict comparison is necessary because `null == undefined`.
		if (a == null || b == null) return a === b;
		// Unwrap any wrapped objects.
		// Compare `[[Class]]` names.
		var className = toString.call(a);
		if (className != toString.call(b)) return false;
		switch (className) {
			// Strings, numbers, dates, and booleans are compared by value.
			case '[object String]':
				// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
				// equivalent to `new String("5")`.
				return a == String(b);
			case '[object Number]':
				// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
				// other numeric values.
				return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
			case '[object Date]':
			case '[object Boolean]':
				// Coerce dates and booleans to numeric primitive values. Dates are compared by their
				// millisecond representations. Note that invalid dates with millisecond representations
				// of `NaN` are not equivalent.
				return +a == +b;
			// RegExps are compared by their source patterns and flags.
			case '[object RegExp]':
				return a.source == b.source &&
							 a.global == b.global &&
							 a.multiline == b.multiline &&
							 a.ignoreCase == b.ignoreCase;
		}
		if (typeof a != 'object' || typeof b != 'object') return false;
		// Assume equality for cyclic structures. The algorithm for detecting cyclic
		// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
		var length = aStack.length;
		while (length--) {
			// Linear search. Performance is inversely proportional to the number of
			// unique nested structures.
			if (aStack[length] == a) return bStack[length] == b;
		}
		// Objects with different constructors are not equivalent, but `Object`s
		// from different frames are.
		var aCtor = a.constructor, bCtor = b.constructor;
		if (aCtor !== bCtor && !(conbo.isFunction(aCtor) && (aCtor instanceof aCtor) &&
														 conbo.isFunction(bCtor) && (bCtor instanceof bCtor))
												&& ('constructor' in a && 'constructor' in b)) {
			return false;
		}
		// Add the first object to the stack of traversed objects.
		aStack.push(a);
		bStack.push(b);
		var size = 0, result = true;
		// Recursively compare objects and arrays.
		if (className == '[object Array]') {
			// Compare array lengths to determine if a deep comparison is necessary.
			size = a.length;
			result = size == b.length;
			if (result) {
				// Deep compare the contents, ignoring non-numeric properties.
				while (size--) {
					if (!(result = eq(a[size], b[size], aStack, bStack))) break;
				}
			}
		} else {
			// Deep compare objects.
			for (var key in a) {
				if (conbo.has(a, key)) {
					// Count the expected number of properties.
					size++;
					// Deep compare each member.
					if (!(result = conbo.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
				}
			}
			// Ensure that both objects contain the same number of properties.
			if (result) {
				for (key in b) {
					if (conbo.has(b, key) && !(size--)) break;
				}
				result = !size;
			}
		}
		// Remove the first object from the stack of traversed objects.
		aStack.pop();
		bStack.pop();
		return result;
	};

	/**
	 * Perform a deep comparison to check if two objects are equal.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	a - Object to compare
	 * @param		{Object}	b - Object to compare
	 * @returns		{boolean}
	 */
	conbo.isEqual = function(a, b) 
	{
		return eq(a, b, [], []);
	};

	/**
	 * Is the value empty?
	 * Based on PHP's `empty()` method
	 * 
	 * @memberof	conbo
	 * @param		{any}		value - Value that might be empty
	 * @returns		{boolean}
	 */
	conbo.isEmpty = function(value)
	{
		return !value // 0, false, undefined, null, ""
			|| (conbo.isIterable(value) && value.length === 0) // [], Arguments, List, etc
			|| (/^0\.?0+?$/.test(value) && !parseFloat(value)) // "0", "0.0", etc
			|| (conbo.isObject(value) && !conbo.keys(value).length) // {}
			;
	};
	
	/**
	 * Can the value be iterated using a for loop? For example an Array, Arguments, ElementsList, etc.
	 * 
	 * @memberof	conbo
	 * @param		{any}		obj - Object that might be iterable 
	 * @returns		{boolean}
	 */
	conbo.isIterable = function(obj)
	{
		return obj && obj.length === +obj.length;
	};
	
	/**
	 * Is a given value a DOM element?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be a DOM element
	 * @returns		{boolean}
	 */
	conbo.isElement = function(obj) 
	{
		return !!(obj && obj.nodeType === 1);
	};
	
	/**
	 * Is a given value an array?
	 * Delegates to ECMA5's native Array.isArray
	 * 
	 * @function
	 * @deprecated	Use Array.isArray
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be an Array
	 * @returns		{boolean}
	 */
	conbo.isArray = nativeIsArray || function(obj) 
	{
		return toString.call(obj) == '[object Array]';
	};

	/**
	 * Is a given variable an object?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be an Object
	 * @returns		{boolean}
	 */
	conbo.isObject = function(obj) 
	{
		return obj === Object(obj);
	};
	
	/**
	 * Is a given variable a plain object (i.e. not an instance of anything)?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be a plain Object
	 * @returns		{boolean}
	 */
	conbo.isPlainObject = function(obj)
	{
		if (!!obj && obj !== Math)
		{
			var p = Object.getPrototypeOf(obj);
			return !p || p === Object.prototype;
		}

		return false;
	};

	/**
	 * Is the specified object Arguments?
	 * @method		isArguments 
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to test
	 * @returns		{boolean}
	 */
	
	/**
	 * Is the specified object a Function?
	 * @method		isFunction  
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to test
	 * @returns		{boolean}
	 */
	
	/**
	 * Is the specified object a String?
	 * @method		isString
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to test
	 * @returns		{boolean}
	 */
	
	/**
	 * Is the specified object a Number?
	 * @method		isNumber  
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to test
	 * @returns		{boolean}
	 */
	
	/**
	 * Is the specified object a Date?
	 * @method		isDate  
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to test
	 * @returns		{boolean}
	 */
	
	/**
	 * Is the specified object a RegExp (regular expression)?
	 * @method		isRegExp  
	 * @memberof	conbo
	 * @param		{Object}	obj - The object to test
	 * @returns		{boolean}
	 */
	
	// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
	forEach(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) 
	{
		conbo['is' + name] = function(obj) 
		{
			return toString.call(obj) == '[object ' + name + ']';
		};
	});

	// Define a fallback version of the method in browsers (ahem, IE), where
	// there isn't any inspectable "Arguments" type.
	if (!conbo.isArguments(arguments)) 
	{
		conbo.isArguments = function(obj) 
		{
			return !!(obj && conbo.has(obj, 'callee'));
		};
	}
	
	// Optimize `isFunction` if appropriate.
	if (typeof(/./) !== 'function') 
	{
		conbo.isFunction = function(obj) 
		{
			return typeof obj === 'function';
		};
	}
	
	/**
	 * Detects whether the specified property was defined as a function, meaning
	 * accessors containing functions are excluded
	 * 
	 * @memberof	conbo
	 * @see			#isFunction
	 * 
	 * @param		{Object}	obj - Object containing the property
	 * @param		{string}	propName - The name of the property
	 * @returns		{boolean}	true if it's a function
	 */
	conbo.isFunc = function(obj, propName)
	{
		var descriptor = conbo.getPropertyDescriptor(obj, propName);
		return descriptor && typeof(descriptor.value) == 'function';
	};
	
	/**
	 * Is a given object a finite number?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be finite
	 * @returns		{boolean}
	 */
	conbo.isFinite = function(obj) 
	{
		return isFinite(obj) && !isNaN(parseFloat(obj));
	};

	/**
	 * Is the given value `NaN`? (NaN is the only number which does not equal itself).
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be NaN
	 * @returns		{boolean}
	 */
	conbo.isNaN = function(obj) 
	{
		return conbo.isNumber(obj) && obj != +obj;
	};

	/**
	 * Is a given value a boolean?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be a Boolean
	 * @returns		{boolean}
	 */
	conbo.isBoolean = function(obj) 
	{
		return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
	};

	/**
	 * Is a given value equal to null?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be null
	 * @returns		{boolean}
	 */
	conbo.isNull = function(obj)
	{
		return obj === null;
	};

	/**
	 * Is a given variable undefined?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - Value that might be undefined
	 * @returns		{boolean}
	 */
	conbo.isUndefined = function(obj) 
	{
		return obj === undefined;
	};

	/**
	 * Is the given value numeric? i.e. a number of a string that can be coerced into a number
	 * 
	 * @memberof	conbo
	 * @param		{*} value - Value that might be numeric
	 * @returns		{boolean}
	 */
	conbo.isNumeric = function(value)
	{
		return !isNaN(parseFloat(value));
	};

	/**
	 * Shortcut function for checking if an object has a given property directly
	 * on itself (in other words, not on a prototype).
	 * 
	 * @memberof	conbo
	 * @deprecated	Use Object.prototype.hasOwnProperty
	 * @param		{Object}	obj - Object
	 * @param		{string}	key - Property name
	 * @returns		{boolean}
	 */
	conbo.has = function(obj, key)
	{
		return hasOwnProperty.call(obj, key);
	};
	
	// Utility Functions
	// -----------------

	/**
	 * Keep the identity function around for default iterators.
	 * 
	 * @memberof	conbo
	 * @param		{*}		obj - Value to return
	 * @returns		{*}
	 */
	conbo.identity = function(value) 
	{
		return value;
	};
	
	/**
	 * Get the property value
	 * 
	 * @memberof	conbo
	 * @param		{string}	key - Property name
	 * @returns		{Function}
	 */
	conbo.property = function(key) 
	{
		return function(obj) 
		{
			return obj[key];
		};
	};

	/**
	 * Returns a predicate for checking whether an object has a given set of `key:value` pairs.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	attrs - Object containing key:value pairs to compare
	 * @returns		{Function}
	 */
	conbo.matches = function(attrs) 
	{
		return function(obj) 
		{
			if (obj === attrs) return true; //avoid comparing an object to itself.
			
			for (var key in attrs) 
			{
				if (attrs[key] !== obj[key])
				{
					return false;
				}
			}
			
			return true;
		};
	};
	
	/**
	 * Return a random integer between min and max (inclusive).
	 * 
	 * @memberof	conbo
	 * @param		{number}	min - Minimum number
	 * @param		{number}	max - Maximum number
	 * @returns		{number}
	 */
	conbo.random = function(min, max)
	{
		if (max == undefined) 
		{
			max = min;
			min = 0;
		}
		
		return min + Math.floor(Math.random() * (max - min + 1));
	};
	
	var idCounter = 0;

	/**
	 * Generate a unique integer id (unique within the entire client session).
	 * Useful for temporary DOM ids.
	 * 
	 * @memberof	conbo
	 * @param		{string}	[prefix] - String to prefix unique ID with
	 * @returns		{string}
	 */
	conbo.uniqueId = function(prefix) 
	{
		var id = ++idCounter + '';
		return prefix ? prefix + id : id;
	};
	
	/**
	 * Generates a version 4 RFC4122 UUID
	 * 
	 * @memberof	conbo
	 * @returns		{string}
	 */
	conbo.guid = function() 
	{
		if (window.crypto)
		{
			return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) 
			{
			    return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
			});
		}
		
		return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) 
		{
			var r = Math.random() * 16 | 0;
			return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
		});
	}	
	
	/**
	 * Is Conbo supported by the current browser?
	 * 
	 * @memberof	conbo
	 * @type		{boolean}
	 */
	conbo.isSupported = 
		window.addEventListener
		&& !!Object.defineProperty 
		&& !!Object.getOwnPropertyDescriptor
		;
	
	/**
	 * Is this script being run using Node.js?
	 * 
	 * @memberof	conbo
	 * @type		{boolean}
	 */
	conbo.isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';
	
	/**
	 * A function that does nothing
	 * 
	 * @memberof	conbo
	 * @returns		{Function}
	 */
	conbo.noop = function() {}; 
	
	/**
	 * Default function to assign to the methods of pseudo-interfaces
	 * 
	 * @example	IExample = { myMethod:conbo.notImplemented };
	 * @memberof	conbo
	 * @returns		{Function}
	 */
	conbo.notImplemented = function() 
	{
		conbo.warn('Method not implemented');
	};
	
	/**
	 * Convert dash-or_underscore separated words into camelCaseWords
	 * 
	 * @memberof	conbo
	 * @param		{string}	string - underscore_case_string to convertToCamelCase
	 * @param		{boolean}	[initCap=false] - Should the first letter be a CapitalLetter? (default: false)
	 * @returns		{string}
	 */
	conbo.toCamelCase = function(string, initCap)
	{
		var s = (string || '').toLowerCase().replace(/([\W_])([a-z])/g, function (g) { return g[1].toUpperCase(); }).replace(/(\W+)/, '');
		if (initCap) return s.charAt(0).toUpperCase() + s.slice(1);
		return s;
	};
	
	/**
	 * Convert camelCaseWords into underscore_case_words (or another user defined separator)
	 * 
	 * @memberof	conbo
	 * @param		{string}	string - camelCase string to convert to underscore_case
	 * @param		{string}	[separator=_] - Default: "_"
	 * @returns		{string}
	 */
	conbo.toUnderscoreCase = function(string, separator)
	{
		separator || (separator = '_');
		return (string || '').replace(/\W+/g, separator).replace(/([a-z\d])([A-Z])/g, '$1'+separator+'$2').toLowerCase();
	};
	
	/**
	 * Convert camelCaseWords into kebab-case-words
	 * 
	 * @memberof	conbo
	 * @param		{string}	string - camelCase string to convert to underscore_case
	 * @returns		{string}
	 */
	conbo.toKebabCase = function(string)
	{
		return conbo.toUnderscoreCase(string, '-');
	};

	/**
	 * Converts a value into a string that can be used as the value of an HTML element
	 * @memberof	conbo
	 * @param		{*}			value - The value to convert to a string
	 * @returns		{string}
	 */
	conbo.toValueString = function(value)
	{
		return value == null ? '' : String(value);
	};
	
	/**
	 * Pads a string with the specified character to the specified length
	 * 
	 * @memberof	conbo
	 * @param		{number|string}	value - String to pad
	 * @param		{number}		[minLength=2] - Minimum length of the padded string
	 * @param		{number|string}	[padChar= ] - The character to use to pad the string
	 * @returns		{string}
	 */
	conbo.padLeft = function(value, minLength, padChar)
	{
		if (!padChar && padChar !== 0) padChar = ' ';
		if (!value && value !== 0) value = '';
		
		minLength || (minLength = 2);
		
		padChar = padChar.toString().charAt(0);
		value = value.toString();
		
		while (value.length < minLength)
		{
			value = padChar + value;
		}
		
		return value;
	};

	/**
	 * Add a leading zero to the specified number and return it as a string
	 * @memberof 	conbo
	 * @param		{number}	number - The number to add a leading zero to
	 * @param		{number}	[minLength=2] - the minumum length of the returned string (default: 2)
	 * @returns		{string}
	 */
	conbo.addLeadingZero = function(number, minLength)
	{
		return conbo.padLeft(number, minLength, 0);
	};
	
	/**
	 * Format a number using the selected number of decimals, using the 
	 * provided decimal point, thousands separator 
	 * 
	 * @memberof	conbo
	 * @see 		http://phpjs.org/functions/number_format/
	 * @param 		{number} 	number
	 * @param 		{number} 	[decimals=0]				default: 0
	 * @param 		{string}	[decimalPoint=.]			default: '.'
	 * @param 		{string}	[thousandsSeparator=,]		default: ','
	 * @returns		{string}	Formatted number
	 */
	conbo.formatNumber = function(number, decimals, decimalPoint, thousandsSeparator) 
	{
		number = (number+'').replace(/[^0-9+\-Ee.]/g, '');
		
		var n = !isFinite(+number) ? 0 : +number,
			prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
			sep = conbo.isUndefined(thousandsSeparator) ? ',' : thousandsSeparator,
			dec = conbo.isUndefined(decimalPoint) ? '.' : decimalPoint,
			s = n.toFixed(prec).split('.')
			;
		
		if (s[0].length > 3) 
		{
			s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
		}
		
		if ((s[1] || '').length < prec) 
		{
			s[1] = s[1] || '';
			s[1] += new Array(prec-s[1].length+1).join('0');
		}
		
		return s.join(dec);
	};
	
	/**
	 * Format a number as a currency
	 * 
	 * @memberof	conbo
	 * @param 		{number}	number
	 * @param 		{string}	[symbol]
	 * @param 		{boolean}	[suffixed]
	 * @param 		{number}	[decimals]
	 * @param 		{string}	[decimalPoint]
	 * @param 		{string}	[thousandsSeparator]
	 * @returns		{string}
	 */
	conbo.formatCurrency = function(number, symbol, suffixed, decimals, decimalPoint, thousandsSeparator)
	{
		if (conbo.isUndefined(decimals)) decimals = 2;
		symbol || (symbol = '');
		var n = conbo.formatNumber(number, decimals, decimalPoint, thousandsSeparator);
		return suffixed ? n+symbol : symbol+n;
	};
	
	/**
	 * Encodes all of the special characters contained in a string into HTML 
	 * entities, making it safe for use in an HTML document
	 * 
	 * @memberof	conbo
	 * @param 		{string}	string - String to encode
	 * @returns		{string}
	 */
	conbo.encodeEntities = function(string)
	{
		if (!conbo.isString(string))
		{
			string = conbo.isNumber(string)
				? string.toString()
				: '';
		}
		
		return string.replace(/[\u00A0-\u9999<>\&]/gim, function(char)
		{
			return '&#'+char.charCodeAt(0)+';';
		});
	};
	
	/**
	 * Decodes all of the HTML entities contained in an string, replacing them with
	 * special characters, making it safe for use in plain text documents
	 * 
	 * @memberof	conbo
	 * @param 		{string}	string - String to dencode
	 * @returns		{string}
	 */
	conbo.decodeEntities = function(string) 
	{
		if (!conbo.isString(string)) string = '';
		
		return string.replace(/&#(\d+);/g, function(match, dec) 
		{
			return String.fromCharCode(dec);
		});
	};
	
	/**
	 * Copies all of the enumerable values from one or more objects and sets
	 * them on another, without affecting the target object's property
	 * descriptors. Unlike Object.assign(), the properties copied are not
	 * limited to own properties.
	 * 
	 * Unlike conbo.defineValues, assign only sets the values on the target 
	 * object and does not destroy and redifine them.
	 * 
	 * @memberof	conbo
	 * @param		{Object}	target - Object to copy properties to
	 * @param		{...Object}	source - Object to copy properties from
	 * @returns		{Object}
	 * 
	 * @example	
	 * conbo.assign({id:1}, {get name() { return 'Arthur'; }}, {get age() { return 42; }});
	 * => {id:1, name:'Arthur', age:42}
	 */
	conbo.assign = function(target)
	{
		conbo.rest(arguments).forEach(function(source) 
		{
			if (!source) return;
			
			for (var propName in source) 
			{
				target[propName] = source[propName];
			}
		});
		
		return target;
	};
	
	/**
	 * @see		conbo.setValues
	 * @param	{*} target 
	 */
	conbo.setValues = function(target)
	{
		__deprecated('conbo.setValues', 'conbo.assign');
		return conbo.assign.apply(conbo, arguments);
	}	

	/**
	 * Is the value a Conbo class?
	 * 
	 * @memberof	conbo
	 * @param		{any}		value - Value that might be a class
	 * @param		{class}		[classReference] - The Conbo class that the value must match or be an extension of 
	 * @returns		{boolean}
	 */
	conbo.isClass = function(value, classReference)
	{
		return !!value 
			&& typeof value == 'function' 
			&& value.prototype instanceof (classReference || conbo.Class)
			;
	};
	
	/**
	 * Copies a property, including defined properties and accessors, 
	 * from one object to another
	 * 
	 * @memberof	conbo
	 * @param		{Object}	source - Source object
	 * @param		{string}	sourceName - Name of the property on the source
	 * @param		{Object}	target - Target object
	 * @param		{string} 	[targetName] - Name of the property on the target (default: sourceName)
	 * @returns		{conbo}
	 */
	conbo.cloneProperty = function(source, sourceName, target, targetName)
	{
		targetName || (targetName = sourceName);
		
		var descriptor = Object.getOwnPropertyDescriptor(source, sourceName);
		
		if (!!descriptor)
		{
			Object.defineProperty(target, targetName, descriptor);
		}
		else 
		{
			target[targetName] = source[sourceName];
		}
		
		return this;
	};
	
	/**
	 * Sorts the items in an array according to one or more fields in the array. 
	 * The array should have the following characteristics:
	 * 
	 * <ul>
	 * <li>The array is an indexed array, not an associative array.</li>
	 * <li>Each element of the array holds an object with one or more properties.</li>
	 * <li>All of the objects have at least one property in common, the values of which can be used to sort the array. Such a property is called a field.</li>
	 * </ul>
	 * 
	 * @memberof	conbo
	 * @param		{Array}		array - The Array to sort
	 * @param		{string}	fieldName - The field/property name to sort on
	 * @param		{Object}	[options] - Optional sort criteria: `descending` (Boolean), `caseInsensitive` (Boolean)
	 * @returns		{Array}
	 */
	conbo.sortOn = function(array, fieldName, options)
	{
		options || (options = {});
		
		if (conbo.isArray(array) && fieldName)
		{
			array.sort(function(a, b)
			{
				var values = [a[fieldName], b[fieldName]];
				
				// Configure
				if (options.descending)
				{
					values.reverse();
				}
				
				if (options.caseInsensitive)
				{
					conbo.forEach(values, function(value, index)
					{
						if (conbo.isString(value)) values[index] = value.toLowerCase();
					});
				}
				
				// Sort
				if (values[0] < values[1]) return -1;
				if (values[0] > values[1]) return 1;
				return 0;
			});
		}
		
		return array;
	};
	
	/**
	 * Performs a comparison of an object against a class, returning true if 
	 * the object is an an instance of the specified class.
	 * 
	 * Unlike the native instanceof, however, this method works with both 
	 * native and user defined classes.
	 * 
	 * @memberof	conbo
	 * @param		{Object}				obj - The class instance
	 * @param		{conbo.Class|function}	clazz - The class to compare against
	 * @example								var b = conbo.instanceOf(69, String);
	 * @example								var b = conbo.instanceOf(user, UserClass);
	 * @returns		{boolean}
	 */
	conbo.instanceOf = function(obj, clazz)
	{
		if (!obj || conbo.isClass(obj) || !clazz) 
		{
			return false;
		}
		
		// Class instances
		
		try
		{
			if (obj instanceof clazz // User defined class 
				|| obj.constructor === clazz) // Primitive class
			{
				return true; 
			}
		}
		catch(e) {}
		
		return false;
	};
	
	/**
	 * Performs a comparison of an object against a class or interface, returning 
	 * true if the object is an an instance of the specified class or an 
	 * implementation of the specified interface
	 * 
	 * @memberof	conbo
	 * @param		{Object}				obj - The object to compare
	 * @param		{conbo.Class|object}	classOrInterface - The class or pseudo-interface to compare against
	 * @param		{boolean}				[strict=true] - Perform a strict interface comparison (default: true)
	 * @example								var b = conbo.is(user, UserClass);
	 * @example								var b = conbo.is(user, IUser);
	 * @example								var b = conbo.is(user, partial, false);
	 * @returns		{boolean}
	 */
	conbo.is = function(obj, classOrInterface, strict)
	{
		if (!obj || conbo.isClass(obj) || !classOrInterface) 
		{
			return false;
		}
		
		// Class instance
		
		if (conbo.instanceOf(obj, classOrInterface))
		{
			return true;
		}
		
		// Pseudo-interface
		
		strict = (strict !== false);
		
		if (conbo.isObject(classOrInterface) && conbo.keys(classOrInterface).length)
		{
			for (var a in classOrInterface)
			{
				if (!(a in obj) || (strict && !conbo.isUndefined(classOrInterface[a]) && !conbo.is(obj[a], classOrInterface[a], strict)))
				{
					return false;
				}
			}
		}
		else
		{
			return false;
		}
		
		return true;
	};
	
	/**
	 * Loads a CSS file and applies it to the DOM
	 * 
	 * @memberof	conbo
	 * @param 		{string}	url			The CSS file's URL
	 * @param 		{string}	[media=all]	The media attribute (defaults to 'all')
	 * @returns		{Promise}
	 */
	conbo.loadCss = function(url, media)
	{
		return new Promise(function(resolve, reject)
		{
			if (!('document' in window) || !!document.querySelector('[href="'+url+'"]'))
			{
				reject();
			}
			
			var link, head;

			link = document.createElement('link');
			link.rel = 'stylesheet';
			link.type = 'text/css';
			link.media = media || 'all';
			link.addEventListener('load', resolve);
			link.addEventListener('error', reject);
			link.href = url;
			
			document
				.querySelector('head')
				.appendChild(link)
				;
		});
	};
	
	/**
	 * Load a JavaScript file and executes it
	 * 
	 * @memberof	conbo
	 * @param 		{string}	url - The JavaScript file's URL
	 * @param 		{Object}	[scope] - The scope in which to run the loaded script
	 * @returns		{Promise}
	 */
	conbo.loadScript = function(url, scope)
	{
		return conbo.httpRequest
		({
			url: url,
			dataType: 'script',
			scope: scope
		});
	};
	
	/*
	 * Property utilities
	 */
	
	/**
	 * Makes the specified properties of an object bindable; if no property 
	 * names are passed, all variables will be made bindable
	 * 
	 * @memberof	conbo
	 * @see 		#makeAllBindable
	 * 
	 * @param		{Object}		obj
	 * @param		{string[]}		[propNames]
	 * @returns		{conbo}
	 */
	conbo.makeBindable = function(obj, propNames)
	{
		if (conbo.isString(propNames))
		{
			propNames = conbo.rest(arguments);
		}

		propNames = conbo.uniq(propNames || conbo.getPublicVariableNames(obj, true));

		propNames.forEach(function(propName)
		{
			__defineBindableProperty(obj, propName);
		});
		
		return this;
	};

	/**
	 * Makes all existing properties of the specified object bindable, and 
	 * optionally creates additional bindable properties for each of the property 
	 * names in the propNames array
	 * 
	 * @memberof	conbo
	 * @see 		#makeBindable
	 * 
	 * @param		{string}		obj
	 * @param		{string[]}		[propNames]
	 * @returns		{conbo}
	 */
	conbo.makeAllBindable = function(obj, propNames)
	{
		if (conbo.isString(propNames))
		{
			propNames = conbo.rest(arguments);
		}
		
		propNames = (propNames || []).concat(conbo.getPublicVariableNames(obj, true));
		conbo.makeBindable(obj, propNames);
		
		return this;
	};
	
	/**
	 * Is the specified property an accessor (defined using a getter and/or setter)?
	 * 
	 * @memberof	conbo
	 * @param		{Object}	Object containing the property
	 * @param		{string}	The name of the property
	 * @returns		{boolean}
	 */
	conbo.isAccessor = function(obj, propName)
	{
		if (obj)
		{
			return !!obj.__lookupGetter__(propName) 
				|| !!obj.__lookupSetter__(propName);
		}
		
		return false;
	};
	
	/**
	 * Is the specified function native?
	 * 
	 * @memberof	conbo
	 * @param		{Function}	func - The function that might be native
	 * @returns		{boolean}	true if it's native, false if it's user defined
	 */
	conbo.isNative = function(value) 
	{
		var toString = Object.prototype.toString;
		var fnToString = Function.prototype.toString;
		var reHostCtor = /^\[object .+?Constructor\]$/;
		var reNative = RegExp('^'+String(toString).replace(/[.*+?^${}()|[\]\/\\]/g, '\\$&').replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$');
		var type = typeof value;
		
		return type == 'function'
			? reNative.test(fnToString.call(value))
			: (value && type == 'object' && reHostCtor.test(toString.call(value))) || false;
	};
	
	/**
	 * Parse a template
	 * 
	 * @memberof	conbo
	 * @param		{string}	template - A string containing property names in {{moustache}} or ${ES2015} format to be replaced with property values
	 * @param		{Object}	data - An object containing the data to be used to populate the template 
	 * @returns		{string}	The populated template
	 */
	conbo.parseTemplate = function(template, data)
	{
		if (!template) return "";
		
		data || (data = {});
		
		return template.replace(/{{(.+?)}}|\${(.+?)}/g, function() 
		{
			try
			{
				var propName = (arguments[1] || arguments[2]).trim();
				var args = propName.split('|');
				var value, parseFunction;
				
				value = data[(args[0] || '').trim()];
				parseFunction = data[(args[1] || '').trim()];
				
				if (!conbo.isFunction(parseFunction)) 
				{
					parseFunction = conbo.value;
				}
				
				return parseFunction(value);
			}
			catch (e) {}
		});
	};
	
	/**
	 * Converts a template string into a pre-populated templating method that can 
	 * be evaluated for rendering.
	 * 
	 * @memberof	conbo
	 * @param		{string}	template - A string containing property names in {{moustache}} or ${ES2015} format to be replaced with property values
	 * @param		{Object}	[defaults] - An object containing default values to use when populating the template
	 * @returns		{Function}	A function that can be called with a data object, returning the populated template
	 */
	conbo.compileTemplate = function(template, defaults)
	{
		return function(data)
		{
			return conbo.parseTemplate(template, conbo.setDefaults(data || {}, defaults));
		}
	};
		
	/**
	 * Serialise an Object as a query string  suitable for appending to a URL 
	 * as GET parameters, e.g. foo=1&bar=2
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj	- The Object to encode
	 * @returns		{string}	The URL encoded string 
	 */
	conbo.toQueryString = function(obj)
	{
		return conbo.keys(obj).map(function(key) {
			var value = obj[key];
			if (value == undefined) value = '';
		    return key + '=' + encodeURIComponent(value);
		}).join('&');
	};
	
	/**
	 * Returns the value of the property matching the specified name, optionally
	 * searching for a case insensitive match. This is useful when extracting 
	 * response headers, where the case of properties such as "Content-Type" 
	 * cannot always be predicted
	 * 
	 * @memberof	conbo
	 * @param		{Object}	obj - The object containing the property
	 * @param		{string}	propName - The property name
	 * @param		{boolean}	[caseSensitive=true] - Whether to search for a case-insensitive match (default: true)
	 * @returns		{*}			The value of the specified property
	 */
	conbo.getValue = function(obj, propName, caseSensitive)
	{
		if (caseSensitive !== false)
		{
			return obj[propName];
		}
		
		for (var a in obj)
		{
			if (a.toLowerCase() == propName.toLowerCase())
			{
				return obj[a];
			}
		}
	};
	
	/**
	 * Prepare data for submission to web services.
	 * 
	 * If no toJSON method is present on the specified Object, this method 
	 * returns a version of the object that can easily be converted into JSON, 
	 * made up of its public properties, with all functions, unenumerable and 
	 * private properties removed.
	 * 
	 * This method can be assigned to an Object or Array as the toJSON method 
	 * for use with JSON.stringify().
	 * 
	 * @memberof	conbo
	 * @param		{*}			obj - Object to convert
	 * @returns		{*}			JSON ready version of the object
	 * 
	 * @example
	 * conbo.jsonify(myObj); // Defers to myObj.toJSON() if it exists
	 * conbo.jsonify.call(myObj); // Ignores myObj.toJSON(), even if it exists
	 * myObj.toJSON = conbo.jsonify; // Assign this method to your Object
	 */
	conbo.jsonify = function(obj)
	{
		if (this != conbo) obj = this;
		
		if (conbo.isObject(obj))
		{
			if (this != obj && 'toJSON' in obj)
			{
				return obj.toJSON();
			}
			else
			{
				if (conbo.isIterable(obj))
				{
					return conbo.map(obj, function(item)
					{
						return conbo.jsonify(item);
					});
				}
				
				var keys = conbo.getPublicVariableNames(obj);
				
				return conbo.pick.apply(conbo, [obj].concat(keys));
			}
		}
		
		return obj;
	};
	
	/*
	 * Logging
	 */
	
	/**
	 * Should Conbo output data to the console when calls are made to loggin methods?
	 * 
	 * @memberof	conbo
	 * @type		{boolean}
	 * @example
	 * conbo.logEnabled = false;
	 * conbo.log('Blah!'); // Nothing will be displayed in the console
	 */
	conbo.logEnabled = true;
	
	/**
	 * @memberof	conbo
	 * @member		{Function}	log - Add a log message to the console
	 * @param		{...*}		values - Values to display in the console	
	 * @returns		{void}
	 * @function
	 */
	
	/**
	 * @memberof	conbo
	 * @member		{Function}	warn - Add a warning message to the console
	 * @param		{...*}		values - Values to display in the console	
	 * @returns		{void}
	 * @function
	 */
	
	/**
	 * @memberof	conbo
	 * @member		{Function}	info - Add information to the console
	 * @param		{...*}		values - Values to display in the console	
	 * @returns		{void}
	 * @function
	 */
	
	/**
	 * @memberof	conbo
	 * @member		{Function}	error - Add an error log message to the console
	 * @param		{...*}		values - Values to display in the console	
	 * @returns		{void}
	 * @function
	 */
	
	var logMethods = ['log','warn','info','error'];
	
	logMethods.forEach(function(method)
	{
		conbo[method] = function()
		{
			if (!console || !conbo.logEnabled) return;
			console[method].apply(console, arguments);
		};
	});
	
})();

/*
 * Functions and utility methods use for manipulating the DOM
 * @author		Neil Rackett
 */

(function()
{
	/**
	 * Initialize Applications in the DOM using the specified namespace
	 * 
	 * By default, Conbo scans the entire DOM, but you can limit the
	 * scope by specifying a root element
	 * 
	 * @memberof	conbo
	 * @param		{conbo.Namespace} namespace
	 * @param		{Element} [rootEl] - Top most element to scan
	 */
	conbo.initDom = function(namespace, rootEl)
	{
		if (!namespace)
		{
			throw new Error('initDom: namespace is undefined');
		}
		
		if (conbo.isString(namespace))
		{
			namespace = conbo(namespace);
		}
		
		rootEl || (rootEl = document.querySelector('html'));
		
		var initDom = function()
		{
			var nodes = conbo.toArray(rootEl.querySelectorAll(':not(.cb-app)'));
			
			nodes.forEach(function(el)
			{
		   		var appName = __ep(el).attributes.cbApp || conbo.toCamelCase(el.tagName, true);
		   		var appClass = namespace[appName];
		   		
		   		if (appClass && conbo.isClass(appClass, conbo.Application))
		   		{
		   			new appClass({el:el});
		   		}
		   	});
		};
		
		conbo.ready(initDom);
		
		return this;	
	};
	
	/**
	 * @private
	 */
	var __observers = [];
	
	/**
	 * @private
	 */
	var __getObserverIndex = function(namespace, rootEl)
	{
		var length = __observers.length;
		
		for (var i=0; i<length; i++)
		{
			var observer = __observers[i];
			
			if (observer[0] == namespace && observer[1] == rootEl)
			{
				return i;
			}
		}
		
		return -1;
	};
	
	/**
	 * Watch the DOM for new Applications using the specified namespace
	 * 
	 * By default, Conbo watches the entire DOM, but you can limit the
	 * scope by specifying a root element
	 * 
	 * @memberof	conbo
	 * @param		{conbo.Namespace} namespace
	 * @param		{Element} [rootEl] - Top most element to observe
	 */
	conbo.observeDom = function(namespace, rootEl)
	{
		if (conbo.isString(namespace))
		{
			namespace = conbo(namespace);
		}
		
		if (__getObserverIndex(namespace, rootEl) != -1)
		{
			return;
		}
		
		rootEl || (rootEl = document.querySelector('html'));
		
		var mo = new conbo.MutationObserver();
		mo.observe(rootEl);
		
		mo.addEventListener(conbo.ConboEvent.ADD, function(event)
		{
			event.nodes.forEach(function(node)
			{
				var ep = __ep(node);
				
				if (!ep.hasClass('cb-app'))
				{
					var appName = ep.cbAttributes.app || conbo.toCamelCase(node.tagName, true);
					
					if (appName && namespace[appName])
					{
						new namespace[appName]({el:node});
					}
				}
			});
		});
		
		__observers.push([namespace, rootEl, mo]);
		
		return this;
	};
	
	/**
	 * Stop watching the DOM for new Applications
	 * 
	 * @memberof	conbo
	 * @param		{conbo.Namespace} namespace
	 * @param		{Element} [rootEl] - Top most element to observe
	 */
	conbo.unobserveDom = function(namespace, rootEl)
	{
		if (conbo.isString(namespace))
		{
			namespace = conbo(namespace);
		}
		
		var i = __getObserverIndex(namespace, rootEl);
		
		if (i != -1)
		{
			var observer = __observers[i];
			
			observer[2].removeEventListener();
			__observers.slice(i,1);
		}
		
		return this;
	};
	
})();
/*
 * CSS styles used by ConboJS
 * @author 	Neil Rackett
 */

if (document && !document.querySelector('#cb-style'))
{
	document.querySelector('head').innerHTML += 
		'<style id="cb-style" type="text/css">'+
			'\n.cb-hide { visibility:hidden !important; }'+
			'\n.cb-exclude { display:none !important; }'+
			'\n.cb-disable { pointer-events:none !important; cursor:default !important; }'+
			'\nspan { font:inherit; color:inherit; }'+
		'\n</style>';
}

/**
 * TypeScript / ES2017 decorator to make a property bindable
 * @memberof			conbo
 * @param	{any}		target - The target object
 * @param	{string}	key - The name of the property
 */
conbo.Bindable = function(target, key)
{
	conbo.makeBindable(target, [key]);
}

/**
 * TypeScript / ES2017 decorator to prepare a property for injection
 * @memberof			conbo
 * @param	{any}		target - The target object
 * @param	{string}	key - The name of the property
 */
conbo.Inject = function(target, key)
{
	if (delete target[key])
	{
		Object.defineProperty(target, key, 
		{
			configurable: true,
			enumerable: true,
			writable: true
		});
	}
}

/**
 * TypeScript / ES2017 decorator for adding Application, View and Glimpse classes a ConboJS namespace to enable auto instantiation
 * @memberof			conbo
 * @param	{string}	[namespace] - The name of the target namespace
 * @param	{string}	[name] - The name to use for this object in the target namespace (required if you use or compile to ES5 and minify your code)
 * @returns	{Function}	Decorator function
 */
conbo.Viewable = function(namespace, name)
{
	switch (arguments.length)
	{
		case 1:
			name = namespace;

			if (name.indexOf('.') !== -1)
			{
				conbo.warn('@Viewable("my.custom.namespace") syntax is no longer valid, please use @Viewable("my.custom.namespace", "MyClassName")');
			}

		case 0:
			namespace = 'default';
			break;
	}

	return function(constructor)
	{
		var imports = {};
		
		name || (name = constructor.name);

		Object.defineProperty(constructor.prototype, '__className', 
		{
			configurable: true,
			enumerable: false,
			writable: true,
			value: conbo.toKebabCase(name)
		});
	
		imports[name] = constructor;
		
		conbo(namespace).import(imports);
		
		return constructor;
	}
};

/**
 * Class
 * Extendable base class from which all others extend
 * @class		Class
 * @memberof	conbo
 * @param 		{Object} options - Object containing initialisation options
 */
conbo.Class = function() 
{
	this.declarations.apply(this, arguments);
	this.preinitialize.apply(this, arguments);
	this.initialize.apply(this, arguments);
};

/**
 * @memberof conbo.Class
 */
conbo.Class.prototype =
{
	/**
	 * Declarations is used to declare instance properties used by this class
	 * @param		{...*}
	 * @returns		{void}
	 */
	declarations: function() {},
	
	/**
	 * Preinitialize is called before any code in the constructor has been run
	 * @param		{...*}
	 * @returns		{void}
	 */
	preinitialize: function() {},
	
	/**
	 * Initialize (entry point) is called immediately after the constructor has completed
	 * @param		{...*}
	 * @returns		{void}
	 */
	initialize: function() {},
	
	/**
	 * Clean everything up ready for garbage collection (you should override in your own classes)
	 * @returns		{void}
	 */
	destroy: function() {},

	/**
	 * Similar to `super` in ActionScript or Java, this property enables 
	 * you to access properties and methods of the super class prototype, 
	 * which is the case of JavaScript is the next prototype up the chain
	 * 
	 * @returns	{*}
	 */
	get supro()
	{
		return Object.getPrototypeOf(Object.getPrototypeOf(this));
	},
	
	/**
	 * Scope all methods of this class instance to this class instance
	 * @param		{...string}	[methodName]	Specific method names to bind (all will be bound if none specified)
	 * @returns 	{this}
	 */
	bindAll: function()
	{
		conbo.bindAll.apply(conbo, [this].concat(conbo.toArray(arguments)));
		return this;
	},
	
	/**
	 * String representation of the current class
	 * @returns		{string}
	 */
	toString: function()
	{
		return 'conbo.Class';
	},
};

__denumerate(conbo.Class.prototype);

/**
 * Extend this class to create a new class
 * 
 * @memberof 	conbo.Class
 * @param		{Object}	[protoProps] - Object containing the new class's prototype
 * @param		{Object}	[staticProps] - Object containing the new class's static methods and properties
 * 
 * @example		
 * var MyClass = conbo.Class.extend
 * ({
 * 	doSomething:function()
 * 	{ 
 * 		conbo.log(':-)');
 * 	}
 * });
 */
conbo.Class.extend = function(protoProps, staticProps)
{
	var parent = this;
	
	/**
	 * The constructor function for the new subclass is either defined by you
	 * (the 'constructor' property in your `extend` definition), or defaulted
	 * by us to simply call the parent's constructor.
	 * @ignore
	 */
	var child = protoProps && conbo.has(protoProps, 'constructor')
		? protoProps.constructor
		: function() { return parent.apply(this, arguments); };
	
	conbo.defineValues(child, parent, staticProps);
	
	/**
	 * Set the prototype chain to inherit from parent, without calling
	 * parent's constructor
	 * @ignore
	 */
	var Surrogate = function(){ this.constructor = child; };
	Surrogate.prototype = parent.prototype;
	child.prototype = new Surrogate();
	
	if (protoProps)
	{
		conbo.defineValues(child.prototype, protoProps);
	}
	
	return child;
};

/**
 * Implements the specified pseudo-interface(s) on the class, copying 
 * the default methods or properties from the partial(s) if they have 
 * not already been implemented.
 * 
 * @memberof	conbo.Class
 * @param		{...Object} interface - Object containing one or more properties or methods to be implemented (an unlimited number of parameters can be passed)
 * 
 * @example
 * var MyClass = conbo.Class.extend().implement(conbo.IInjectable);
 */
conbo.Class.implement = function()
{
	var implementation = conbo.defineDefaults.apply(conbo, conbo.union([{}], arguments)),
		keys = conbo.keys(implementation),
		prototype = this.prototype;
	
	conbo.defineDefaults(this.prototype, implementation);
	
	var rejected = conbo.reject(keys, function(key)
	{
		return prototype[key] !== conbo.notImplemented;
	});
	
	if (rejected.length)
	{
		throw new Error(prototype.toString()+' does not implement the following method(s): '+rejected.join(', '));
	}
	
	return this;
};

/**
 * Conbo class
 * 
 * Base class for most Conbo framework classes that calls preinitialize before 
 * the constructor and initialize afterwards, populating the options parameter
 * with an empty Object if no parameter is passed and automatically making all
 * properties bindable.
 * 
 * @class		ConboClass
 * @memberof	conbo
 * @augments	conbo.Class
 * @author		Neil Rackett
 * @param 		{Object}	options - Class configuration object
 */
conbo.ConboClass = conbo.Class.extend(
/** @lends conbo.ConboClass.prototype */
{
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param 	{Object|conbo.Context}	[options] - Options object
	 */
	constructor: function(options)
	{
		var args = conbo.toArray(arguments);
		
		if (args[0] === undefined) 
		{
			args[0] = {};
		}
		else if (args[0] instanceof conbo.Context && conbo.is(this, conbo.IInjectable, false)) 
		{
			args[0] = args[0].addTo();
		}
		
		this.declarations.apply(this, args);
		
		if (!!args[0].context)
		{
			this.context = args[0].context;
		}
	
		if (this instanceof conbo.EventDispatcher)
		{
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.PREINITIALIZE));
		}

		this.preinitialize.apply(this, args);
		this.__construct.apply(this, args);
		
		if (this instanceof conbo.EventDispatcher)
		{
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.INITIALIZE));
		}

		this.initialize.apply(this, args);
		conbo.makeAllBindable(this, this.bindable);
		this.__postInitialize.apply(this, args);
		
		if (this instanceof conbo.EventDispatcher)
		{
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.INIT_COMPLETE));
		}		
	},
	
	toString: function()
	{
		return 'conbo.ConboClass';
	},
	
	/**
	 * @private
	 */
	__construct: function() {},
	
	/**
	 * @private
	 */
	__postInitialize: function() {}
	
});

__denumerate(conbo.ConboClass.prototype);

/**
 * Conbo namespaces enable you to create modular, encapsulated code, similar to
 * how you might use packages in languages like Java or ActionScript.
 * 
 * By default, namespaces will automatically call initDom() when the HTML page
 * has finished loading.
 * 
 * @class		Namespace
 * @memberof	conbo
 * @augments	conbo.Class
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options
 */
conbo.Namespace = conbo.ConboClass.extend(
/** @lends conbo.Namespace.prototype */
{
	__construct: function()
	{
		var readyHandler = function()
		{
			if (document && this.autoInit !== false)
			{
				this.initDom();
			}
		};
		
		conbo.ready(readyHandler, this);
	},
	
	/**
	 * Search the DOM and initialize Applications contained in this namespace
	 * 
	 * @param 	{Element} 	[rootEl] - The root element to initialize
	 * @returns {this}
	 */
	initDom: function(rootEl)
	{
		conbo.initDom(this, rootEl);
		return this;
	},
	
	/**
	 * Watch the DOM and automatically initialize Applications contained in 
	 * this namespace when an element with the appropriate cb-app attribute
	 * is added.
	 * 
	 * @param 	{Element} 	[rootEl] - The root element to initialize
	 * @returns {this}
	 */
	observeDom: function(rootEl)
	{
		conbo.observeDom(this, rootEl);
		return this;
	},
	
	/**
	 * Stop watching the DOM for Applications
	 * 
	 * @param 	{Element} 	[rootEl] - The root element to initialize
	 * @returns {this}
	 */
	unobserveDom: function(rootEl)
	{
		conbo.unobserveDom(this, rootEl);
		return this;
	},
	
	/**
	 * Add classes, properties or methods to the namespace. Using this method
	 * will not overwrite existing items of the same name.
	 * 
	 * @param 	{Object}			obj - An object containing items to add to the namespace 
	 * @returns	{conbo.Namespace}	This Namespace instance
	 */
	import: function(obj)
	{
		conbo.setDefaults.apply(conbo, [this].concat(conbo.toArray(arguments)));
		return this;
	},
	
});

/**
 * Partial class that enables the ConboJS framework to add the application
 * specific Context class instance and inject specified dependencies 
 * (properties of undefined value which match registered singletons); should
 * be used via the Class.implement method
 * 
 * @memberof	conbo
 * @example		var C = conbo.Class.extend().implement(conbo.IInjectable);
 * @example		conbo.defineValues(classInstance, conbo.IInjectable);
 * @author		Neil Rackett
 */
conbo.IInjectable =
{
	/**
	 * The current class instance's context
	 * @type	{conbo.Context}
	 */
	get context()
	{
		return this.__context;
	},
	
	set context(value)
	{
		if (value == this.__context) return;
		
		if (value instanceof conbo.Context) 
		{
			value.inject(this);
		}
		
		this.__context = value;
		
		__denumerate(this, '__context');
	}
	
};

/**
 * Event class
 * 
 * Base class for all events triggered in ConboJS
 * 
 * @class		Event
 * @memberof	conbo
 * @augments	conbo.Class
 * @author		Neil Rackett
 * @param 		{string}	type - The type of event this object represents
 */
conbo.Event = conbo.Class.extend(
/** @lends conbo.Event.prototype */
{
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param 	{string} type - The type of event this class instance represents
	 */
	constructor: function(type)
	{
		this.preinitialize.apply(this, arguments);
		
		if (conbo.isString(type)) 
		{
			this.type = type;
		}
		else 
		{
			conbo.defineDefaults(this, type);
		}
		
		if (!this.type) 
		{
			throw new Error('Invalid or undefined event type');
		}
		
		this.initialize.apply(this, arguments);
	},
	
	/**
	 * Initialize: Override this!
	 * @param 	{string} type - The type of event this class instance represents
	 * @param 	{*} data - Data to store in the event's data property
	 */
	initialize: function(type, data)
	{
		this.data = data;
	},
	
	/**
	 * Create an identical clone of this event
	 * @returns 	{conbo.Event}	A clone of this event
	 */
	clone: function()
	{
		return conbo.clone(this);
	},
	
	/**
	 * Prevent whatever the default framework action for this event is
	 * @returns	{conbo.Event}	A reference to this event instance 
	 */
	preventDefault: function() 
	{
		this.defaultPrevented = true;
		return this;
	},
	
	/**
	 * Not currently used
	 * @returns	{conbo.Event}	A reference to this event instance
	 */
	stopPropagation: function() 
	{
		this.cancelBubble = true;
		
		return this;
	},
	
	/**
	 * Keep the rest of the handlers from being executed
	 * @returns	{conbo.Event}	A reference to this event 
	 */
	stopImmediatePropagation: function() 
	{
		this.immediatePropagationStopped = true;
		this.stopPropagation();
		
		return this;
	},
	
	toString: function()
	{
		return 'conbo.Event';
	}
},
/** @lends conbo.Event */
{
	ALL: '*',
});

__denumerate(conbo.Event.prototype);

/**
 * DataEvent class
 * 
 * Event with data property to enable arbitrary data to be passed when an event is dispatched
 * 
 * @class		DataEvent
 * @memberof	conbo
 * @augments	conbo.Event
 * @author		Neil Rackett
 */
conbo.DataEvent = conbo.Event.extend();

/**
 * conbo.Event
 * 
 * Default event class for events fired by ConboJS
 * 
 * For consistency, callback parameters of Backbone.js derived classes 
 * are event object properties in ConboJS
 * 
 * @class		ConboEvent
 * @memberof	conbo
 * @augments	conbo.Event
 * @author		Neil Rackett
 * @param 		{string}	type - The type of event this object represents
 * @param 		{Object}	options - Properties to be added to this event object
 */
conbo.ConboEvent = conbo.Event.extend(
/** @lends conbo.ConboEvent.prototype */
{
	/**
	 * Initialize the ConboEvent class instance
	 * @param	{string}	type - The type of event this class instance represents
	 * @param	{Object}	[options] - Object containing additional properties to add to this class instance	
	 */
	initialize: function(type, options)
	{
		conbo.defineDefaults(this, options);
	},
	
	toString: function()
	{
		return 'conbo.ConboEvent';
	}
},
/** @lends conbo.ConboEvent */
{
	/** 
	 * Special event used to listed for all event types 
	 * 
	 * @event			conbo.ConboEvent#ALL
     * @type 			{conbo.ConboEvent}
	 */
	ALL:				'*',

	/**
	 * Something has changed (also 'change:[name]')
	 * 
	 * @event			conbo.ConboEvent#CHANGE
     * @type 			{conbo.ConboEvent}
     * @property		{string} property - The name of the property that changed
     * @property		{*} value - The new value of the property
	 */
	CHANGE:				'change',
	
	/** 
	 * Something was added
	 * 
	 * @event			conbo.ConboEvent#ADD
     * @type 			{conbo.ConboEvent}
	 */
	ADD:				'add', 				

	/**
	 * Something was removed
	 * 
	 * @event			conbo.ConboEvent#REMOVE
     * @type 			{conbo.ConboEvent}
	 */
	REMOVE:				'remove',

	/**
	 * The route has changed (also 'route:[name]')
	 * 
	 * @event			conbo.ConboEvent#ROUTE
     * @type 			{conbo.ConboEvent}
     * @property		{conbo.Router}	router - The router that handled the route change
     * @property		{RegExp} 		route - The route that was followed
     * @property		{string} 		name - The name assigned to the route
     * @property		{Array} 		parameters - The parameters extracted from the route
     * @property		{string} 		path - The new path 
	 */
	ROUTE:				'route', 			

	/** 
	 * Something has started
	 * 
	 * @event			conbo.ConboEvent#START
     * @type 			{conbo.ConboEvent}
	 */
	START:				'start',

	/**
	 * Something has stopped
	 * 
	 * @event			conbo.ConboEvent#STOP
     * @type 			{conbo.ConboEvent}
	 */
	STOP:				'stop',
	
	/**
	 * A template is ready to use
	 * 
	 * @event			conbo.ConboEvent#TEMPLATE_COMPLETE
     * @type 			{conbo.ConboEvent}
	 */
	TEMPLATE_COMPLETE:	'templateComplete',

	/** 
	 * A template error has occurred
	 *  
	 * @event			conbo.ConboEvent#TEMPLATE_ERROR
     * @type 			{conbo.ConboEvent}
	 */
	TEMPLATE_ERROR:		'templateError',

	/** 
	 * Something has been bound
	 *  
	 * @event			conbo.ConboEvent#BIND
     * @type 			{conbo.ConboEvent}
	 */
	BIND:				'bind',

	/** 
	 * Something has been unbound
	 *  
	 * @event			conbo.ConboEvent#UNBIND
     * @type 			{conbo.ConboEvent}
	 */
	UNBIND:				'unbind',			

	/** 
	 * Something is about to initialize
	 * 
	 * @event			conbo.ConboEvent#PREINITIALIZE
     * @type 			{conbo.ConboEvent}
	 */
	PREINITIALIZE:	'preinitialize',

	/** 
	 * Something is initializing
	 * 
	 * @event			conbo.ConboEvent#INITIALIZE
     * @type 			{conbo.ConboEvent}
	 */
	INITIALIZE:		'initialize',

	/** 
	 * Something has finished initializing
	 * 
	 * @event			conbo.ConboEvent#INIT_COMPLETE
     * @type 			{conbo.ConboEvent}
	 */
	INIT_COMPLETE:	'initComplete',

	/** 
	 * Something has been created and it's ready to use
	 * 
	 * @event			conbo.ConboEvent#CREATION_COMPLETE
     * @type 			{conbo.ConboEvent}
	 */
	CREATION_COMPLETE:	'creationComplete',
	
	/** 
	 * Something has been detached
	 * 
	 * @event			conbo.ConboEvent#DETACH
     * @type 			{conbo.ConboEvent}
	 */
	DETACH:				'detach',
	
	/** 
	 * A result has been received
	 *  
	 * @event			conbo.ConboEvent#RESULT
     * @type 			{conbo.ConboEvent}
     * @property		{*} result - The data received 
	 */
	RESULT:				'result',
	
	/** 
	 * A fault has occurred
	 *  
	 * @event			conbo.ConboEvent#FAULT
     * @type 			{conbo.ConboEvent}
     * @property		{*} fault - The fault received 
	 */
	FAULT:				'fault',			
	
});

(function()
{
	/**
	 * @private
	 */
	var EventDispatcher__addEventListener = function(type, handler, scope, priority, once)
	{
		if (type == '*') type = 'all';
		if (!this.__queue) __definePrivateProperty(this, '__queue', {});
		
		if (!this.hasEventListener(type, handler, scope))
		{
			if (!(type in this.__queue)) this.__queue[type] = [];
			this.__queue[type].push({handler:handler, scope:scope, once:!!once, priority:~~priority});
			this.__queue[type].sort(function(a,b){return b.priority-a.priority;});
		}
	};
	
	/**
	 * @private
	 */
	var EventDispatcher__removeEventListener = function(type, handler, scope)
	{
		if (type == '*') type = 'all';
		if (!this.__queue) return;
		
		var queue;
		var i;
		var self = this;
		
		var removeFromQueue = function(queue, key)
		{
			for (i=0; i<queue.length; i++)
			{
				if ((!handler || handler == queue[i].handler) && (!scope || scope == queue[i].scope))
				{
					queue.splice(i--, 1);
				}
			}

			if (!queue.length)
			{
				delete self.__queue[key];
			}
		};
		
		if (type in self.__queue)
		{
			queue = self.__queue[type];
			removeFromQueue(queue, type);
		}
		else if (!type)
		{
			conbo.forEach(self.__queue, removeFromQueue, self);
		}
	};
	
	/**
	 * Event Dispatcher
	 * 
	 * Event model designed to bring events into line with DOM events and those 
	 * found in HTML DOM, jQuery and ActionScript 2 & 3, offering a more 
	 * predictable, object based approach to event dispatching and handling
	 * 
	 * Should be used as the base class for any class that won't be used for 
	 * data binding
	 * 
	 * @class		EventDispatcher
	 * @memberof	conbo
	 * @augments	conbo.Class
	 * @augments	conbo.IInjectable
	 * @author		Neil Rackett
	 * @param 		{Object} options - Object containing optional initialisation options, including 'context'
	 */
	conbo.EventDispatcher = conbo.ConboClass.extend(
	/** @lends conbo.EventDispatcher.prototype */
	{
		/**
		 * Add a listener for a particular event type
		 * 
		 * @param 	{string}		type - Type of event ('change') or events ('change blur')
		 * @param 	{Function}		handler - Function that should be called
		 * @param 	{Object}		[scope] - Options object (recommended) or the scope in which to run the event handler (deprecated)
		 * @param 	{number}		[priority=0] - The event handler's priority when the event is dispatached (deprecated)
		 * @param 	{boolean}		[once=false] - Should the event listener automatically be removed after it has been called once? (deprecated)
		 * @returns	{conbo.EventDispatcher}	A reference to this class instance 
		 */
		addEventListener: function(type, handler, scope, priority, once)
		{
			if (!type) throw new Error('Event type undefined');
			if (!handler || !conbo.isFunction(handler)) throw new Error('Event handler is undefined or not a function');
	
			if (scope)
			{
				// Options object?
				if (conbo.isPlainObject(scope))
				{
					var options = scope;

					scope = options.scope;
					priority = options.priority || 0;
					once = options.once || false;
				}
				else
				{
					__deprecated('addEventListener(type, handler, scope, priority, once)', 'addEventListener(type, handler, options)');
				}
			}

			if (conbo.isString(type)) type = type.split(' ');
			if (conbo.isArray(type)) conbo.forEach(type, function(value, index, list)
			{
				EventDispatcher__addEventListener.call(this, value, handler, scope, priority, once); 
			},
			this);
			
			return this;
		},
		
		/**
		 * Remove a listener for a particular event type
		 * 
		 * @param 	{string}		[type] - Type of event ('change') or events ('change blur'), if not specified, all listeners will be removed 
		 * @param 	{Function}		[handler] - Function that should be called, if not specified, all listeners of the specified type will be removed
		 * @param 	{Object}		[scope] - Options object (recommended) or the scope in which to run the event handler (deprecated)
		 * @returns	{conbo.EventDispatcher}	A reference to this class instance 
		 */
		removeEventListener: function(type, handler, scope)
		{
			if (!arguments.length)
			{
				__definePrivateProperty(this, '__queue', {});
				return this;
			}

			// Options object?
			if (conbo.isPlainObject(scope))
			{
				var options = scope;
				scope = options.scope;
			}			

			if (conbo.isString(type)) type = type.split(' ');
			if (!conbo.isArray(type)) type = [undefined];
			
			conbo.forEach(type, function(value, index, list) 
			{
				EventDispatcher__removeEventListener.call(this, value, handler, scope); 
			}, 
			this);
			
			return this;
		},
		
		/**
		 * Does this object have an event listener of the specified type?
		 * 
		 * @param 	{string}	type - Type of event (e.g. 'change') 
		 * @param 	{Function}	[handler] - Function that should be called
		 * @param 	{Object}	[scope] - Options object (recommended) or the scope in which to run the event handler (deprecated)
		 * @returns	{boolean}	True if this object has the specified event listener, false if it does not
		 */
		hasEventListener: function(type, handler, scope)
		{
			if (!this.__queue || !(type in this.__queue) || !this.__queue[type].length)
			{
				return false;
			}

			// Options object?
			if (conbo.isPlainObject(scope))
			{
				var options = scope;
				scope = options.scope;
			}
			
			var filtered = this.__queue[type].filter(function(queued)
			{
				return (!handler || queued.handler == handler) && (!scope || queued.scope == scope);
			});

			return !!filtered.length;
		},
		
		/**
		 * Dispatch the event to listeners
		 * @param {conbo.Event} 	event - The event to dispatch
		 * @returns	{conbo.EventDispatcher}	A reference to this class instance 
		 */
		dispatchEvent: function(event)
		{
			if (!(event instanceof conbo.Event))
			{
				throw new Error('event parameter is not an instance of conbo.Event');
			}
			
			if (!this.__queue || (!(event.type in this.__queue) && !this.__queue.all)) return this;
			
			if (!event.target) event.target = this;
			event.currentTarget = this;
			
			var queue = conbo.union(this.__queue[event.type] || [], this.__queue.all || []);
			if (!queue || !queue.length) return this;
			
			for (var i=0, length=queue.length; i<length; ++i)
			{
				var value = queue[i];
				var returnValue = value.handler.call(value.scope || this, event);
				if (value.once) EventDispatcher__removeEventListener.call(this, event.type, value.handler, value.scope);
				if (event.immediatePropagationStopped) break;
			}
			
			return this;
		},
		
		/**
		 * Dispatch a change event for one or more changed properties
		 * @param {string}	propName - The name of the property that has changed
		 * @returns	{conbo.EventDispatcher}	A reference to this class instance 
		 */
		dispatchChange: function(propName)
		{
			conbo.forEach(arguments, function(propName)
			{
				__dispatchChange(this, propName);
			},
			this);
			
			return this;
		},
	
		toString: function()
		{
			return 'conbo.EventDispatcher';
		},
		
	}).implement(conbo.IInjectable);

	__denumerate(conbo.EventDispatcher.prototype);
	
})();

/**
 * Event Proxy
 * 
 * Standardises the adding and removing of event listeners across DOM elements,
 * Conbo EventDispatchers and jQuery instances 
 * 
 * @class		EventProxy
 * @memberof	conbo
 * @augments	conbo.Class
 * @author 		Neil Rackett
 * @param 		{Object} eventDispatcher - Element, EventDispatcher or jQuery object to be proxied
 */
conbo.EventProxy = conbo.Class.extend(
/** @lends conbo.EventProxy.prototype */
{
	constructor: function(obj)
	{
		this.__obj = obj;
	},
	
	/**
	 * Add a listener for a particular event type
	 * 
	 * @param 	{string}			type - Type of event ('change') or events ('change blur')
	 * @param 	{Function}			handler - Function that should be called
	 * @returns	{conbo.EventProxy}	A reference to this class instance 
	 */
	addEventListener: function(type, handler)
	{
		var obj = this.__obj;
		
		if (obj)
		{
			switch (true)
			{
				// TODO Remove the last tiny piece of jQuery support?
				case conbo.$ && obj instanceof conbo.$:
				case window.$ && obj instanceof window.$:
				{
					obj.on(type, handler);
					break;
				}
				
				case obj instanceof conbo.EventDispatcher:
				{
					obj.addEventListener(type, handler);
					break;
				}
				
				default:
				{
					var types = type.split(' ');
					
					types.forEach(function(type)
					{
						obj.addEventListener(type, handler);
					});
				}
			}
		}
		
		return this;
	},
	
	/**
	 * Remove a listener for a particular event type
	 * 
	 * @param 	{string}			type - Type of event ('change') or events ('change blur')
	 * @param 	{Function}			handler - Function that should be called
	 * @returns	{conbo.EventProxy}	A reference to this class instance 
	 */
	removeEventListener: function(type, handler)
	{
		var obj = this.__obj;
		
		if (obj)
		{
			switch (true)
			{
				// TODO Remove the last tiny piece of jQuery support?
				case conbo.$ && obj instanceof conbo.$:
				case window.$ && obj instanceof window.$:
				{
					obj.off(type, handler);
					break;
				}
				
				case obj instanceof conbo.obj:
				{
					obj.removeEventListener(type, handler);
					break;
				}
				
				default:
				{
					var types = type.split(' ');
					
					types.forEach(function(type)
					{
						obj.removeEventListener(type, handler);
					});
				}
			}
		}
		
		return this;
	},
	
});

/**
 * Headless Application 
 * 
 * Base class for applications that don't require DOM, e.g. Node.js
 * 
 * @class		HeadlessApplication
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options
 */
conbo.HeadlessApplication = conbo.EventDispatcher.extend(
/** @lends conbo.HeadlessApplication.prototype */
{
	/**
	 * Default context class to use
	 * You'll normally want to override this with your own
	 */
	contextClass: conbo.Context,
	
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 */
	__construct: function(options)
	{
		options = conbo.clone(options || {});
		options.app = this;
		
		this.context = new this.contextClass(options);
	},
	
	toString: function()
	{
		return 'conbo.HeadlessApplication';
	}
	
}).implement(conbo.IInjectable);

__denumerate(conbo.HeadlessApplication.prototype);

/**
 * conbo.Context
 * 
 * This is your application's event bus and dependency injector, and is
 * usually where all your models and web service classes are registered,
 * using mapSingleton(...), and Command classes are mapped to events 
 * 
 * @class		Context
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options, including 'app' (Application) and 'namespace' (Namespace) 
 */
conbo.Context = conbo.EventDispatcher.extend(
/** @lends conbo.Context.prototype */
{
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 */
	__construct: function(options)
	{
		__definePrivateProperties(this, 
		{
			__commands: options.commands || {},
			__singletons: options.singletons || {},
			__app: options.app,
			__namespace: options.namespace || (options.app && options.app.namespace),
			__parentContext: options.context
		});
		
		this.addEventListener(conbo.Event.ALL, this.__allHandler);
	},
	
	/**
	 * The Application instance associated with this context
	 * @returns {conbo.Application}
	 */
	get app()
	{
		return this.__app;
	},
	
	/**
	 * The Namespace this context exists in
	 * @returns {conbo.Namespace}
	 */
	get namespace()
	{
		return this.__namespace;
	},
	
	/**
	 * If this is a subcontext, this is a reference to the Context that created it
	 * @returns {conbo.Context}
	 */
	get parentContext()
	{
		return this.__parentContext;
	},
	
	/**
	 * Create a new subcontext that shares the same application
	 * and namespace as this one
	 * 
	 * @param	{class} [contextClass] - The context class to use (default: conbo.Context)
	 * @param	{boolean} [cloneSingletons] - Should this Context's singletons be duplicated on the new subcontext? (default: false)
	 * @param	{boolean} [cloneCommands] - Should this Context's commands be duplicated on the new subcontext? (default: false)
	 * @returns {conbo.Context}
	 */
	createSubcontext: function(contextClass, cloneSingletons, cloneCommands)
	{
		contextClass || (contextClass = conbo.Context);
		return new contextClass
		({
			context: this,
			app: this.app,
			namespace: this.namespace,
			commands: cloneCommands ? conbo.clone(this.__commands) : undefined,
			singletons: cloneSingletons ? conbo.clone(this.__singletons) : undefined
		});
	},
	
	/**
	 * Map specified Command class the given event
	 * @param	{string}	eventType - The name of the event
	 * @param	{class}		commandClass - The command class to instantiate when the event is dispatched
	 */
	mapCommand: function(eventType, commandClass)
	{
		if (!eventType) throw new Error('eventType cannot be undefined');
		if (!commandClass) throw new Error('commandClass for '+eventType+' cannot be undefined');
		
		if (this.__commands[eventType] && this.__commands[eventType].indexOf(commandClass) != -1)
		{
			return this;
		}
		
		this.__commands[eventType] = this.__commands[eventType] || [];
		this.__commands[eventType].push(commandClass);
		
		return this;
	},
	
	/**
	 * Unmap specified Command class from given event
	 */
	unmapCommand: function(eventType, commandClass)
	{
		if (!eventType) throw new Error('eventType cannot be undefined');
		
		if (commandClass === undefined)
		{
			delete this.__commands[eventType];
			return this;
		}
		
		if (!this.__commands[eventType]) return;
		var index = this.__commands[eventType].indexOf(commandClass);
		if (index == -1) return;
		this.__commands[eventType].splice(index, 1);
		
		return this;
	},
	
	/**
	 * Map class instance to a property name
	 * 
	 * To inject a property into a class, register the property name
	 * with the Context and declare the value as undefined in your class
	 * to enable it to be injected at run time
	 * 
	 * @example		context.mapSingleton('myProperty', MyModel);
	 * @example		myProperty: undefined
	 */
	mapSingleton: function(propertyName, singletonClass)
	{
		if (!propertyName) throw new Error('propertyName cannot be undefined');
		
		if (singletonClass === undefined)
		{
			conbo.warn('singletonClass for '+propertyName+' is undefined');
		}

		if (conbo.isClass(singletonClass))
		{
			var args = conbo.rest(arguments);

			if (args.length == 1 && singletonClass.prototype instanceof conbo.ConboClass)
			{
				args.push(this);
			}

			this.__singletons[propertyName] = new (Function.prototype.bind.apply(singletonClass, args))
		}
		else
		{
			this.__singletons[propertyName] = singletonClass;
		}
			
		return this;
	},
	
	/**
	 * Unmap class instance from a property name
	 */
	unmapSingleton: function(propertyName)
	{
		if (!propertyName) throw new Error('propertyName cannot be undefined');
		
		if (!this.__singletons[propertyName]) return;
		delete this.__singletons[propertyName];
		
		return this;
	},
	
	/**
	 * Map constant value to a property name
	 * 
	 * To inject a constant into a class, register the property name
	 * with the Context and declare the property as undefined in your 
	 * class to enable it to be injected at run time
	 * 
	 * @example		context.mapConstant('MY_VALUE', 123);
	 * @example		MY_VALUE: undefined
	 */
	mapConstant: function(propertyName, value)
	{
		return this.mapSingleton(propertyName, value);
	},
	
	/**
	 * Unmap constant value from a property name
	 */
	unmapConstant: function(propertyName)
	{
		return this.unmapSingleton(propertyName);
	},
	
	/**
	 * Add this Context to the specified Object, or create an object with a 
	 * reference to this Context
	 */
	addTo: function(obj)
	{
		return conbo.defineValues(obj || {}, {context:this});
	},
	
	/**
	 * Inject constants and singleton instances into specified object
	 * 
	 * @deprecated					Use inject()
	 * @param	{*}			obj 	The object to inject singletons into
	 */
	injectSingletons: function(obj)
	{
		__deprecated('injectSingletons', 'inject');

		this.inject(obj);
		return this;
	},
	
	/**
	 * Inject constants and singleton instances into specified object
	 * 
	 * @param	{*}			obj 	The object to inject singletons into
	 * @param	{...string}	names 	Names of properties to inject (optional)
	 */
	inject: function(obj)
	{
		var scope = this;
		var names;
		var hasNames = arguments.length > 1;
		
		if (hasNames)
		{
			names = conbo.rest(arguments);
		}

		for (var a in scope.__singletons)
		{
			if (hasNames ? names.indexOf(a) != -1 : a in obj)
			{
				(function(value)
				{
					Object.defineProperty(obj, a,
					{
						configurable: true,
						get: function() { return value; }
					});
				})(scope.__singletons[a]);
			}
		}
		
		return obj;
	},

	/**
	 * Set constants and singleton instances on the specified object to undefined
	 * 
	 * @deprecated					Use uninject()
	 * @param	{*}	obj 	The object to remove singletons from
	 */
	uninjectSingletons: function(obj)
	{
		__deprecated('uninjectSingletons', 'uninject');
		
		this.uninject(obj);
		return this;
	},
	
	/**
	 * Set constants and singleton instances on the specified object to undefined
	 * 
	 * @param	{*}	obj 	The object to remove singletons from
	 */
	uninject: function(obj)
	{
		var scope = this;
		var names;
		var hasNames = arguments.length > 1;
		
		if (hasNames)
		{
			names = conbo.rest(arguments);
		}

		for (var a in scope.__singletons)
		{
			if (hasNames ? names.indexOf(a) != -1 : a in obj)
			{
				Object.defineProperty(obj, a,
				{
					configurable: true,
					value: undefined
				});
			}
		}

		return obj;
	},

	/**
	 * Clears all commands and singletons, and removes all listeners
	 */
	destroy: function()
	{
		conbo.assign(this,
		{
			__commands: undefined,
			__singletons: undefined,
			__app: undefined,
			__namespace: undefined,
			__parentContext: undefined
		});

		this.removeEventListener();

		return this;
	},
	
	toString: function()
	{
		return 'conbo.Context';
	},
	
	/**
	 * @private
	 */
	__allHandler: function(event)
	{
		var commands = conbo.union(this.__commands.all || [], this.__commands[event.type] || []);
		if (!commands.length) return;
		
		conbo.forEach(commands, function(commandClass, index, list)
		{
			this.__executeCommand(commandClass, event);
		}, 
		this);
	},
	
	/**
	 * @private
	 */
	__executeCommand: function(commandClass, event)
	{
		var command, options;
		
		options = {event:event};
		
		command = new commandClass(this.addTo(options));
		command.execute();
		command = null;
		
		return this;
	},
	
});

__denumerate(conbo.Context.prototype);

/**
 * conbo.Hash
 * 
 * A Hash is a bindable object of associated keys and values.
 * 
 * This class implements the Web Storage API, with the exception of the `length` property.
 * 
 * @class		Hash
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing optional initialisation options, including 'source' (object) containing initial values
 * @fires		conbo.ConboEvent#CHANGE
 */
conbo.Hash = conbo.EventDispatcher.extend(
/** @lends conbo.Hash.prototype */
{
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 * @private
	 */
	__construct: function(options)
	{
		// If this Hash has an external source, ensure it's kept up-to-date
		if (options.source)
		{
			var changeHandler = function(event)
			{
				options.source[event.property] = event.value;
			};

			this.addEventListener('change', changeHandler, {scope:this});
		}

		conbo.assign(this, conbo.setDefaults({}, options.source || {}, this._defaults));
		delete this._defaults;
	},

	/**
	 * Returns a version of this object that can easily be converted into JSON
	 * @function
	 * @returns	{Object}
	 */
	toJSON: conbo.jsonify,
	
	toString: function()
	{
		return 'conbo.Hash';
	},

	// Web Storage API

	// /**
	//  * The read-only length property returns the number of data items stored in this Hash
	//  */
	// TODO Can we implement length without messing up JSON?
	// get length()
	// {
	// 	return conbo.keys(this.toJSON()).length;
	// },

	/**
	 * [Web Storage API] When passed a number n, this method will return the name of the nth key in the Hash
	 * @param {number} index
	 */
	key: function(index)
	{
		var keys = conbo.keys(this.toJSON()).sort();
		return keys[index];
	},
	
	/**
	 * [Web Storage API] When passed a key name, will return that key's value
	 * @param {string} keyName
	 */
	getItem: function(keyName)
	{
		return this[keyName];
	},
	
	/**
	 * [Web Storage API] When passed a key name and value, will add that key to the Hash, or update that key's value if it already exists
	 * @param {string} keyName
	 * @param {*} keyValue
	 */
	setItem: function(keyName, keyValue)
	{
		if (!conbo.isAccessor(this, keyName))
		{
			conbo.makeBindable(this, [keyName]);
		}

		this[keyName] = keyValue;
	},

	/**
	 * [Web Storage API] When passed a key name, will remove that key from the Hash (or set it to undefined if it cannot be deleted)
	 * @param {string} keyName
	 */
	removeItem: function(keyName)
	{
		if (!(keyName in this)) return;

		if (!(delete this[keyName]))
		{
			this.setItem(keyName, undefined);
		}
	},

	/**
	 * [Web Storage API] When invoked, will empty all keys out of the Hash (or set them to undefined if they cannot be deleted)
	 */
	clear: function()
	{
		var keys = conbo.keys(this.toJSON());

		for (var keyName in keys)
		{
			this.removeItem(keyName);
		}
	},

});

__denumerate(conbo.Hash.prototype);

/**
 * A persistent Hash that stores data in LocalStorage or Session
 * 
 * @class		LocalHash
 * @memberof	conbo
 * @augments	conbo.Hash
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options, including 'name' (string), 'session' (Boolean) and 'source' (object) containing default values; see Hash for other options
 * @fires		conbo.ConboEvent#CHANGE
 */
conbo.LocalHash = conbo.Hash.extend(
/** @lends conbo.LocalHash.prototype */
{
	__construct: function(options)
	{
		var defaultName = 'ConboLocalHash';
		
		options = conbo.defineDefaults(options, {name:defaultName});
		
		var name = options.name;
		
		var storage = options.session
			? window.sessionStorage
			: window.localStorage;
		
		if (name == defaultName)
		{
			conbo.warn('No name specified for '+this.toString()+', using "'+defaultName+'"');
		}
		
		var getLocal = function()
		{
			return name in storage 
				? JSON.parse(storage.getItem(name) || '{}')
				: options.source || {};
		};
		
		// Sync with LocalStorage
		this.addEventListener(conbo.ConboEvent.CHANGE, function(event)
  		{
  			storage.setItem(name, JSON.stringify(this.toJSON()));
  		}, 
  		{scope:this, priority:1000});
		
		options.source = getLocal();
		
		conbo.Hash.prototype.__construct.call(this, options);
	},
	
	/**
	 * Immediately writes all data to local storage. If you don't use this method, 
	 * Conbo writes the data the next time it detects a change to a bindable property.
	 */
	flush: function()
	{
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CHANGE));
		return this;
	},
	
	toString: function()
	{
		return 'conbo.LocalHash';
	}
	
});

__denumerate(conbo.LocalHash.prototype);

/**
 * A bindable Array wrapper that can be used when you don't require 
 * web service connectivity.
 * 
 * Plain objects will automatically be converted into an instance of 
 * the specified `itemClass` when added to a List, and the appropriate
 * events dispatched if the items it contains are changed or updated.
 * 
 * This class implements the Web Storage API.
 * 
 * @class		List
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing optional initialisation options, including `source` (array), `context` (Context) and `itemClass` (Class)
 * @fires		conbo.ConboEvent#CHANGE
 * @fires		conbo.ConboEvent#ADD
 * @fires		conbo.ConboEvent#REMOVE
 */
conbo.List = conbo.EventDispatcher.extend(
/** @lends conbo.List.prototype */
{
	/**
	 * The class to use for items in this list (plain JS objects will 
	 * automatically be wrapped using this class), defaults to conbo.Hash
	 */
	itemClass: conbo.Hash,
	
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 */
	__construct: function(options) 
	{
		this.addEventListener(conbo.ConboEvent.ADD, this.__updateArrayAccess, {scope:this, priority:9999})
			.addEventListener(conbo.ConboEvent.REMOVE, this.__updateArrayAccess, {scope:this, priority:9999})
			;
		
		var listOptions = ['itemClass'];
		
		conbo.assign(this, conbo.pick(options, listOptions));
		
		this.source = options.source || [];
	},
	
	/**
	 * The Array used as the source for this List
	 */
	get source()
	{
		if (!this.__source)
		{
			this.__source = [];
		}
		
		return this.__source;
	},
	
	set source(value)
	{
		this.__source = [];
		this.push.apply(this, conbo.toArray(value));
		this.dispatchChange('source', 'length');
	},
	
	/**
	 * The number of items in the List
	 */
	get length()
	{
		if (this.source)
		{
			return this.source.length;
		}
		
		return 0;
	},
	
	/**
	 * Add an item to the end of the collection.
	 */
	push: function(item)
	{
		var items = conbo.toArray(arguments);
		
		if (items.length)
		{
			this.source.push.apply(this.source, this.__applyItemClass(items));
			this.__updateBindings(items);
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.ADD));
			this.dispatchChange('length');
		}
		
		return this.length;
	},
	
	/**
	 * Remove an item from the end of the collection.
	 */
	pop: function()
	{
		if (!this.length) return;
		
		var item = this.source.pop();
		
		this.__updateBindings(item, false);
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.REMOVE));
		this.dispatchChange('length');
		
		return item;
	},
	
	/**
	 * Add an item to the beginning of the collection.
	 */
	unshift: function(item) 
	{
		if (item)
		{
			this.source.unshift.apply(this.source, this.__applyItemClass(conbo.toArray(arguments)));
			this.__updateBindings(conbo.toArray(arguments));
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.ADD));
			this.dispatchChange('length');
		}
		
		return this.length;
	},
	
	/**
	 * Remove an item from the beginning of the collection.
	 */
	shift: function()
	{
		if (!this.length) return;
		
		var item = this.source.shift();
		
		this.__updateBindings(item, false);
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.REMOVE));
		this.dispatchChange('length');
		
		return item;
	},
	
	/**
	 * Slice out a sub-array of items from the collection.
	 */
	slice: function(begin, length)
	{
		begin || (begin = 0);
		if (conbo.isUndefined(length)) length = this.length;
		
		return new conbo.List({source:this.source.slice(begin, length)});
	},
	
	/**
	 * Splice out a sub-array of items from the collection.
	 */
	splice: function(begin, length)
	{
		begin || (begin = 0);
		if (conbo.isUndefined(length)) length = this.length;
		
		var inserts = conbo.rest(arguments,2);
		var items = this.source.splice.apply(this.source, [begin, length].concat(inserts));
		
		if (items.length) this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.REMOVE));
		if (inserts.length) this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.ADD));
		
		if (items.length || inserts.length)
		{
			this.dispatchChange('length');
		}
		
		return new conbo.List({source:items});
	},
	
	/**
	 * Get the item at the given index; similar to array[index]
	 * @deprecated	Use getItem()
	 */
	getItemAt: function(index) 
	{
		return this.source[index];
	},
	
	/**
	 * Add (or replace) item at given index with the one specified,
	 * similar to array[index] = value;
	 * @deprecated	Use setItem()
	 */
	setItemAt: function(index, item)
	{
		var length = this.length;
		
		var replaced = this.source[index];
		this.__updateBindings(replaced, false);
		
		this.source[index] = item;
		this.__updateBindings(item);
		
		if (this.length > length)
		{
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.ADD));
			this.dispatchChange('length');
		}
		
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CHANGE, {item:item}));
		
		return replaced;
	},
	
	/**
	 * Force the collection to re-sort itself.
	 * @param	{Function}	[compareFunction] - Compare function to determine sort order
	 */
	sort: function(compareFunction) 
	{
		this.source.sort(compareFunction);
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CHANGE));
		
		return this;
	},
	
	/**
   	 * Create a new List identical to this one.
	 */
	clone: function() 
	{
		return new this.constructor(this.source);
	},
	
	/**
	 * The JSON-friendly representation of the List
	 */
	toJSON: function() 
	{
		return conbo.jsonify(this.source);
	},
	
	toArray: function()
	{
		return this.source.slice();
	},

	toString: function()
	{
		return 'conbo.List';
	},
	
	// Web Storage API

	// [Web Storage API] for 'length' property, see above

	/**
	 * [Web Storage API] When passed a number n, this method will return n if that index exists or -1 if it does not
	 * @param {number} index
	 */
	key: function(index)
	{
		if (index in this.source)
		{
			return index;
		}

		return -1;
	},
	
	/**
	 * [Web Storage API] When passed a key name, will return that key's value
	 * @param {number} keyName
	 */
	getItem: function(keyName)
	{
		return this.getItemAt(keyName);
	},
	
	/**
	 * [Web Storage API] When passed a key name and value, will add that key to the List (i.e. add a new value at that index), or update that key's value if it already exists
	 * @param {number} keyName
	 * @param {*} keyValue
	 */
	setItem: function(keyName, keyValue)
	{
		this.setItemAt(keyName, keyValue);
	},

	/**
	 * [Web Storage API] When passed an key name, will remove that key from the List, equivalent to List.splice(keyName, 1)
	 * @param {number} keyName
	 */
	removeItem: function(keyName)
	{
		this.splice(keyName, 1);
	},

	/**
	 * [Web Storage API] When invoked, will empty all items out of the List, reducing its length to zero
	 */
	clear: function()
	{
		this.splice();
	},

	// Internal

	/**
	 * Listen to the events of Bindable values so we can detect changes
	 * @param 	{any}		models
	 * @param 	{Boolean}	enabled
	 * @private
	 */
	__updateBindings: function(items, enabled)
	{
		var method = enabled === false ? 'removeEventListener' : 'addEventListener';
		
		items = (conbo.isArray(items) ? items : [items]).slice();
		
		while (items.length)
		{
			var item = items.pop();
			
			if (item instanceof conbo.EventDispatcher)
			{
				item[method](conbo.ConboEvent.CHANGE, this.dispatchEvent, this);
			}
		}
	},
	
	/**
	 * Enables array access operator, e.g. myList[0]
	 * @private
	 */
	__updateArrayAccess: function(event)
	{
		var i;
		
		var define = (function(n)
		{
			Object.defineProperty(this, n, 
			{
				get: function() { return this.getItemAt(n); },
				set: function(value) { this.setItemAt(n, value); },
				configurable: true,
				enumerable: true
			});
		}).bind(this);
		
		for (i=0; i<this.length; i++)
		{
			if (!(i in this)) define(i);
		}
		
		while (i in this)
		{
			delete this[i++];
		}
	},
	
	/**
	 * @private
	 */
	__applyItemClass: function(item)
	{
		if (item instanceof Array)
		{
			for (var i=0; i<item.length; i++)
			{
				item[i] = this.__applyItemClass(item[i]);
			}
			
			return item;
		}
		
		if (conbo.isObject(item) 
			&& !conbo.isClass(item)
			&& !(item instanceof conbo.Class)
			)
		{
			item = new this.itemClass({source:item, context:this.context});
		}
		
		return item;
	},
	
}).implement(conbo.IInjectable);

// Utility methods that we want to implement on the List.
var listMethods = 
[
	'forEach', 'map', 'find', 'findIndex', 'filter', 'reject', 'every', 
	'contains', 'invoke', 'indexOf', 'lastIndexOf', 'max', 'min',
	'size', 'rest', 'last', 'without', 'shuffle', 'isEmpty', 'sortOn'
];

// Mix in each available Conbo utility method as a proxy
listMethods.forEach(function(method) 
{
	if (!(method in conbo)) return;
	
	conbo.List.prototype[method] = function() 
	{
		var args = [this.source].concat(conbo.toArray(arguments)),
			result = conbo[method].apply(conbo, args);
		
		// TODO What's the performance impact of doing this?
//		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CHANGE));
		
		return conbo.isArray(result)
//			? new this.constructor({source:result}) // TODO Return List of same type as original?
			? new conbo.List({source:result, itemClass:this.itemClass})
			: result;
	};
});

__denumerate(conbo.List.prototype);

/**
 * LocalList is a persistent List class that is saved into LocalStorage
 * or SessionStorage
 * 
 * @class		LocalList
 * @memberof	conbo
 * @augments	conbo.List
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options, including 'name' (String), 'session' (Boolean) and 'source' (Array) of default options
 * @fires		conbo.ConboEvent#CHANGE
 * @fires		conbo.ConboEvent#ADD
 * @fires		conbo.ConboEvent#REMOVE
 */
conbo.LocalList = conbo.List.extend(
/** @lends conbo.LocalList.prototype */
{
	__construct: function(options)
	{
		var defaultName = 'ConboLocalList';
		
		options = conbo.defineDefaults(options, this.options, {name:defaultName});
		
		var name = options.name;
		
		var storage = options.session 
			? window.sessionStorage
			: window.localStorage;
		
		if (name == defaultName)
		{
			conbo.warn('No name specified for '+this.toString()+', using "'+defaultName+'"');
		}
		
		var getLocal = function()
		{
			return name in storage
				? JSON.parse(storage.getItem(name) || '[]')
				: options.source || [];
		};
		
		// Sync with LocalStorage
		this.addEventListener(conbo.ConboEvent.CHANGE, function(event)
		{
  			storage.setItem(name, JSON.stringify(this));
		}, 
		{scope:this, priority:1000});
		
		options.source = getLocal();
		
		conbo.List.prototype.__construct.call(this, options);
	},
	
	/**
	 * Immediately writes all data to local storage. If you don't use this method, 
	 * Conbo writes the data the next time it detects a change to a bindable property.
	 */
	flush: function()
	{
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CHANGE));
		return this;
	},
	
	toString: function()
	{
		return 'conbo.LocalList';
	}
	
});

__denumerate(conbo.LocalList.prototype);

/**
 * Attribute Bindings
 * 
 * Functions that can be used to bind DOM elements to properties of Bindable 
 * class instances to DOM elements via their attributes.
 * 
 * @class		AttributeBindings
 * @memberof	conbo
 * @augments	conbo.Class
 * @author 		Neil Rackett
 */
conbo.AttributeBindings = conbo.Class.extend(
/** @lends conbo.AttributeBindings.prototype */
{
	initialize: function()
	{
		// Methods that can accept multiple parameters
		
		this.cbAria.multiple = true;
		this.cbClass.multiple = true;
		this.cbStyle.multiple = true;
		
		// Methods that require raw attribute data instead of bound property values
		
		this.cbIncludeIn.raw = true;
		this.cbExcludeFrom.raw = true;
		this.cbRef.raw = true;

		// Methods that don't require any parameters

		this.cbDetectChange.readOnly = true;
	},
	
	/**
	 * Can the given attribute be bound to multiple properties at the same time?
	 * @param 	{string}	attribute
	 * @returns {Boolean}
	 */
	canHandleMultiple: function(attribute)
	{
		var f = conbo.toCamelCase(attribute);
		return (f in this) && this[f].multiple;
	},
	
	/**
	 * Makes an element visible
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-show="propertyName"></div>
	 */
	cbShow: function(el, value)
	{
		this.cbHide(el, conbo.isEmpty(value));
	},
	
	/**
	 * Hides an element by making it invisible, but does not remove
	 * if from the layout of the page, meaning a blank space will remain
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-hide="propertyName"></div>
	 */
	cbHide: function(el, value)
	{
		!conbo.isEmpty(value)
			? el.classList.add('cb-hide')
			: el.classList.remove('cb-hide');
	},
	
	/**
	 * Include an element on the screen and in the layout of the page
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-include="propertyName"></div>
	 */
	cbInclude: function(el, value)
	{
		this.cbExclude(el, conbo.isEmpty(value));
	},
	
	/**
	 * Remove an element from the screen and prevent it having an effect
	 * on the layout of the page
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-exclude="propertyName"></div>
	 */
	cbExclude: function(el, value)
	{
		!conbo.isEmpty(value)
			? el.classList.add('cb-exclude')
			: el.classList.remove('cb-exclude')
			;
	},
	
	/**
	 * The exact opposite of HTML's built-in `disabled` property
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-enabled="propertyName"></div>
	 */
	cbEnabled: function(el, value)
	{
		el.disabled = !value;
	},
	
	/**
	 * Inserts raw HTML into the element, which is rendered as HTML
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-html="propertyName"></div>
	 */
	cbHtml: function(el, value)
	{
		el.innerHTML = value;
	},
	
	/**
	 * Inserts text into the element so that it appears on screen exactly as
	 * it's written by converting special characters (<, >, &, etc) into HTML
	 * entities before rendering them, e.g. "8 < 10" becomes "8 &lt; 10", and
	 * line breaks into <br/>
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-text="propertyName"></div>
	 */
	cbText: function(el, value)
	{
		value = conbo.encodeEntities(value).replace(/\r?\n|\r/g, '<br/>');
		el.innerHTML = value;
	},
	
	/**
	 * Applies or removes a CSS class on an element based on the value
	 * of the bound property, where cb-class="myProperty:class-name" will apply
	 * the class "class-name" when "myProperty" is a truthy value, or 
	 * cb-class="myProperty" will apply the class "myProperty" when "myProperty"
	 * is a truthy value
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-class="propertyName"></div>
	 * <div cb-class="propertyName:my-class-name"></div>
	 */
	cbClass: function(el, value, options, className)
	{
		className || (className = options.propertyName);
		
		!conbo.isEmpty(value)
			? __ep(el).addClass(className)
			: __ep(el).removeClass(className)
			;
	},
	
	/**
	 * Applies class(es) to the element based on the value contained in a variable. 
	 * Experimental.
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-classes="propertyName"></div>
	 */
	cbClasses: function(el, value)
	{
		if (el.cbClasses)
		{
			__ep(el).removeClass(el.cbClasses);
		}
		
		el.cbClasses = value;
		
		if (value)
		{
			__ep(el).addClass(value);
		}
	},
	
	/**
	 * Apply styles from a variable
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @param 		{Object} 		options - Options relating to this binding
	 * @param 		{string} 		styleName - The name of the style to bind
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-="propertyName:font-weight"></div>
	 */
	cbStyle: function(el, value, options, styleName)
	{
		if (!styleName)
		{
			conbo.warn('cb-style attributes must specify one or more styles in the format cb-style="myProperty:style-name"');
		}
		
		styleName = conbo.toCamelCase(styleName);
		el.style[styleName] = value;
	},
	
	/**
	 * Repeats the element once for each item of the specified list or Array,
	 * applying the specified Glimpse or View class to the element and passing
	 * each value to the item renderer as a "data" property.
	 * 
	 * The optional item renderer class can be specified by following the 
	 * property name with a colon and the class name or by using the tag name.
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @param 		{Object} 		options - Options relating to this binding
	 * @param 		{string} 		itemRendererClassName - The name of the class to apply to each item rendered
	 * @returns		{void}
	 * 
	 * @example
	 * <li cb-repeat="people" cb-html="data.firstName"></li>
	 * <li cb-repeat="people:PersonItemRenderer">{{data.firstName}}</li>
	 * <person-item-renderer cb-repeat="people"></person-item-renderer>
	 */
	cbRepeat: function(el, values, options, itemRendererClassName)
	{
		var a; 
		var args = conbo.toArray(arguments);
		var viewClass;
		var ep = __ep(el);

		options || (options = {});
		
		if (options.context && options.context.namespace)
		{
			itemRendererClassName || (itemRendererClassName = conbo.toCamelCase(el.tagName, true));
			viewClass = conbo.bindingUtils.getClass(itemRendererClassName, options.context.namespace);
		}
		
		viewClass || (viewClass = conbo.ItemRenderer);
		el.cbRepeat || (el.cbRepeat = {});
		
		var elements = el.cbRepeat.elements || [];
		var placeholder;

		if (el.cbRepeat.placeholder)
		{
			placeholder = el.cbRepeat.placeholder;
		}
		else
		{
			placeholder = document.createComment(conbo.bindingUtils.removeAttributeAfterBinding ? '' : 'cb-repeat');
			el.parentNode.insertBefore(placeholder, el);
			el.parentNode.removeChild(el);
		}
		
		if (el.cbRepeat.list != values && values instanceof conbo.List)
		{
			var changeTimeout;

			if (el.cbRepeat.list)
			{
				el.cbRepeat.list.removeEventListener('change', el.cbRepeat.changeHandler, {scope:this});
			}
			
			var applyChange = function(event)
			{
				event.property === 'length'
					? options.view.dispatchChange(options.propertyName)
					: this.cbRepeat.apply(this, args)
					;
			};

			// TODO Optimise this
			el.cbRepeat.changeHandler = function(event)
			{
				// Ensure a single when multiple changes
				clearTimeout(changeTimeout);
				changeTimeout = setTimeout(applyChange, 0, event);
			};
			
			values.addEventListener('change', el.cbRepeat.changeHandler, {scope:this});

			el.cbRepeat.list = values;
		}
		
		switch (true)
		{
			case values instanceof Array:
			case values instanceof conbo.List:
			{
				a = values;
				break;
			}
			
			default:
			{
				// To support element lists, etc
				a = conbo.isIterable(values)
					? conbo.toArray(values)
					: [];
				break;
			}
		}
		
		while (elements.length)
		{
			var rEl = elements.pop();
			var rView = rEl.cbView || rEl.cbGlimpse;
			
			if (rView) rView.remove();
			else rEl.parentNode.removeChild(rEl);
		}
		
		// Switched from forEach loop to resolve issues using "new Array(n)"
		// see: http://stackoverflow.com/questions/23460301/foreach-on-array-of-undefined-created-by-array-constructor
		for (var index=0,length=a.length; index<length; ++index)
		{
			var value = a[index];
			var clone = el.cloneNode(true);
			
			// Wraps non-iterable objects to make them bindable
			if (conbo.isObject(value) && !conbo.isIterable(value) && !(value instanceof conbo.Hash))
			{
				value = new conbo.Hash({source:value});
			}
			
			clone.removeAttribute('cb-repeat');
			
			var viewOptions = 
			{
				data: value, 
				el: clone, 
				index: index,
				isLast: index == a.length-1,
				list: a,
				className: 'cb-repeat'
			};
			
			var view = new viewClass(conbo.assign(viewOptions, options));
			
			elements.push(view.el);
		};
		
		var fragment = document.createDocumentFragment();
		
		elements.forEach(function(el)
		{
			fragment.appendChild(el);
		});
	
		placeholder.parentNode.insertBefore(fragment, placeholder);
		
		el.cbRepeat.elements = elements;
		el.cbRepeat.placeholder = placeholder;
	},
	
	/**
	 * Sets the properties of the element's dataset (it's `data-*` attributes)
	 * using the properties of the object being bound to it. Non-Object values 
	 * will be disregarded. You'll need to use a polyfill for IE <= 10.
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-dataset="propertyName"></div>
	 */
	cbDataset: function(el, value)
	{
		if (conbo.isObject(value))
		{
			conbo.assign(el.dataset, value);
		}
	},
	
	/**
	 * When used with a standard DOM element, the properties of the element's
	 * `dataset` (it's `data-*` attributes) are set using the properties of the 
	 * object being bound to it; you'll need to use a polyfill for IE <= 10
	 * 
	 * When used with a Glimpse, the Glimpse's `data` property is set to
	 * the value of the bound property. 
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-data="propertyName"></div>
	 */
	cbData: function(el, value)
	{
		if (el.cbGlimpse)
		{
			el.cbGlimpse.data = value;
		}
		else
		{
			this.cbDataset(el, value);
		}
	},
	
	/**
	 * Only includes the specified element in the layout when the View's `currentState`
	 * matches one of the states listed in the attribute's value; multiple states should
	 * be separated by spaces
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @param 		{Object} 		options - Options relating to this binding
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-include-in="happy sad elated"></div>
	 */
	cbIncludeIn: function(el, value, options)
	{
		var view = options.view;
		var states = value.split(' ');
		
		var stateChangeHandler = (function()
		{
			this.cbInclude(el, states.indexOf(view.currentState) != -1);
		}).bind(this);
		
		view.addEventListener('change:currentState', stateChangeHandler, {scope:this});
		stateChangeHandler.call(this);
	},
	
	/**
	 * Removes the specified element from the layout when the View's `currentState`
	 * matches one of the states listed in the attribute's value; multiple states should
	 * be separated by spaces
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @param 		{Object} 		options - Options relating to this binding
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-exclude-from="confused frightened"></div>
	 */
	cbExcludeFrom: function(el, value, options)
	{
		var view = options.view;
		var states = value.split(' ');
		
		var stateChangeHandler = function()
		{
			this.cbExclude(el, states.indexOf(view.currentState) != -1);
		};
		
		view.addEventListener('change:currentState', stateChangeHandler, {scope:this});
		stateChangeHandler.call(this);
	},
	
	/**
	 * Completely removes an element from the DOM based on a bound property value, 
	 * primarily intended to facilitate graceful degredation and removal of desktop 
	 * features in mobile environments.
	 * 
	 * @example		cb-remove="isMobile"
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-remove="propertyName"></div>
	 */
	cbRemove: function(el, value)
	{
		if (!conbo.isEmpty(value))
		{
			// TODO Remove binding, etc?
			el.parentNode.removeChild(el);
		}
	},
	
	/**
	 * The opposite of `cbRemove`
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-keep="propertyName"></div>
	 */
	cbKeep: function(el, value)
	{
		this.cbRemove(el, !value);
	},
	
	/**
	 * Enables the use of cb-onbind attribute to handle the 'bind' event 
	 * dispatched by the element after it has been bound by Conbo
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-onbind="functionName"></div>
	 */
	cbOnbind: function(el, handler)
	{
		el.addEventListener('bind', handler);
	},
	
	/**
	 * Uses JavaScript to open an anchor's HREF so that the link will open in
	 * an iOS WebView instead of Safari
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-jshref="propertyName"></div>
	 */
	cbJshref: function(el)
	{
		if (el.tagName == 'A')
		{
			el.onclick = function(event)
			{
				window.location = el.href;
				event.preventDefault();
				return false;
			};
		}
	},
	
	/*
	 * FORM HANDLING & VALIDATION
	 */
	
	/**
	 * Detects changes to the specified element and applies the CSS class
	 * cb-changed or cb-unchanged to the parent form, depending on whether
	 * the contents have changed from their original value.
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-detect-change></div>
	 */
	cbDetectChange: function(el)
	{
		var ep = __ep(el); 
		var form = ep.closest('form');
		var fp = __ep(form);
		var originalValue = el.value || el.innerHTML;
		
		var updateForm = function()
		{
			fp.removeClass('cb-changed cb-unchanged')
				.addClass(form.querySelector('.cb-changed') ? 'cb-changed' : 'cb-unchanged');
		};
		
		var changeHandler = function()
		{
			var changed = (el.value || el.innerHTML) != originalValue;
			
			ep.removeClass('cb-changed cb-unchanged')
				.addClass(changed ? 'cb-changed' : 'cb-unchanged')
				;
			
			updateForm();
		};
		
		ep.addEventListener('change input', changeHandler)
			.addClass('cb-unchanged')
			;
		
		updateForm();
	},
	
	/**
	 * Use a method or regex to validate a form element and apply a
	 * cb-valid or cb-invalid CSS class based on the outcome
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{Function} 		validator - The function referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-validate="functionName"></div>
	 */
	cbValidate: function(el, validator)
	{
		var validateFunction;
		
		switch (true)
		{
			case conbo.isFunction(validator):
			{
				validateFunction = validator;
				break;
			}
			
			case conbo.isString(validator):
			{
				validator = new RegExp(validator);
			}
			
			case conbo.isRegExp(validator):
			{
				validateFunction = function(value)
				{
					return validator.test(value);
				};
				
				break;
			}
		}
		
		if (!conbo.isFunction(validateFunction))
		{
			conbo.warn(validator+' cannot be used with cb-validate');
			return;
		}
		
		var ep = __ep(el);
		var form = ep.closest('form');
		
		var getClasses = function(regEx) 
		{
			return function (classes) 
			{
				return classes.split(/\s+/).filter(function(el)
				{
					return regEx.test(el); 
				})
				.join(' ');
			};
		};
		
		var validate = function()
		{
			// Form item
			
			var value = el.value || el.innerHTML
				, result = validateFunction(value) 
				, valid = (result === true)
				, classes = []
				;
			
			classes.push(valid ? 'cb-valid' : 'cb-invalid');
			
			if (conbo.isString(result))
			{
				classes.push('cb-invalid-'+result);
			}
			
			ep.removeClass('cb-valid cb-invalid')
				.removeClass(getClasses(/^cb-invalid-/))
				.addClass(classes.join(' '))
				;
			
			// Form
			
			if (form)
			{
				var fp = __ep(form);
				
				fp.removeClass('cb-valid cb-invalid')
					.removeClass(getClasses(/^cb-invalid-/))
					;
				
				if (valid) 
				{
					valid = !form.querySelector('.cb-invalid');
					
					if (valid)
					{
						conbo.toArray(form.querySelectorAll('[required]')).forEach(function(rEl) 
						{
							if (!String(rEl.value || rEl.innerHTML).trim())
							{
								valid = false;
								return false; 
							}
						});
					}
				}
				
				fp.addClass(valid ? 'cb-valid' : 'cb-invalid');
			}
			
		};
		
		ep.addEventListener('change input blur', validate);
	},
	
	/**
	 * Restricts text input to the specified characters
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{string} 		value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-restrict="propertyName"></div>
	 */
	cbRestrict: function(el, value)
	{
		// TODO Restrict to text input fields?
		
		if (el.cbRestrict)
		{
			el.removeEventListener('keypress', el.cbRestrict);
		}
		
		el.cbRestrict = function(event)
		{
			if (event.ctrlKey)
			{
				return;
			}
			
			var code = event.keyCode || event.which;
			var char = event.key || String.fromCharCode(code);
			var regExp = value;
				
			if (!conbo.isRegExp(regExp))
			{
				regExp = new RegExp('['+regExp+']', 'g');
			}
			
			if (!char.match(regExp))
			{
				event.preventDefault();
			}
		};
		
		el.addEventListener('keypress', el.cbRestrict);
	},
	
	/**
	 * Limits the number of characters that can be entered into
	 * input and other form fields
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{string} 		value - The value referenced by the attribute
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-max-chars="propertyName"></div>
	 */
	cbMaxChars: function(el, value)
	{
		// TODO Restrict to text input fields?
		
		if (el.cbMaxChars)
		{
			el.removeEventListener('keypress', el.cbMaxChars);
		}
		
		el.cbMaxChars = function(event)
		{
			if ((el.value || el.innerHTML).length >= value)
			{
				event.preventDefault();
			}
		};
		
		el.addEventListener('keypress', el.cbMaxChars);
	},
	
	/**
	 * Sets the aria accessibility attributes on an element based on the value
	 * of the bound property, e.g. cb-aria="myProperty:label" to set aria-label 
	 * to the value of myProperty
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{*} 			value - The value referenced by the attribute
	 * @param 		{*} 			options
	 * @param 		{string} 		ariaName - The name of the aria value to set (without the aria- prefix)
	 * @returns		{void}
	 * 
	 * @example
	 * <div cb-class="ariaLabel:label"></div>
	 */
	cbAria: function(el, value, options, ariaName)
	{
		if (!ariaName)
		{
			conbo.warn('cb-aria attributes must specify one or more name in the format cb-class="myProperty:aria-name"');
		}
		
		el.setAttribute('aria-'+ariaName, value);
	},

	/**
	 * Enables you to detect and handle a long press (500ms) on an element
	 * 
	 * @param 		{HTMLElement}	el - DOM element to which the attribute applies
	 * @param 		{Function} 		handler - The method that will handle long presses
	 * 
	 * @example
	 * <button cb-onlongpress="myLongPressHandler">Hold me!</button>
	 */
	cbOnlongpress: function(el, handler)
	{
		var isLongPress = false;
		var pressTimer;
		
		var cancel = function(event)
		{
			if (pressTimer) 
			{
				clearTimeout(pressTimer);
				pressTimer = 0;
			}
		};
		
		var click = function(event)
		{
			if (pressTimer) 
			{
				clearTimeout(pressTimer);
				pressTimer = 0;
			}
			
			if (isLongPress) return false;
		};
		
		var start = function(event)
		{
			if (event.type === 'click' && event.button !== 0)
			{
				return;
			}
			
			isLongPress = false;
			
			pressTimer = setTimeout(function() 
			{
				isLongPress = true;
				handler(new MouseEvent('longpress', event));
			}, 500);
			
			return false;
		};
		
		el.addEventListener('mousedown', start);
		el.addEventListener('touchstart', start);
		el.addEventListener('click', click);
		el.addEventListener('mouseout', cancel);
		el.addEventListener('touchend', cancel);
		el.addEventListener('touchleave', cancel);
		el.addEventListener('touchcancel', cancel);
	},
	
	/**
	 * Sets the value of the specified property of the View instance to a reference
	 * to the element with this attribute set
	 * 
	 * @param {HTMLElement} el 			HTML Element
	 * @param {String} 		value 		Name of the property to set as a reference to the element
	 * @param {*} 			options 
	 */
	cbRef: function(el, value, options)
	{
		options.view[value] = el;
	},

});

(function()
{
	'strict mode';

	var BindingUtils__cbAttrs = new conbo.AttributeBindings();
	var BindingUtils__customAttrs = {};
	var BindingUtils__reservedAttrs = ['cb-app', 'cb-view', 'cb-glimpse', 'cb-content'];
	var BindingUtils__reservedNamespaces = ['cb', 'data', 'aria'];
	var BindingUtils__registeredNamespaces = ['cb'];
	
	/**
	 * Set the value of a property, ensuring Numbers are types correctly
	 * 
	 * @private
	 * @param 	propertyName
	 * @param 	value
	 * @example	BindingUtils__set.call(target, 'n', 123);
	 * @returns	this
	 */
	var BindingUtils__set = function(propertyName, value)
	{
		if (this[propertyName] === value)
		{
			return this;
		}
		
		// Ensure numbers are returned as Number not String
		if (value && conbo.isString(value) && !isNaN(value))
		{
			value = parseFloat(value);
			if (isNaN(value)) value = '';
		}
		
		this[propertyName] = value;
		
		return this;
	};
	
	/**
	 * Is the specified attribute reserved for another purpose?
	 * 
	 * @private
	 * @param 		{string}	value
	 * @returns		{Boolean}
	 */
	var BindingUtils__isReservedAttr = function(value)
	{
		return BindingUtils__reservedAttrs.indexOf(value) != -1;
	};
	
	/**
	 * Attempt to make a property bindable if it isn't already
	 * 
	 * @private
	 * @param 		{string}	value
	 * @returns		{Boolean}
	 */
	var BindingUtils__makeBindable = function(source, propertyName)
	{
		if (!conbo.isAccessor(source, propertyName) && !conbo.isFunc(source, propertyName))
		{
			if (source instanceof conbo.EventDispatcher)
			{
				conbo.makeBindable(source, [propertyName]);
			}
			else
			{
				conbo.warn('It will not be possible to detect changes to "'+propertyName+'" because "'+source.toString()+'" is not an EventDispatcher');
			}
		}
	}
	
	/**
	 * Remove everything except alphanumeric, dot, space and underscore 
	 * characters from Strings
	 * 
	 * @private
	 * @param 		{string}	value - String value to clean
	 * @returns		{string}
	 */
	var BindingUtils__cleanPropertyName = function(value)
	{
		return (value || '').trim().replace(/[^\w\._\s]/g, '');
	};
	
	var BindingUtils_eval = function(obj, strOrArray)
	{
		var a = conbo.isString(strOrArray)
			? BindingUtils__cleanPropertyName(strOrArray).split('.')
			: strOrArray
			;

		return a.reduce(function(obj, i) { return obj[i]; }, obj);
	};

	/**
	 * Binding utilities class
	 * 
	 * Used to bind properties of EventDispatcher class instances to DOM elements, 
	 * other EventDispatcher class instances or setter functions
	 * 
	 * @class		BindingUtils
	 * @memberof	conbo
	 * @augments	conbo.Class
	 * @author 		Neil Rackett
	 */
	conbo.BindingUtils = conbo.Class.extend(
	/** @lends conbo.BindingUtils.prototype */
	{
		/**
		 * Should binding attributes, like "cb-bind", be removed after they've been processed?
		 * @type	{boolean}
		 */
		removeAttributeAfterBinding: true,
		
		/**
		 * Bind a property of a EventDispatcher class instance (e.g. Hash or View) 
		 * to a DOM element's value/content, using ConboJS's best judgement to
		 * work out how the value should be bound to the element.
		 * 
		 * This method of binding also allows for the use of a parse function,
		 * which can be used to manipulate bound data in real time
		 * 
		 * @param 		{conbo.EventDispatcher}	source - Class instance which extends from conbo.EventDispatcher
		 * @param 		{string} 				propertyName - Property name to bind
		 * @param 		{HTMLElement} 			el - DOM element to bind value to (two-way bind on input/form elements)
		 * @param 		{Function}				[parseFunction] - Optional method used to parse values before outputting as HTML
		 * 
		 * @returns		{Array}					Array of bindings
		 */
		bindElement: function(source, propertyName, el, parseFunction)
		{
			var isEventDispatcher = source instanceof conbo.EventDispatcher;
			
			if (!el)
			{
				throw new Error('el is undefined');
			}
			
			BindingUtils__makeBindable(source, propertyName);
			
			var scope = this;
			var bindings = [];
			var eventType;
			var eventHandler;
			
			parseFunction || (parseFunction = this.defaultParseFunction);
			
			var ep = new conbo.EventProxy(el);
			var tagName = el.tagName;
			
			switch (tagName)
			{
				case 'INPUT':
				case 'SELECT':
				case 'TEXTAREA':
				{	
					var type = (el.type || tagName).toLowerCase();
					
					switch (type)
					{
						case 'checkbox':
						{
							el.checked = !!source[propertyName];
							
							if (isEventDispatcher)
							{
								eventType = 'change:'+propertyName;
								
								eventHandler = function(event)
								{
									el.checked = !!event.value;
								};
								
								source.addEventListener(eventType, eventHandler);
								bindings.push([source, eventType, eventHandler]);
							}
							
							eventType = 'input change';
							
							eventHandler = function(event)
							{
								BindingUtils__set.call(source, propertyName, el.checked);
							};
							
							ep.addEventListener(eventType, eventHandler);
							bindings.push([ep, eventType, eventHandler]);
							
							return;
						}
						
						case 'radio':
						{
							if (el.value == source[propertyName]) 
							{
								el.checked = true;
							}
							
							if (isEventDispatcher)
							{
								eventType = 'change:'+propertyName;
								
								eventHandler = function(event)
								{
									if (event.value == null) event.value = '';
									if (el.value != event.value) return; 
									
									el.checked = true;
								};
								
								source.addEventListener(eventType, eventHandler);
								bindings.push([source, eventType, eventHandler]);
							}
							
							break;
						}
						
						default:
						{
							el.value = conbo.toValueString(source[propertyName]);
							
							if (isEventDispatcher)
							{
								eventType = 'change:'+propertyName;
								
								eventHandler = function(event)
								{
									if (event.value == null) event.value = '';
									if (el.value == event.value) return;
									
									el.value = conbo.toValueString(event.value);
								};
								
								source.addEventListener(eventType, eventHandler);
								bindings.push([source, eventType, eventHandler]);
							}
							
							break;
						}
					}
					
					eventType = 'input change';
					
					eventHandler = function(event)
					{
						BindingUtils__set.call(source, propertyName, el.value === undefined ? el.innerHTML : el.value);
					};
					
					ep.addEventListener(eventType, eventHandler);
					bindings.push([ep, eventType, eventHandler]);
					
					break;
				}
				
				case 'CB-TEXT':
				{
					var textNode = document.createTextNode(parseFunction(source[propertyName]))
					
					el.parentNode.insertBefore(textNode, el);
					el.parentNode.removeChild(el);
					
					if (isEventDispatcher)
					{
						eventType = 'change:'+propertyName;
						
						eventHandler = function(event) 
						{
							textNode.data = parseFunction(event.value);
						};
						
						source.addEventListener(eventType, eventHandler);
						bindings.push([source, eventType, eventHandler]);
					}
					
					break;
				}
				
				default:
				{
					el.innerHTML = parseFunction(source[propertyName]);
					
					if (isEventDispatcher)
					{
						eventType = 'change:'+propertyName;
						
						eventHandler = function(event) 
						{
							var html = parseFunction(event.value);
							el.innerHTML = html;
						};
						
						source.addEventListener(eventType, eventHandler);
						bindings.push([source, eventType, eventHandler]);
					}
					
					break;
				}
			}
			
			return bindings;
		},
		
		/**
		 * Unbinds the specified property of a bindable class from the specified DOM element
		 * 
		 * @param 		{conbo.EventDispatcher}	source - Class instance which extends from conbo.EventDispatcher
		 * @param 		{string} 				propertyName - Property name to bind
		 * @param 		{HTMLElement} 			el - DOM element to unbind value from
		 * @returns		{conbo.BindingUtils}	A reference to this object 
		 */
		unbindElement: function(source, propertyName, element)
		{
			// TODO Implement unbindElement
			return this;
		},
		
		/**
		 * Bind a DOM element to the property of a EventDispatcher class instance,
		 * e.g. Hash or Model, using cb-* attributes to specify how the binding
		 * should be made.
		 * 
		 * Two way bindings will automatically be applied where the attribute name 
		 * matches a property on the target element, meaning your EventDispatcher object 
		 * will automatically be updated when the property changes.
		 * 
		 * @param 	{conbo.EventDispatcher}	source - Class instance which extends from conbo.EventDispatcher (e.g. Hash or Model)
		 * @param 	{string}				propertyName - Property name to bind
		 * @param 	{HTMLElement}			element - DOM element to bind value to (two-way bind on input/form elements)
		 * @param 	{string}				attributeName - The attribute to bind as it appears in HTML, e.g. "cb-prop-name"
		 * @param 	{Function} 				[parseFunction] - Method used to parse values before outputting as HTML
		 * @param	{Object}				[options] - Options related to this attribute binding
		 * 
		 * @returns	{Array}					Array of bindings
		 */
		bindAttribute: function(source, propertyName, element, attributeName, parseFunction, options)
		{
			var bindings = [];
			
			if (BindingUtils__isReservedAttr(attributeName))
			{
				return bindings;
			}
			
			if (!element)
			{
				throw new Error('element is undefined');
			}
			
			var split = attributeName.split('-'),
				hasNs = split.length > 1
				;
			
			if (!hasNs)
			{
				return bindings;
			}
			
			if (attributeName == 'cb-bind')
			{
				bindings = this.bindElement(source, propertyName, element, parseFunction);
				
				if (this.removeAttributeAfterBinding)
				{
					element.removeAttribute(attributeName);
				}
				
				return bindings;
			}
			
			BindingUtils__makeBindable(source, propertyName);
			
			var scope = this,
				eventType,
				eventHandler,
				args = conbo.toArray(arguments).slice(5),
				camelCase = conbo.toCamelCase(attributeName),
				ns = split[0],
				isConboNs = (ns == 'cb'),
				isConbo = isConboNs && camelCase in BindingUtils__cbAttrs,
				isCustom = !isConbo && camelCase in BindingUtils__customAttrs,
				isNative = isConboNs && split.length == 2 && split[1] in element,
				attrFuncs = BindingUtils__cbAttrs
				;
			
			parseFunction || (parseFunction = this.defaultParseFunction);
			
			switch (true)
			{
				// If we have a bespoke handler for this attribute, use it
				case isCustom:
					attrFuncs = BindingUtils__customAttrs;
				
				case isConbo:
				{
					if (!(source instanceof conbo.EventDispatcher))
					{
						conbo.warn('Source is not EventDispatcher');
						return this;
					}
					
					var fn = attrFuncs[camelCase];
					
					if (fn.raw)
					{
						fn.apply(attrFuncs, [element, propertyName].concat(args));
					}
					else
					{
						eventHandler = function(event)
						{
							fn.apply(attrFuncs, [element, parseFunction(source[propertyName])].concat(args));
						};
						
						eventType = 'change:'+propertyName;
						
						source.addEventListener(eventType, eventHandler);
						eventHandler();
						
						bindings.push([source, eventType, eventHandler]);
					}
					
					break;
				}
				
				case isNative:
				{
					var nativeAttr = split[1];
					
					switch (true)
					{
						case nativeAttr.indexOf('on') !== 0 && conbo.isFunction(element[nativeAttr]):
						{
							conbo.warn(attributeName+' is not a recognised attribute, did you mean cb-on'+nativeAttr+'?');
							break;
						}
						
						// If it's an event, add a listener
						case nativeAttr.indexOf('on') === 0:
						{
							if (!conbo.isFunction(source[propertyName]))
							{
								conbo.warn(propertyName+' is not a function and cannot be bound to DOM events');
								return this;
							}
							
							eventType = nativeAttr.substr(2);
							eventHandler = source[propertyName];
							
							element.addEventListener(eventType, eventHandler);
							bindings.push([element, eventType, eventHandler]);
							
							break;
						}
						
						// ... otherwise, bind to the native property
						default:
						{
							if (!(source instanceof conbo.EventDispatcher))
							{
								conbo.warn('Source is not EventDispatcher');
								return this;
							}
							
							eventHandler = function()
							{
								var value;
								
								value = parseFunction(source[propertyName]);
								value = conbo.isBoolean(element[nativeAttr]) ? !!value : value;
								
								element[nativeAttr] = value;
							};
						    
							eventType = 'change:'+propertyName;
							source.addEventListener(eventType, eventHandler);
							eventHandler();
							
							bindings.push([source, eventType, eventHandler]);
							
							var ep = new conbo.EventProxy(element);
							
							eventHandler = function()
			     			{
								BindingUtils__set.call(source, propertyName, element[nativeAttr]);
			     			};
							
			     			eventType = 'input change';
							ep.addEventListener(eventType, eventHandler);
							
							bindings.push([ep, eventType, eventHandler]);
							
							break;
						}
					}
					
					break;
				}
				
				default:
				{
					conbo.warn(attributeName+' is not recognised or does not exist on specified element');
					break;
				}
			}
			
			if (attributeName !== 'cb-repeat' && this.removeAttributeAfterBinding)
			{
				element.removeAttribute(attributeName);
			}
			
			return bindings;
		},
		
		/**
		 * Applies the specified read-only Conbo or custom attribute to the specified element
		 * 
		 * @param 	{HTMLElement}			element - DOM element to bind value to (two-way bind on input/form elements)
		 * @param 	{string}				attributeName - The attribute to bind as it appears in HTML, e.g. "cb-prop-name"
		 * @returns	{conbo.BindingUtils}	A reference to this object 
		 * 
		 * @example
		 * conbo.bindingUtils.applyAttribute(el, "my-custom-attr");
		 */
		applyAttribute: function(element, attributeName)
		{
			if (this.attributeExists(attributeName))
			{
				var camelCase = conbo.toCamelCase(attributeName),
					ns = attributeName.split('-')[0],
					attrFuncs = (ns == 'cb') ? BindingUtils__cbAttrs : BindingUtils__customAttrs,
					fn = attrFuncs[camelCase]
					;
				
				if (fn.readOnly)
				{
					fn.call(attrFuncs, element);
				}
				else
				{
					conbo.warn(attributeName+' attribute cannot be used without a value');
				}
			}
			else
			{
				conbo.warn(attributeName+' attribute does not exist');
			}
			
			return this;
		},
		
		/**
		 * Does the specified Conbo or custom attribute exist?
		 * @param 	{string}				attributeName - The attribute name as it appears in HTML, e.g. "cb-prop-name"
		 * @returns	{Boolean}
		 */
		attributeExists: function(attributeName)
		{
			var camelCase = conbo.toCamelCase(attributeName);
			return camelCase in BindingUtils__cbAttrs || camelCase in BindingUtils__customAttrs;
		},
		
		/**
		 * Bind everything within the DOM scope of a View to properties of the View instance
		 * 
		 * @param 	{conbo.View}			view - The View class controlling the element
		 * @returns	{conbo.BindingUtils}	A reference to this object 
		 */
		bindView: function(view)
		{
			if (!view)
			{
				throw new Error('view is undefined');
			}
			
			if (!!view.__bindings)
			{
				this.unbindView(view);
			}
			
			var options = {view:view},
				bindings = [],
				scope = this;
			
			if (!!view.subcontext) 
			{
				view.subcontext.addTo(options);
			}
			
			var ns = view.context && view.context.namespace;
			
			if (ns)
			{
				this.applyViews(view, ns, 'glimpse')
					.applyViews(view, ns, 'view')
					;
			}
			
			var ignored = [];
			
			view.querySelectorAll('[cb-repeat]').forEach(function(el)
			{
				ignored = ignored.concat(conbo.toArray(el.querySelectorAll('*')));
			});
			
			var elements = conbo.difference(view.querySelectorAll('*').concat([view.el]), ignored);

			// Prioritises processing of cb-repeat over other attributes
			elements.sort(function(el1, el2)
			{
				var r1 = __ep(el1).attributes.hasOwnProperty('cbRepeat');
				var r2 = __ep(el2).attributes.hasOwnProperty('cbRepeat');

				if (r1 && r2) return 0;
				if (r1 && !r2) return -1;
				if (!r1 && r2) return 1;
			});
			
			elements.forEach(function(el, index)
			{
				var attrs = __ep(el).attributes;
				
				if (!conbo.keys(attrs).length) 
				{
					return;
				}
				
				var keys = conbo.keys(attrs);
				
				// Prevents Conbo trying to populate repeat templates 
				if (keys.indexOf('cbRepeat') != -1)
				{
					keys = ['cbRepeat'];
				}
				
				keys.forEach(function(key)
				{
					var type = conbo.toUnderscoreCase(key, '-');
					var typeSplit = type.split('-');
					
					if (typeSplit.length < 2 
						|| BindingUtils__registeredNamespaces.indexOf(typeSplit[0]) == -1 
						|| BindingUtils__isReservedAttr(type))
					{
						return;
					}
					
					var splits = attrs[key].split(',');
					
					if (!BindingUtils__cbAttrs.canHandleMultiple(type))
					{
						splits = [splits[0]];
					}
					
					var splitsLength = splits.length;
					
					for (var i=0; i<splitsLength; i++)
					{
						var parseFunction,
							d = (splits[i] || '');
						
						if (!d)
						{
							scope.applyAttribute(el, type);
							break;
						}
						
						var b = d.split('|'),
							v = b[0].split(':'),
							propertyName = v[0],
							param = v[1],
							split = BindingUtils__cleanPropertyName(propertyName).split('.'),
							property = split.pop(),
							model;
						
						try
						{
							parseFunction = !!b[1] ? BindingUtils_eval(view, b[1]) : undefined;
							parseFunction = conbo.isFunction(parseFunction) ? parseFunction : undefined;
						}
						catch (e) {}
						
						try
						{
							model = !!split.length ? BindingUtils_eval(view, split) : view;
						}
						catch (e) {}
						
						if (!model) 
						{
							conbo.warn(propertyName+' is not defined in this View');
							return;
						}
						
						var opts = conbo.defineValues({propertyName:property}, options);
						var args = [model, property, el, type, parseFunction, opts, param];
						
						bindings = bindings.concat(scope.bindAttribute.apply(scope, args));
					}
					
					// Dispatch a `bind` event from the element at the end of the current call stack
					conbo.defer(function()
					{
						var customEvent;
						
						customEvent = document.createEvent('CustomEvent');
						customEvent.initCustomEvent('bind', false, false, {});					
						
						el.dispatchEvent(customEvent);
					});
				});
				
			});
			
			__definePrivateProperty(view, '__bindings', bindings);
			
			return this;
		},
		
		/**
		 * Removes all data binding from the specified View instance
		 * @param 	{conbo.View}			view
		 * @returns	{conbo.BindingUtils}	A reference to this object 
		 */
		unbindView: function(view)
		{
			if (!view)
			{
				throw new Error('view is undefined');
			}
			
			if (!view.__bindings || !view.__bindings.length)
			{
				return this;
			}
			
			var bindings = view.__bindings;
			
			while (bindings.length)
			{
				var binding = bindings.pop();
				
				try
				{
					binding[0].removeEventListener(binding[1], binding[2]);
				}
				catch (e) {}
			}
			
			delete view.__bindings;
			
			return this;
		},
		
		/**
		 * Applies View and Glimpse classes DOM elements based on their cb-view 
		 * attribute or tag name
		 * 
		 * @param	{HTMLElement} 			rootView - DOM element, View or Application class instance
		 * @param	{conbo.Namespace} 		namespace - The current namespace
		 * @param	{string} 				[type=view] - View type, 'view' or 'glimpse'
		 * @returns	{conbo.BindingUtils}	A reference to this object 
		 */
		applyViews: function(rootView, namespace, type)
		{
			type || (type = 'view');
			
			if (['view', 'glimpse'].indexOf(type) == -1)
			{
				throw new Error(type+' is not a valid type parameter for applyView');
			}
			
			var typeClass = conbo[type.charAt(0).toUpperCase()+type.slice(1)],
				scope = this
				;
			
			var rootEl = conbo.isElement(rootView) ? rootView : rootView.el;
			
			for (var className in namespace)
			{
				var classReference = scope.getClass(className, namespace);
				var isView = conbo.isClass(classReference, conbo.View);
				var isGlimpse = conbo.isClass(classReference, conbo.Glimpse) && !isView;
				
				if ((type == 'glimpse' && isGlimpse) || (type == 'view' && isView))
				{
					var tagName = conbo.toKebabCase(className);
					var nodes = conbo.toArray(rootEl.querySelectorAll(tagName+':not(.cb-'+type+'):not([cb-repeat]), [cb-'+type+'='+className+']:not(.cb-'+type+'):not([cb-repeat])'));
					
					nodes.forEach(function(el)
					{
						var ep = __ep(el);

						// Ignore anything that's inside a cb-repeat
						if (!ep.closest('[cb-repeat]'))
						{
							var closestView = ep.closest('.cb-view');
							var context = closestView ? closestView.cbView.subcontext : rootView.subcontext;
							
							new classReference({el:el, context:context});
						}
					});
				}
			}
			
			return this;
		},
		
		/**
		 * Bind the property of one EventDispatcher class instance (e.g. Hash or View) to another
		 * 
		 * @param 	{conbo.EventDispatcher}	source - Class instance which extends conbo.EventDispatcher
		 * @param 	{string}				sourcePropertyName - Source property name
		 * @param 	{*}						destination - Object or class instance which extends conbo.EventDispatcher
		 * @param 	{string}				[destinationPropertyName] Defaults to same value as sourcePropertyName
		 * @param 	{Boolean}				[twoWay=false] - Apply 2-way binding
		 * @returns	{conbo.BindingUtils}	A reference to this object 
		 */
		bindProperty: function(source, sourcePropertyName, destination, destinationPropertyName, twoWay)
		{
			if (!(source instanceof conbo.EventDispatcher))
			{
				throw new Error(sourcePropertyName+' source is not EventDispatcher');
			}
			
			var scope = this;
			
			destinationPropertyName || (destinationPropertyName = sourcePropertyName);
			
			BindingUtils__makeBindable(source, sourcePropertyName);
			
			source.addEventListener('change:'+sourcePropertyName, function(event)
			{
				if (!(destination instanceof conbo.EventDispatcher))
				{
					destination[destinationPropertyName] = event.value;
					return;
				}
				
				BindingUtils__set.call(destination, destinationPropertyName, event.value);
			});
			
			if (twoWay && destination instanceof conbo.EventDispatcher)
			{
				this.bindProperty(destination, destinationPropertyName, source, sourcePropertyName);
			}
			
			return this;
		},
		
		/**
		 * Call a setter function when the specified property of a EventDispatcher 
		 * class instance (e.g. Hash or Model) is changed
		 * 
		 * @param 	{conbo.EventDispatcher}	source				Class instance which extends conbo.EventDispatcher
		 * @param 	{string}			propertyName
		 * @param 	{Function}			setterFunction
		 * @returns	{conbo.BindingUtils}	A reference to this object 
		 */
		bindSetter: function(source, propertyName, setterFunction)
		{
			if (!(source instanceof conbo.EventDispatcher))
			{
				throw new Error('Source is not EventDispatcher');
			}
			
			if (!conbo.isFunction(setterFunction))
			{
				if (!setterFunction || !(propertyName in setterFunction))
				{
					throw new Error('Invalid setter function');
				}
				
				setterFunction = setterFunction[propertyName];
			}
			
			BindingUtils__makeBindable(source, propertyName);
			
			source.addEventListener('change:'+propertyName, function(event)
			{
				setterFunction(event.value);
			});
			
			return this;
		},
		
		/**
		 * Default parse function
		 * 
		 * @param	{*} 		value - The value to be parsed
		 * @returns	{*}			The parsed value
		 */
		defaultParseFunction: function(value)
		{
			return typeof(value) == 'undefined' ? '' : value;
		},
		
		/**
		 * Attempt to convert string into a conbo.Class in the specified namespace
		 * 
		 * @param 		{string} 			className - The name of the class
		 * @param 		{conbo.Namespace}	namespace - The namespace containing the class
		 * @returns		{*}
		 */
		getClass: function(className, namespace)
		{
			if (!className || !namespace) return;
			
			try
			{
				var classReference = namespace[className];
				
				if (conbo.isClass(classReference)) 
				{
					return classReference;
				}
			}
			catch (e) {}
		},
		
		/**
		 * Register a custom attribute handler
		 * 
		 * @param		{string}	name - camelCase version of the attribute name (must include a namespace prefix)
		 * @param		{Function}	handler - function that will handle the data bound to the element
		 * @param 		{boolean}	readOnly - Whether or not the attribute is read-only (default: false)
		 * @param 		{boolean}	[raw=false] - Whether or not parameters should be passed to the handler as a raw String instead of a bound value
		 * @returns		{conbo.BindingUtils}	A reference to this object 
		 * 
		 * @example 
		 * // HTML: <div my-font-name="myProperty"></div>
		 * conbo.bindingUtils.registerAttribute('myFontName', function(el, value, options, param)
		 * {
		 *		el.style.fontName = value;
		 * });
		 */
		registerAttribute: function(name, handler, readOnly, raw)
		{
			if (!conbo.isString(name) || !conbo.isFunction(handler))
			{
				conbo.warn("registerAttribute: both 'name' and 'handler' parameters are required");
				return this;
			}
			
			var split = conbo.toUnderscoreCase(name).split('_');
			
			if (split.length < 2)
			{
				conbo.warn("registerAttribute: "+name+" does not include a namespace, e.g. "+conbo.toCamelCase('my-'+name));
				return this;
			}
			
			var ns = split[0];
			
			if (BindingUtils__reservedNamespaces.indexOf(ns) != -1)
			{
				conbo.warn("registerAttribute: custom attributes cannot to use the "+ns+" namespace");
				return this;
			}
			
			BindingUtils__registeredNamespaces = conbo.union(BindingUtils__registeredNamespaces, [ns]);
			
			conbo.assign(handler, 
			{
				readOnly: !!readOnly,
				raw: !!raw
			});
			
			BindingUtils__customAttrs[name] = handler;
			
			return this;
		},
		
		/**
		 * Register one or more custom attribute handlers 
		 * 
		 * @see			#registerAttribute
		 * @param 		{Object}				handlers - Object containing one or more custom attribute handlers
		 * @param 		{boolean}				[readOnly=false] - Whether or not the attributes are read-only
		 * @returns		{conbo.BindingUtils}	A reference to this object 
		 * 
		 * @example
		 * conbo.bindingUtils.registerAttributes({myFoo:myFooFunction, myBar:myBarFunction});
		 */
		registerAttributes: function(handlers, readOnly)
		{
			for (var a in handlers)
			{
				this.addAttribute(a, handlers[a], readOnly);
			}
			
			return this;
		},
		
		/**
		 * Parses a template, preparing values in {{double}} curly brackets to
		 * be replaced with bindable text nodes 
		 * 
		 * @param	{string}	template - String containing a View template
		 * @returns	{string}	The parsed template
		 */
		parseTemplate: function(template)
		{
			return (template || '').replace(/{{(.+?)}}/g, function(ignored, propName) 
			{
				return '<cb-text cb-bind="'+propName.trim()+'"></cb-text>';
			});
		},

		toString: function()
		{
			return 'conbo.BindingUtils';
		},
	});
	
	/**
	 * Default instance of the BindingUtils data-binding utility class
	 * @memberof	conbo
	 * @type		{conbo.BindingUtils}
	 */
	conbo.bindingUtils = new conbo.BindingUtils();
	
})();

/**
 * Mutation Observer
 * 
 * Simplified mutation observer dispatches ADD and REMOVE events following 
 * changes in the DOM, compatible with IE9+ and all modern browsers
 * 
 * @class		MutationObserver
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options
 * @fires		conbo.ConboEvent#ADD
 * @fires		conbo.ConboEvent#REMOVE
 */
conbo.MutationObserver = conbo.EventDispatcher.extend(
/** @lends conbo.MutationObserver.prototype */
{
	initialize: function()
	{
		this.bindAll();
	},
	
	observe: function(el)
	{
		this.disconnect();
		
		if (!el) return;
		
		var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
		
		// Modern browsers
		if (MutationObserver)
		{
			var mo = new MutationObserver((function(mutations, observer)
			{
				var added = mutations[0].addedNodes;
				var removed = mutations[0].removedNodes;
				
				if (added.length)
				{
					this.__addHandler(conbo.toArray(added));
				}
			
				if (mutations[0].removedNodes.length)
				{
					this.__removeHandler(conbo.toArray(removed));
				}
			}).bind(this));
			
			mo.observe(el, {childList:true, subtree:true});
			
			this.__mo = mo;
		}
		// IE9
		else
		{
			el.addEventListener('DOMNodeInserted', this.__addHandler);
			el.addEventListener('DOMNodeRemoved', this.__removeHandler);
			
			this.__el = el;
		}
		
		return this;
	},
	
	disconnect: function()
	{
		var mo = this.__mo;
		var el = this.__el;
		
		if (mo) 
		{
			mo.disconnect();
		}
		
		if (el) 
		{
			el.removeEventListener('DOMNodeInserted', this.__addHandler);
			el.removeEventListener('DOMNodeRemoved', this.__removeHandler);
		}
		
		return this;
	},
	
	/**
	 * @private
	 */
	__addHandler: function(event)
	{
		var nodes = conbo.isArray(event)
			? event
			: [event.target];
		
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.ADD, {nodes:nodes}));
	},
	
	/**
	 * @private
	 */
	__removeHandler: function(event)
	{
		var nodes = conbo.isArray(event)
			? event
			: [event.target];
		
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.REMOVE, {nodes:nodes}));
	}
});

/**
 * Element Proxy
 * 
 * Wraps an Element to add cross browser or simplified functionality;
 * think of it as "jQuery nano"
 * 
 * @class		ElementProxy
 * @memberof	conbo
 * @augments	conbo.EventProxy
 * @author 		Neil Rackett
 * @deprecated	This class will be replaced by standard HTML5 functionality in future and may be removed without notice
 * @param 		{Element} el - Element to be proxied
 */
conbo.ElementProxy = conbo.EventProxy.extend(
/** @lends conbo.ElementProxy.prototype */
{
	/**
	 * Returns object containing the value of all attributes on a DOM element
	 * 
	 * @returns		{Object}
	 * 
	 * @example
	 * ep.attributes; // results in something like {src:"foo/bar.jpg"}
	 */
	getAttributes: function()
	{
		var el = this.__obj;
		var a = {};
		
		if (el)
		{
			conbo.forEach(el.attributes, function(p)
			{
				a[conbo.toCamelCase(p.name)] = p.value;
			});
		}
		
		return a;
	},
	
	/**
	 * Sets the attributes on a DOM element from an Object, converting camelCase to kebab-case, if needed
	 * 
	 * @param 		{Element}	obj - Object containing the attributes to set
	 * @returns		{conbo.ElementProxy}
	 * 
	 * @example
	 * ep.setAttributes({foo:1, bar:"red"});
	 */
	setAttributes: function(obj)
	{
		var el = this.__obj;
		
		if (el && obj)
		{
			conbo.forEach(obj, function(value, name)
			{
				el.setAttribute(conbo.toKebabCase(name), value);
			});
		}
		
		return this;
	},
	
	/**
	 * @see #getAttributes
	 */
	get attributes()
	{
		return this.getAttributes();
	},
	
	/**
	 * @see #setAttributes
	 */
	set attributes(value)
	{
		return this.setAttributes(value);
	},
	
	/**
	 * Returns object containing the value of all cb-* attributes on a DOM element
	 * 
	 * @returns		{Array}
	 * 
	 * @example
	 * ep.cbAttributes.view;
	 */
	get cbAttributes()
	{
		var el = this.__obj;
		var a = {};
		
		if (el)
		{
			conbo.forEach(el.attributes, function(p)
			{
				if (p.name.indexOf('cb-') === 0)
				{
					a[conbo.toCamelCase(p.name.substr(3))] = p.value;
				}
			});
		}
		
		return a;
	},
	
	/**
	 * Add the specified CSS class(es) to the element
	 *  
	 * @param 		{string}	className - One or more CSS class names, separated by spaces
	 * @returns		{conbo.ElementProxy}
	 */
	addClass: function(className)
	{
		var el = this.__obj;
		
		if (el instanceof Element && className)
		{
			var classNames = className.trim().split(' ');

			// IE11 doesn't support multiple parameters
			while (className = classNames.pop())
			{
				el.classList.add(className);
			}
		}
		
		return this;
	},
	
	/**
	 * Remove the specified CSS class(es) from the element
	 * 
	 * @param 		{string|function}		className - One or more CSS class names, separated by spaces, or a function extracts the classes to be removed from the existing className property
	 * @returns		{conbo.ElementProxy}
	 */
	removeClass: function(className)
	{
		var el = this.__obj;
		
		if (el instanceof Element && className)
		{
			if (conbo.isFunction(className))
			{
				className = className(el.className);
			}
			
			var classNames = className.trim().split(' ');

			// IE11 doesn't support multiple parameters
			while (className = classNames.pop())
			{
				el.classList.remove(className);
			}
		}
		
		return this;
	},
	
	/**
	 * Is this element using the specified CSS class?
	 *  
	 * @param 		{string}	className - CSS class name
	 * @returns		{boolean}
	 */
	hasClass: function(className)
	{
		var el = this.__obj;
		
		return el instanceof Element && className
			? el.classList.contains(className)
			: false;
	},
	
	/**
	 * Finds the closest parent element matching the specified selector
	 *  
	 * @param 		{string}	selector - Query selector
	 * @returns		{Element}
	 */
	closest: function(selector)
	{
		var el = this.__obj;
		
		if (el)
		{
			var matchesFn;
			
			['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) 
			{
				if (typeof document.body[fn] == 'function') 
				{
					matchesFn = fn;
					return true;
				}
				
				return false;
			});
			
			var parent;
			
			// traverse parents
			while (el)
			{
				parent = el.parentElement;
				
				if (parent && parent[matchesFn](selector)) 
				{
					return parent;
				}
				
				el = parent;
			}
		}
	},
	
});

/**
 * Interface class for data renderers, for example an item renderer for
 * use with the cb-repeat attribute
 * 
 * @member		{object}	IDataRenderer
 * @memberof	conbo
 * @author 		Neil Rackett
 */
conbo.IDataRenderer =
{
	/**
	 * Data to be rendered
	 * @type	{*}
	 */
	data: undefined,
	
	/**
	 * Index of the current item
	 * @type	{number}
	 */
	index: -1,
	
	/**
	 * Is this the last item in the list?
	 * @type	{boolean}
	 */
	isLast: false,
	
	/**
	 * The list containing the data for this item
	 * @type	{(conbo.List|Array)}
	 */
	list: undefined
};

/**
 * Glimpse
 * 
 * A lightweight element wrapper that has no dependencies, no context and 
 * no data binding, but is able to apply a super-simple template.
 * 
 * It's invisible to View, so it's great for creating components, and you 
 * can bind data to it using the `cb-data` attribute to set the data 
 * property of your Glimpse
 * 
 * @class		Glimpse
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options
 */
conbo.Glimpse = conbo.EventDispatcher.extend(
/** @lends conbo.Glimpse.prototype */
{
	/**
	 * @member		{*}			data - Arbitrary data
	 * @memberof	conbo.Glimpse.prototype
	 */

	/**
	 * @member		{string}	template - Template to apply to the Glimpse's element
	 * @memberof	conbo.Glimpse.prototype
	 */

	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param {Object} [options]
	 * @private
	 */
	__construct: function(options)
	{
		this.__setEl(options.el || document.createElement(this.tagName));
		
		if (this.template)
		{
			this.el.innerHTML = this.template;
		}
	},
	
	/**
	 * When a new instance of this class is created without specifying an element,
	 * it will use this tag name (the default is `div`)
	 * @type	{string}
	 */
	get tagName()
	{
		return this.__tagName || 'div';
	},
	
	set tagName(value)
	{
		__definePrivateProperty(this, '__tagName', value);
	},
	
	/**
	 * A reference to this class instance's element
	 * @type	{HTMLElement}
	 */
	get el()
	{
		return this.__el;
	},
	
	toString: function()
	{
		return 'conbo.Glimpse';
	},
	
	/**
	 * Set this View's element
	 * @private
	 */
	__setEl: function(el)
	{
		var attrs = conbo.assign({}, this.attributes);
		
		if (this.id && !el.id) 
		{
			attrs.id = this.id;
		}
		
		el.classList.add('cb-glimpse');
		el.classList.add(this.__className);
		el.cbGlimpse = this;
		
		for (var attr in attrs)
		{
			el.setAttribute(conbo.toKebabCase(attr), attrs[attr]);		
		}		
		
		if (this.style)
		{
			el.style = conbo.assign(el.style, this.style);
		}
		
		__definePrivateProperty(this, '__el', el);
		
		return this;
	}
	
});

__denumerate(conbo.Glimpse.prototype);

var View__templateCache = {};

/**
 * View
 * 
 * Creating a conbo.View creates its initial element outside of the DOM,
 * if an existing element is not provided...
 * 
 * @class		View
 * @memberof	conbo
 * @augments	conbo.Glimpse
 * @author 		Neil Rackett
 * @param 		{Object}	[options] - Object containing optional initialisation options, including 'attributes', 'className', 'data', 'el', 'id', 'tagName', 'template', 'templateUrl'
 * @fires		conbo.ConboEvent#ADD
 * @fires		conbo.ConboEvent#DETACH
 * @fires		conbo.ConboEvent#REMOVE
 * @fires		conbo.ConboEvent#BIND
 * @fires		conbo.ConboEvent#UNBIND
 * @fires		conbo.ConboEvent#TEMPLATE_COMPLETE
 * @fires		conbo.ConboEvent#TEMPLATE_ERROR
 * @fires		conbo.ConboEvent#PREINITIALIZE
 * @fires		conbo.ConboEvent#INITIALIZE
 * @fires		conbo.ConboEvent#INIT_COMPLETE
 * @fires		conbo.ConboEvent#CREATION_COMPLETE
 */
conbo.View = conbo.Glimpse.extend(
/** @lends 		conbo.View.prototype */
{
	/**
	 * @member		{Object}	attributes - Attributes to apply to the View's element
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{string}	className - CSS class name(s) to apply to the View's element
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{Object}	data - Arbitrary data Object
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{string}	id - ID to apply to the View's element
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{any}		style - Object containing CSS styles to apply to this View's element
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{string}	tagName - The tag name to use for the View's element (if no element specified)
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{string}	template - Template to apply to the View's element
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{string}	templateUrl - Template to load and apply to the View's element
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{boolean}	templateCacheEnabled - Whether or not the contents of templateUrl should be cached on first load for use with future instances of this View class (default: true)
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * @member		{boolean}	autoInitTemplate - Whether or not the template should automatically be loaded and applied, rather than waiting for the user to call initTemplate (default: true)
	 * @memberof	conbo.View.prototype
	 */
	
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 * @private
	 */
	__construct: function(options)
	{
		options = conbo.clone(options) || {};
		
		if (options.className && this.className)
		{
			options.className += ' '+this.className;
		}
		
		var viewOptions = conbo.union
		(
			[
				'attributes',
				'className', 
				'data', 
				'id', 
				'style', 
				'tagName', 
				'template', 
				'templateUrl',
				'templateCacheEnabled',
				'autoInitTemplate',
			],
			
			// Adds interface properties
			conbo.intersection
			(
				conbo.variables(this, true), 
				conbo.variables(options)
			)
		);
		
		conbo.assign(this, conbo.pick(options, viewOptions));
		conbo.makeBindable(this);
		
		this.context = options.context;
		this.__setEl(options.el || document.createElement(this.tagName));
	},

	/**
	 * @private
	 */
	__postInitialize: function(options)
	{
		__definePrivateProperty(this, '__initialized', true);
		
		this.__content =  this.el.innerHTML;
		
		if (this.autoInitTemplate !== false)
		{
			this.initTemplate();
		}
	},
	
	/**
	 * This View's element
	 * @type		{HTMLElement}
	 */
	get el()
	{
		return this.__el;
	},
	
	/**
	 * Has this view completed its life cycle phases?
	 * @type	{boolean}
	 */
	get initialized()
	{
		return !!this.__initialized;
	},
	
	/**
	 * Returns a reference to the parent View of this View, based on this 
	 * View element's position in the DOM
	 * @type	{conbo.View}
	 */
	get parent()
	{
		if (this.initialized)
		{
			return this.__getParent('.cb-view');
		}
	},
	
	/**
	 * Returns a reference to the parent Application of this View, based on
	 * this View element's position in the DOM
	 * @type	{conbo.Application}
	 */
	get parentApp()
	{
		if (this.initialized)
		{
			return this.__getParent('.cb-app');
		}
	},
	
	/**
	 * Does this view have a template?
	 * @type	{boolean}
	 */
	get hasTemplate()
	{
		return !!(this.template || this.templateUrl);
	},
	
	/**
	 * The element into which HTML content should be placed; this is either the 
	 * first DOM element with a `cb-content` or the root element of this view
	 * @type	{HTMLElement}
	 */
	get content()
	{
		return this.querySelector('[cb-content]');
	},
	
	/**
	 * Does this View support HTML content?
	 * @type	{boolean}
	 */
	get hasContent()
	{
		return !!this.content;
	},
	
	/**
	 * A View's body is the element to which content should be added:
	 * the View's content, if it exists, or the View's main element, if it doesn't
	 * @type	{HTMLElement}
	 */
	get body()
	{
		return this.content || this.el;
	},
	
	/**
	 * The context that will automatically be applied to children
	 * when binding or appending Views inside of this View
	 * @type	{conbo.Context}
	 */
	get subcontext()
	{
		return this.__subcontext || this.context;
	},
	
	set subcontext(value)
	{
		this.__subcontext = value;
	},
	
	/**
	 * The current view state.
	 * When set, adds "cb-state-x" CSS class on the View's element, where "x" is the value of currentState.
	 * @type	{string}
	 */
	get currentState()
	{
		return this.__currentState || '';
	},

	set currentState(value)
	{
		// If the view is ready the state class is applied immediately, otherwise it gets set in __setEl()
		if (this.el)
		{
			if (this.currentState)
			{
				this.el.classList.remove('cb-state-'+this.currentState);
			}

			if (value)
			{
				this.el.classList.add('cb-state-'+value);
			}
		}

		this.__currentState = value;
		this.dispatchChange('currentState');
	},

	/**
	 * Convenience method for conbo.ConboEvent.TEMPLATE_COMPLETE event handler
	 */
	templateComplete: function() {},
	
	/**
	 * Convenience method for conbo.ConboEvent.CREATION_COMPLETE event handler
	 */
	creationComplete: function() {},

	/**
	 * Uses querySelector to find the first matching element contained within the
	 * current View's element, but not within the elements of child Views
	 * 
	 * @param	{string}		selector - The selector to use
	 * @param	{boolean}		deep - Include elements in child Views?
	 * @returns	{HTMLElement}	The first matching element
	 */
	querySelector: function(selector, deep)
	{
		return this.querySelectorAll(selector, deep)[0];
	},
	
	/**
	 * Uses querySelectorAll to find all matching elements contained within the
	 * current View's element, but not within the elements of child Views
	 * 
	 * @param	{string}		selector - The selector to use
	 * @param	{boolean}		deep - Include elements in child Views?
	 * @returns	{Array}			All elements matching the selector
	 */
	querySelectorAll: function(selector, deep)
	{
		if (this.el)
		{
			var results = conbo.toArray(this.el.querySelectorAll(selector));
			
			if (!deep)
			{
				var views = this.el.querySelectorAll('.cb-view, [cb-view], [cb-app]');
				
				// Remove elements in child Views
				conbo.forEach(views, function(el)
				{
					var els = conbo.toArray(el.querySelectorAll(selector));
					results = conbo.difference(results, els.concat(el));
				});
			}
			
			return results;
		}
		
		return [];
	},
	
	/**
	 * Take the View's element element out of the DOM
	 * @returns	{this}
	 */
	detach: function() 
	{
		try
		{
			var el = this.el;

			if (el.parentNode)
			{
				el.parentNode.removeChild(el);		
				this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.DETACH));
			}
		}
		catch(e) {}
		
		return this;
	},
	
	/**
	 * Remove and destroy this View by taking the element out of the DOM, 
	 * unbinding it, removing all event listeners and removing the View from 
	 * its Context.
	 * 
	 * You should use a REMOVE event handler to destroy any event listeners,
	 * timers or other persistent code you may have added.
	 * 
	 * @returns	{this}
	 */
	remove: function()
	{
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.REMOVE));
		
		if (this.data)
		{
			this.data = undefined;
		}
		
		if (this.subcontext && this.subcontext != this.context)
		{
			this.subcontext.destroy();
			this.subcontext = undefined;
		}
		
		if (this.context)
		{
			this.context.uninject(this);
			this.context.removeEventListener(undefined, undefined, this);
			this.context = undefined;
		}
		
		var children = this.querySelectorAll('.cb-view', true);

		while (children.length)
		{
			var child = children.pop();

			try { child.cbView.remove(); }
			catch (e) {}
		}

		this.unbindView()
			.detach()
			.removeEventListener()
			.destroy()
			;
		
		return this;
	},

	/**
	 * Append this DOM element from one View class instance this class 
	 * instances DOM element
	 * 
	 * @param 		{conbo.View|Function} view - The View instance to append
	 * @returns		{this}
	 */
	appendView: function(view)
	{
		if (arguments.length > 1)
		{
			conbo.forEach(arguments, function(view, index, list) 
			{
				this.appendView(view);
			},
			this);
			
			return this;
		}
	
		if (typeof view === 'function')
		{
			view = new view(this.context);
		}

		if (!(view instanceof conbo.View))
		{
			throw new Error('Parameter must be conbo.View class or instance of it');
		}
	
		this.body.appendChild(view.el);
		
		return this;
	},
	
	/**
	 * Prepend this DOM element from one View class instance this class 
	 * instances DOM element
	 * 
	 * @param 		{conbo.View} view - The View instance to preppend
	 * @returns		{this}
	 */
	prependView: function(view)
	{
		if (arguments.length > 1)
		{
			conbo.forEach(arguments, function(view, index, list) 
			{
				this.prependView(view);
			}, 
			this);
			
			return this;
		}
	
		if (typeof view === 'function')
		{
			view = new view(this.context);
		}
		
		if (!(view instanceof conbo.View))
		{
			throw new Error('Parameter must be conbo.View class or instance of it');
		}
		
		var firstChild = this.body.firstChild;
		
		firstChild
			? this.body.insertBefore(view.el, firstChild)
			: this.appendView(view);
		
		return this;
	},
	
	/**
	 * Automatically bind elements to properties of this View
	 * 
	 * @example	<div cb-bind="property|parseMethod" cb-hide="property">Hello!</div> 
	 * @returns	{this}
	 */
	bindView: function()
	{
		conbo.bindingUtils.bindView(this);
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.BIND));
		return this;
	},
	
	/**
	 * Unbind elements from class properties
	 * @returns	{this}
	 */
	unbindView: function() 
	{
		conbo.bindingUtils.unbindView(this);
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.UNBIND));
		return this;
	},
	
	/**
	 * Initialize the View's template, either by loading the templateUrl
	 * or using the contents of the template property, if either exist
	 * @returns	{this}
	 */
	initTemplate: function()
	{
		var template = this.template;
		
		if (!!this.templateUrl)
		{
			this.loadTemplate();
		}
		else
		{
			if (conbo.isFunction(template))
			{
				template = template(this);
			}
			
			var el = this.el;
			
			if (conbo.isString(template))
			{
				el.innerHTML = this.__parseTemplate(template);
			}
			else if (/{{(.+?)}}/.test(el.textContent))
			{
				el.innerHTML = this.__parseTemplate(el.innerHTML);
			}
			
			this.__initView();
		}
		
		return this;
	},
	
	/**
	 * Load HTML template and use it to populate this View's element
	 * 
	 * @param 	{string}	[url]	- The URL to which the request is sent
	 * @returns	{this}
	 */
	loadTemplate: function(url)
	{
		url || (url = this.templateUrl);
		
		var el = this.body;
		
		this.unbindView();
		
		if (this.templateCacheEnabled !== false && View__templateCache[url])
		{
			el.innerHTML = View__templateCache[url];
			this.__initView();
			
			return this;
		}
		
		var resultHandler = function(event)
		{
			var result = this.__parseTemplate(event.result);
			
			if (this.templateCacheEnabled !== false)
			{
				View__templateCache[url] = result;
			}
			
			el.innerHTML = result;
			this.__initView();
		};
		
		var faultHandler = function(event)
		{
			el.innerHTML = '';
			
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.TEMPLATE_ERROR));
			this.__initView();
		};
		
		conbo
			.httpRequest({url:url, dataType:'text'})
			.then(resultHandler.bind(this), faultHandler.bind(this))
			;
		
		return this;
	},
	
	toString: function()
	{
		return 'conbo.View';
	},

	
	/* INTERNAL */
	
	/**
	 * Set this View's element
	 * @private
	 */
	__setEl: function(el)
	{
		if (!conbo.isElement(el))
		{
			conbo.error('Invalid element passed to View');
			return;
		}
		
		var attrs = conbo.assign({}, this.attributes);
		
		if (this.id && !el.id) 
		{
			attrs.id = this.id;
		}
		
		if (this.style) 
		{
			conbo.assign(el.style, this.style);
		}
		
		var ep = __ep(el);
		
		el.cbView = this;
		
		ep.addClass('cb-view')
			.addClass(this.className)
			.addClass(this.__className)
			.setAttributes(attrs)
			;

		if (this.currentState)
		{
			ep.addClass('cb-state-'+this.currentState);			
		}

		__definePrivateProperty(this, '__el', el);
		
		return this;
	},

	/**
	 * Populate and render the View's HTML content
	 * @private
	 */
	__initView: function()
	{
		if (this.hasTemplate && this.hasContent)
		{
			this.content.innerHTML = this.__content;
		}
		
		delete this.__content;
		
		this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.TEMPLATE_COMPLETE));
		this.templateComplete();
		this.bindView();
		
		conbo.defer(function()
		{
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CREATION_COMPLETE));
			this.creationComplete();
		}, this);
		
		return this;
	},
	
	/**
	 * @private
	 */
	__getParent: function(selector) 
	{
		var el = __ep(this.el).closest(selector);
	    if (el) return el.cbView;
	},
	
	/**
	 * @private
	 */
	__parseTemplate: function(template)
	{
		return conbo.bindingUtils.parseTemplate(template);
	}
	
});

__denumerate(conbo.View.prototype);

/**
 * ItemRenderer
 * 
 * A conbo.View class that implements the conbo.IDataRenderer interface
 * 
 * @class		ItemRenderer
 * @memberof	conbo
 * @augments	conbo.View
 * @augments	conbo.IDataRenderer
 * @param 		{Object} [options] - Object containing initialisation options
 * @see			conbo.View
 * @author 		Neil Rackett
 */
conbo.ItemRenderer = conbo.View.extend().implement(conbo.IDataRenderer);

/**
 * Data to be rendered
 * @member		{*}			data
 * @memberof	conbo.ItemRenderer.prototype
 */

 /**
 * Index of the current item
 * @member		{number}	index
 * @memberof	conbo.ItemRenderer.prototype
 */

/**
 * Is this the last item in the list?
 * @member		{boolean}	isLast
 * @memberof	conbo.ItemRenderer.prototype
 */

/**
 * The list containing the data for this item
 * @member		{(conbo.List|Array)}	list
 * @memberof	conbo.ItemRenderer.prototype
 */

/**
 * Application
 * 
 * Base application class for client-side applications
 * 
 * @class		Application
 * @memberof	conbo
 * @augments	conbo.View
 * @author		Neil Rackett
 * @param 		{Object} options - Object containing optional initialisation options, see View
 * @fires		conbo.ConboEvent#ADD
 * @fires		conbo.ConboEvent#DETACH
 * @fires		conbo.ConboEvent#REMOVE
 * @fires		conbo.ConboEvent#BIND
 * @fires		conbo.ConboEvent#UNBIND
 * @fires		conbo.ConboEvent#TEMPLATE_COMPLETE
 * @fires		conbo.ConboEvent#TEMPLATE_ERROR
 * @fires		conbo.ConboEvent#CREATION_COMPLETE
 */
conbo.Application = conbo.View.extend(
/** @lends conbo.Application.prototype */
{
	/**
	 * @member		{conbo.Namespace} namespace - The application's namespace (uses 'default' namespace if not overridden)
	 * @memberof	conbo.Application.prototype
	 */
	
	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 * @private
	 */
	__construct: function(options)
	{
		options = conbo.clone(options) || {};
		
		if (!(this.namespace instanceof conbo.Namespace))
		{
			this.namespace = conbo();
		}
		
		options.app = this;
		options.context = new this.contextClass(options);

		this.addEventListener(conbo.ConboEvent.CREATION_COMPLETE, this.__creationComplete, {scope:this, once:true});
		
		conbo.View.prototype.__construct.call(this, options);
	},
	
	/**
	 * @private
	 */
	__creationComplete: function(options)
	{
		if (this.initialView)
		{
			this.appendView(this.initialView);
		}
	},

	/**
	 * If specified, this View will be appended immediately after the Application is intialized.
	 * If this property is set to a class, it will be instantiated automatically the first time
	 * this property is read, with initialViewOptions passed to the constructor.
	 * @type	{conbo.View|Function}
	 */
	get initialView()
	{
		if (typeof this.__initialView == 'function')
		{
			var options = conbo.assign({}, this.initialViewOptions, {context:this.context});
			this.initialView = new this.__initialView(options);
		}

		return this.__initialView;
	},

	set initialView(value)
	{
		this.__initialView = value;
	},

	/**
	 * If initialView is a View class, the initialViewOptions will be passed to the
	 * constructor when it is instantiated and added to the application
	 * @type	{*}
	 */
	get initialViewOptions()
	{
		return this.__initialViewOptions || {};
	},

	set initialViewOptions(value)
	{
		this.__initialViewOptions = value;
	},

	/**
	 * Default context class to use
	 * You'll normally want to override this with your own
	 * @type	{conbo.Context}
	 */
	get contextClass() 
	{
		return this.__contextClass || conbo.Context;
	},
	
	set contextClass(value)
	{
		this.__contextClass = value;
	},
	
	/**
	 * If true, the application will automatically apply Glimpse and View 
	 * classes to elements when they're added to the DOM 
	 * @type	{boolean}
	 */
	get observeEnabled()
	{
		return !!this.__mo;
	},
	
	set observeEnabled(value)
	{
		if (value == this.observeEnabled) return;
		
		var mo;
		
		if (value)
		{
			mo = new conbo.MutationObserver();
			mo.observe(this.el);
			
			mo.addEventListener(conbo.ConboEvent.ADD, function(event)
			{
				conbo.bindingUtils
					.applyViews(this, this.namespace)
					.applyViews(this, this.namespace, 'glimpse')
					;
			}, 
			{scope:this});
			
			this.__mo = mo;
		}
		else if (this.__mo)
		{
			mo = this.__mo;
			mo.removeEventListener();
			mo.disconnect();
			
			delete this.__mo;
		}
		
		this.dispatchChange('observeEnabled');
		
		return this;
	},
	
	toString: function()
	{
		return 'conbo.Application';
	},
	
	/**
	 * @private
	 */
	__setEl: function(element)
	{
		conbo.View.prototype.__setEl.call(this, element);
		__ep(this.el).addClass('cb-app');
		return this;
	},
	
});

__denumerate(conbo.Application.prototype);

/**
 * conbo.Command
 * 
 * Base class for commands to be registered in your Context 
 * using mapCommand(...)
 * 
 * @class		Command
 * @memberof	conbo
 * @augments	conbo.ConboClass
 * @author		Neil Rackett
 * @param 		{Object} options - Object containing optional initialisation options, including 'context' (Context)
 */
conbo.Command = conbo.ConboClass.extend(
/** @lends conbo.Command.prototype */
{
	/**
	 * @member		{conbo.Context}	context - Application context
	 * @memberof	conbo.Command.prototype
	 */

	/**
	 * @member		{conbo.Event}	event - The event that caused this command to execute
	 * @memberof	conbo.Command.prototype
	 */

	/**
	 * Constructor: DO NOT override! (Use initialize instead)
	 * @param options
	 * @private
	 */
	__construct: function(options)
	{
		this.event = options.event || {};
	},
	
	/**
	 * Execute: should be overridden
	 * 
	 * When a Command is called in response to an event registered with the
	 * Context, the class is instantiated, this method is called then the 
	 * class instance is destroyed
	 */
	execute: function() {},
	
	toString: function()
	{
		return 'conbo.Command';
	}
	
}).implement(conbo.IInjectable);

__denumerate(conbo.Command.prototype);

/**
 * HTTP Request
 * 
 * Sends data to and/or loads data from a URL; advanced requests can be made 
 * by passing a single options object, roughly analogous to the jQuery.ajax() 
 * settings object plus `resultClass` and `makeObjectsBindable` properties;
 * or by passing URL, data and method parameters.
 * 
 * @example		conbo.httpRequest("http://www.foo.com/bar", {user:1}, "GET");
 * @example		conbo.httpRequest({url:"http://www.foo.com/bar", data:{user:1}, method:"GET", headers:{'X-Token':'ABC123'}});
 * 
 * @see			http://api.jquery.com/jquery.ajax/
 * @memberof	conbo
 * @param 		{string|object}		urlOrOptions - URL string or Object containing URL and other settings for the HTTP request
 * @param 		{Object}			data - Data to be sent with request (ignored when using options object)
 * @param 		{string}			method - HTTP method to use, e.g. "GET" or "POST" (ignored when using options object)
 * @returns		{Promise}
 */
conbo.httpRequest = function(options)
{
	// Simple mode
	if (conbo.isString(options))
	{
		options = {url:options, data:arguments[1], method:arguments[2]};
	}
	
	if (!conbo.isObject(options) || !options.url)
	{
		throw new Error('httpRequest called without specifying a URL');
	}
	
	return new Promise(function(resolve, reject)
	{
		var xhr = new XMLHttpRequest();
		var aborted;
		var url = options.url;
		var method = (options.method || options.type || "GET").toUpperCase();
		var data = options.data || options.body;
		var headers = options.headers || {};
		var timeoutTimer;
		var contentType = conbo.getValue(headers, "Content-Type", false) || options.contentType || conbo.CONTENT_TYPE_JSON;
		var dataType = options.dataType || conbo.DATA_TYPE_JSON;
		var decodeFunction = options.decodeFunction || options.dataFilter;
		
		var getXml = function()
		{
			if (xhr.responseType === "document") 
			{
				return xhr.responseXML;
			}
			
			var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
			
			if (xhr.responseType === "" && !firefoxBugTakenEffect) 
			{
				return xhr.responseXML;
			}
		
			return null;
		};
		
		var getResult = function() 
		{
			// TODO Handle Chrome with requestType=blob throwing errors when testing access to responseText
			var result = xhr.response || xhr.responseText || getXml(xhr);
			
			if (conbo.isFunction(decodeFunction))
			{
				result = decodeFunction(result);
			}
			else
			{
				switch (dataType)
				{
					case conbo.DATA_TYPE_SCRIPT:
					{
						(function() { eval(result); }).call(options.scope || window);
						break;
					}
					
					case conbo.DATA_TYPE_JSON:
					{
						try { result = JSON.parse(result); }
						catch (e) { result = undefined; }
						
						break;
					}
					
					case conbo.DATA_TYPE_TEXT:
					{
						// Nothing to do
						break;
					}
				}
			}
			
			var resultClass = options.resultClass;
			
			if (!resultClass && options.makeObjectsBindable)
			{
				switch (true)
				{
					case conbo.isArray(result):
						resultClass = conbo.List;
						break;
					
					case conbo.isObject(result):
						resultClass = conbo.Hash;
						break;
				}
			}
			
			if (resultClass)
			{
				result = new resultClass({source:result});
			}
			
			return result;
		};

		var getResponseHeaders = function()
		{
			var responseHeaders = xhr.getAllResponseHeaders();
			var newValue = {};
			
			responseHeaders.split('\r\n').forEach(function(header)
			{
				var splitIndex = header.indexOf(':');
				var propName = header.substr(0,splitIndex).trim();
				
				newValue[propName] = header.substr(splitIndex+1).trim();
			});
			
			return newValue;
		};
		
		var errorHandler = function() 
		{
			clearTimeout(timeoutTimer);
			
			var response = 
			{
				fault: getResult(),
				responseHeaders: getResponseHeaders(),
				status: xhr.status,
				method: method,
				url: url,
				xhr: xhr
			};
			
			reject(new conbo.ConboEvent(conbo.ConboEvent.FAULT, response));
		};
		
		// will load the data & process the response in a special response object
		var loadHandler = function() 
		{
			if (aborted) return;
			
			clearTimeout(timeoutTimer);
			
			var status = (xhr.status === 1223 ? 204 : xhr.status);
			
			if (status === 0 || status >= 400)
			{
				errorHandler();
				return;
			}
			
			var response = 
			{
				result: getResult(),
				responseHeaders: getResponseHeaders(),
				status: status,
				method: method,
				url: url,
				xhr: xhr
			};
			
			resolve(new conbo.ConboEvent(conbo.ConboEvent.RESULT, response));
		}
		
		var readyStateChangeHandler = function() 
		{
			if (xhr.readyState === 4) 
			{
				conbo.defer(loadHandler);
			}
		};
		
		if (method !== "GET" && method !== "HEAD") 
		{
			conbo.getValue(headers, "Content-Type", false) || (headers["Content-Type"] = contentType);
			
			if (contentType == conbo.CONTENT_TYPE_JSON && conbo.isObject(data))
			{
				data = JSON.stringify(data);
			}
		}
		else if (method === 'GET' && conbo.isObject(data))
		{
			var query = conbo.toQueryString(data);
			if (query) url += '?'+query;
			data = undefined;
		}
		
		'onload' in xhr
			? xhr.onload = loadHandler // XHR2
			: xhr.onreadystatechange = readyStateChangeHandler; // XHR1 (should never be needed)
		
		xhr.onerror = errorHandler;
		xhr.onprogress = function() {}; // IE9 must have unique onprogress function
		xhr.onabort = function() { aborted = true; };
		xhr.ontimeout = errorHandler;
		
		xhr.open(method, url, true, options.username, options.password);
		xhr.withCredentials = !!options.withCredentials;
		
		// not setting timeout on using XHR because of old webkits not handling that correctly
		// both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
		if (options.timeout > 0) 
		{
			timeoutTimer = setTimeout(function()
			{
				if (aborted) return;
				aborted = true; // IE9 may still call readystatechange
				xhr.abort("timeout");
				errorHandler();
			}, 
			options.timeout);
		}
		
		for (var key in headers)
		{
			if (headers.hasOwnProperty(key))
			{
				xhr.setRequestHeader(key, headers[key]);
			}
		}

		if ("responseType" in options) 
		{
			xhr.responseType = options.responseType;
		}

		if (typeof options.beforeSend === "function") 
		{
			options.beforeSend(xhr);
		}
		
		// Microsoft Edge browser sends "undefined" when send is called with undefined value.
		// XMLHttpRequest spec says to pass null as data to indicate no data
		// See https://github.com/naugtur/xhr/issues/100.
		xhr.send(data || null);

	});		

};

/**
 * HTTP Service
 * 
 * Base class for HTTP data services, with default configuration designed 
 * for use with JSON REST APIs.
 * 
 * For XML data sources, you will need to override decodeFunction to parse 
 * response data, change the contentType and implement encodeFunction if 
 * you're using RPC.  
 * 
 * @class		HttpService
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing optional initialisation options, including 'rootUrl', 'contentType', 'dataType', 'headers', 'encodeFunction', 'decodeFunction', 'resultClass','makeObjectsBindable'
 * @fires		conbo.ConboEvent#RESULT
 * @fires		conbo.ConboEvent#FAULT
 */
conbo.HttpService = conbo.EventDispatcher.extend(
/** @lends conbo.HttpService.prototype */
{
	__construct: function(options)
	{
		options = conbo.setDefaults(options, 
		{
			contentType: conbo.CONTENT_TYPE_JSON
		});
		
		conbo.assign(this, conbo.setDefaults(conbo.pick(options, 
		    'rootUrl', 
		    'contentType', 
		    'dataType', 
		    'headers', 
		    'encodeFunction', 
		    'decodeFunction', 
		    'resultClass',
		    'makeObjectsBindable'
		), {
			dataType: 'json'
		}));
		
		var verbs = ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'];
		
		verbs.forEach(function(verb)
		{
			this[verb.toLowerCase()] = function(command, data, method, resultClass)
			{
				return this.call(command, data, verb, resultClass);
			};
		}, 
		this);
		
		conbo.EventDispatcher.prototype.__construct.apply(this, arguments);
	},
	
	/**
	 * The root URL of the web service
	 */
	get rootUrl()
	{
		return this._rootUrl || '';
	},
	
	set rootUrl(value)
	{
		value = String(value);
		
		if (value && value.slice(-1) != '/')
		{
			value += '/';
		}
		
		this._rootUrl = value;
	},
	
	/**
	 * Call a method of the web service using the specified verb
	 * 
	 * @param	{string}	command - The name of the command
	 * @param	{Object}	[data] - Object containing the data to send to the web service
	 * @param	{string}	[method=GET] - GET, POST, etc (default: GET)
	 * @param	{Class}		[resultClass] - Optional
	 * @returns	{Promise}
	 */
	call: function(command, data, method, resultClass)
	{
		var scope = this;

		data = conbo.clone(data || {});
		command = this.parseUrl(command, data);
		data = this.encodeFunction(data, method);
		
		return new Promise(function(resolve, reject)
		{
			conbo.httpRequest
			({
				data: data,
				type: method || 'GET',
				headers: scope.headers,
				url: (scope.rootUrl+command).replace(/\/$/, ''),
				contentType: scope.contentType || conbo.CONTENT_TYPE_JSON,
				dataType: scope.dataType,
				dataFilter: scope.decodeFunction,
				resultClass: resultClass || scope.resultClass, 
				makeObjectsBindable: scope.makeObjectsBindable
			})
			.then(function(event)
			{
				scope.dispatchEvent(event);
				resolve(event);
			})
			.catch(function(event)
			{
				scope.dispatchEvent(event);
				reject(event);
			});
		});
	},
	
	/**
	 * Call a method of the web service using the POST verb
	 * 
	 * @memberof	conbo.HttpService.prototype
	 * @method		post
	 * @param		{string}	command - The name of the command
	 * @param		{Object}	[data] - Object containing the data to send to the web service
	 * @param		{Class}		[resultClass] - Optional
	 * @returns		{Promise}
	 */
	
	/**
	 * Call a method of the web service using the GET verb
	 * 
	 * @memberof	conbo.HttpService.prototype
	 * @method		get 
	 * @param		{string}	command - The name of the command
	 * @param		{Object}	[data] - Object containing the data to send to the web service
	 * @param		{Class}		[resultClass] - Optional
	 * @returns		{Promise}
	 */
	
	/**
	 * Call a method of the web service using the PUT verb
	 * 
	 * @memberof	conbo.HttpService.prototype
	 * @method		put
	 * @param		{string}	command - The name of the command
	 * @param		{Object}	[data] - Object containing the data to send to the web service
	 * @param		{Class}		[resultClass] - Optional
	 * @returns		{Promise}
	 */
	
	/**
	 * Call a method of the web service using the PATCH verb
	 * 
	 * @memberof	conbo.HttpService.prototype
	 * @method		patch
	 * @param		{string}	command - The name of the command
	 * @param		{Object}	[data] - Object containing the data to send to the web service
	 * @param		{Class}		[resultClass] - Optional
	 * @returns		{Promise}
	 */
	
	/**
	 * Call a method of the web service using the DELETE verb
	 * 
	 * @memberof	conbo.HttpService.prototype
	 * @method		delete
	 * @param		{string}	command - The name of the command
	 * @param		{Object}	[data] - Object containing the data to send to the web service
	 * @param		{Class}		[resultClass] - Optional
	 * @returns		{Promise}
	 */
	
	/**
	 * Add one or more remote commands as methods of this class instance
	 * @param	{string}	command - The name of the command
	 * @param	{string}	[method=GET] - GET, POST, etc (default: GET)
	 * @param	{Class}		[resultClass] - Optional
	 */
	addCommand: function(command, method, resultClass)
	{
		if (conbo.isObject(command))
		{
			method = command.method;
			resultClass = command.resultClass;
			command = command.command;
		}
		
		this[conbo.toCamelCase(command)] = function(data)
		{
			return this.call(command, data, method, resultClass);
		};
		
		return this;
	},
	
	/**
	 * Add multiple commands as methods of this class instance
	 * @param	{string[]}		commands
	 */
	addCommands: function(commands)
	{
		if (!conbo.isArray(commands))
		{
			return this;
		}
		
		commands.forEach(function(command)
		{
			this.addCommand(command);
		}, 
		this);
		
		return this;
	},
	
	/**
	 * Method that encodes data to be sent to the API
	 * 
	 * @param	{Object}	data - Object containing the data to be sent to the API
	 * @param	{string}	[method=GET] - GET, POST, etc (default: GET)
	 */
	encodeFunction: function(data, method)
	{
		return data;
	},
	
	/**
	 * Splice data into URL and remove spliced properties from data object
	 */
	parseUrl: function(url, data)
	{
		var parsedUrl = url,
			matches = parsedUrl.match(/:\b\w+\b/g);
		
		if (!!matches)
		{
			matches.forEach(function(key) 
			{
				key = key.substr(1);
				
				if (!(key in data))
				{
					throw new Error('Property "'+key+'" required but not found in data');
				}
			});
		}
			
		conbo.keys(data).forEach(function(key)
		{
			var regExp = new RegExp(':\\b'+key+'\\b', 'g');
			
			if (regExp.test(parsedUrl))
			{
				parsedUrl = parsedUrl.replace(regExp, data[key]);
				delete data[key];
			}
		});
		
		return parsedUrl;
	},
	
	toString: function()
	{
		return 'conbo.HttpService';
	}
	
})
.implement(conbo.IInjectable);

/**
 * ISyncable pseudo-interface
 * 
 * @augments	conbo
 * @author 		Neil Rackett
 */
conbo.ISyncable =
{
	load: conbo.notImplemented,
	save: conbo.notImplemented,
	destroy: conbo.notImplemented
};

/**
 * Remote Hash
 * Used for syncing remote data with a local Hash
 * 
 * @class		RemoteHash
 * @memberof	conbo
 * @augments	conbo.Hash
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options, see Hash
 * @fires		conbo.ConboEvent#CHANGE
 * @fires		conbo.ConboEvent#RESULT
 * @fires		conbo.ConboEvent#FAULT
 */
conbo.RemoteHash = conbo.Hash.extend(
/** @lends conbo.RemoteHash.prototype */
{
	/**
	 * Constructor
	 * @param {Object}	options		Object containing `source` (initial properties), `rootUrl` and `command` parameters
	 */
	__construct: function(options)
	{
		options = conbo.defineDefaults(options, this.options);
		
		if (!!options.context) this.context = options.context;
		this.preinitialize(options);
		
		this._httpService = new conbo.HttpService(options);
		this._command = options.command;
		
		var resultHandler = function(event)
		{
			conbo.makeBindable(this, conbo.variables(event.result));
			conbo.assign(this, event.result);
			
			this.dispatchEvent(event);
		};
		
		this._httpService
			.addEventListener(conbo.ConboEvent.RESULT, resultHandler, {scope:this})
			.addEventListener(conbo.ConboEvent.FAULT, this.dispatchEvent, {scope:this});
		
		__denumerate(this);
		
		conbo.Hash.prototype.__construct.apply(this, arguments);
	},
	
	load: function(data)
	{
		data = arguments.length ? data : this.toJSON();
		this._httpService.call(this._command, data, 'GET');
		return this;
	},
	
	save: function()
	{
		this._httpService.call(this._command, this.toJSON(), 'POST');
		return this;
	},
	
	destroy: function()
	{
		this._httpService.call(this._command, this.toJSON(), 'DELETE');
		return this;
	},
	
	toString: function()
	{
		return 'conbo.RemoteHash';
	}
	
}).implement(conbo.ISyncable);

__denumerate(conbo.HttpService.prototype);

/**
 * Remote List
 * Used for syncing remote array data with a local List
 * 
 * @class		RemoteList
 * @memberof	conbo
 * @augments	conbo.List
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options, including HttpService options
 * @fires		conbo.ConboEvent#CHANGE
 * @fires		conbo.ConboEvent#ADD
 * @fires		conbo.ConboEvent#REMOVE
 * @fires		conbo.ConboEvent#RESULT
 * @fires		conbo.ConboEvent#FAULT
 */
conbo.RemoteList = conbo.List.extend(
/** @lends conbo.RemoteList.prototype */
{
	//itemClass: conbo.RemoteHash,
	
	/**
	 * Constructor
	 * @param {Object}	[options] - Object containing 'source' (Array, optional), 'rootUrl', 'command' and (optionally) 'itemClass' parameters
	 */
	__construct: function(options)
	{
		options = conbo.defineDefaults(options, this.options);
		
		this.context = options.context;
		
		this._httpService = new conbo.HttpService(options);
		this._command = options.command;
		
		var resultHandler = function(event)
		{
			this.source = event.result;
			this.dispatchEvent(event);
		};
		
		this._httpService
			.addEventListener(conbo.ConboEvent.RESULT, resultHandler, {scope:this})
			.addEventListener(conbo.ConboEvent.FAULT, this.dispatchEvent, {scope:this})
			;
		
		__denumerate(this);
		
		conbo.List.prototype.__construct.apply(this, arguments);
	},
	
	load: function()
	{
		this._httpService.call(this._command, this.toJSON(), 'GET');
		return this;
	},
	
	save: function()
	{
		this._httpService.call(this._command, this.toJSON(), 'POST');
		return this;
	},
	
	destroy: function()
	{
		// TODO
		return this;
	},
	
	toString: function()
	{
		return 'conbo.RemoteList';
	}
	
}).implement(conbo.ISyncable, conbo.IPreinitialize);

__denumerate(conbo.HttpService.prototype);

/**
 * Default history manager used by Router, implemented using onhashchange 
 * event and hash or hash-bang URL fragments
 * 
 * @author 		Neil Rackett
 * @fires		conbo.ConboEvent#CHANGE
 * @fires		conbo.ConboEvent#FAULT
 */
conbo.History = conbo.EventDispatcher.extend(
/** @lends conbo.History.prototype */
{
	__construct: function(options)
	{
		this.handlers = [];
		this.location = window.location;
		this.history = window.history;
		
		this.bindAll('__checkUrl');
	},
	
	start: function(options)
	{
		options || (options = {});
		
		window.addEventListener('hashchange', this.__checkUrl);
		
		this.useHashBang = !!options.useHashBang;
		this.fragment = this.__getFragment();
		
		if (options.trigger !== false)
		{
			this.__loadUrl();
		}
		
		return this;
	},
	
	stop: function()
	{
		window.removeEventListener('hashchange', this.__checkUrl);
		return this;
	},
	
	addRoute: function(route, callback)
	{
		this.handlers.unshift({route:route, callback:callback});
		return this;
	},
	
	/**
	 * The current path
	 * @returns	{string}
	 */
	getPath: function()
	{
		// Workaround for bug in Firefox where location.hash will always be decoded
		var match = this.location.href.match(/#!?(.*)$/);
		return match ? match[1] : '';
	},
	
	/**
	 * Set the current path
	 * 
	 * @param	{string}	path - The path
	 * @param	{}
	 */
	setPath: function(fragment, options)
	{
		options || (options = {});
		fragment = this.__getFragment(fragment);
		
		if (this.fragment === fragment) 
		{
			return;
		}
		
		var location = this.location;
		var prefix = this.useHashBang ? '#!/' : '#/';

		this.fragment = fragment;
		
		if (options.replace)
		{
			var href = location.href.replace(/(javascript:|#).*$/, '');
			location.replace(href + prefix + fragment);
		}
		else
		{
			location.hash = prefix + fragment;
		}
		
		if (options.trigger)
		{
			this.__loadUrl(fragment);
		}
		
		return this;
	},
	
	/**
	 * @private
	 */
	__checkUrl: function(event)
	{
		var changed = this.__getFragment() !== this.fragment;
		
		if (changed)
		{
			this.__loadUrl();
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.CHANGE));
		}
		
		return !changed;
	},
	
	/**
	 * Get the cross-browser normalized URL fragment, either from the URL, the hash, or the override.
	 * @private
	 */
	__getFragment: function(fragment)
	{
		return (fragment || this.getPath()).replace(/^#!|^[#\/]|\s+$/g, '');
	},
	
	/**
	 * Attempt to load the current URL fragment
	 * @private
	 * @returns 	{boolean}	Whether or not the path is a valid route
	 */
	__loadUrl: function(fragmentOverride)
	{
		var fragment = this.fragment = this.__getFragment(fragmentOverride);
		
		var matched = conbo.some(this.handlers, function(handler)
		{
			if (handler.route.test(fragment))
			{
				handler.callback(fragment);
				return true;
			}
		});
		
		if (!matched)
		{
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.FAULT));
		}
		
		return matched;
	},
	
});

/**
 * Router
 * 
 * Routers map faux-URLs to actions, and fire events when routes are
 * matched. Creating a new one sets its `routes` hash, if not set statically.
 * 
 * Derived from the Backbone.js class of the same name
 * 
 * @class		Router
 * @memberof	conbo
 * @augments	conbo.EventDispatcher
 * @author 		Neil Rackett
 * @param 		{Object} options - Object containing initialisation options
 * @fires		conbo.ConboEvent#CHANGE
 * @fires		conbo.ConboEvent#FAULT
 * @fires		conbo.ConboEvent#ROUTE
 * @fires		conbo.ConboEvent#START
 * @fires		conbo.ConboEvent#STOP
 */
conbo.Router = conbo.EventDispatcher.extend(
/** @lends conbo.Router.prototype */
{
	/**
	 * @private
	 */
	__construct: function(options) 
	{
		if (options.routes) 
		{
			this.routes = options.routes;
		}
		
		this.historyClass = conbo.History;
		this.context = options.context;
	},
	
	/**
	 * Start the router
	 */
	start: function(options)
	{
		if (!this.__history)
		{
			this.__history = new this.historyClass();
			this.__bindRoutes();
			
			this.__history
				.addEventListener(conbo.ConboEvent.FAULT, this.dispatchEvent, {scope:this})
				.addEventListener(conbo.ConboEvent.CHANGE, this.dispatchEvent, {scope:this})
				.start(options)
				;
			
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.START));
		}
		
		return this;
	},
	
	/**
	 * Stop the router
	 */
	stop: function()
	{
		if (this.__history)
		{
			this.__history
				.removeEventListener()
				.stop()
				;
			
			delete this.__history;
			
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.STOP));
		}
		
		return this;
	},
	
	/**
	 * Adds a named route
	 * 
	 * @example
	 * 		this.addRoute('search/:query/p:num', 'search', function(query, num) {
	 * 			 ...
	 * 		});
	 */ 
	addRoute: function(route, name, callback) 
	{
		var regExp = conbo.isRegExp(route) ? route : this.__routeToRegExp(route);
		
		if (!callback) 
		{
			callback = this[name];
		}
		
		if (conbo.isFunction(name)) 
		{
			callback = name;
			name = '';
		}
		
		if (!callback) 
		{
			callback = this[name];
		}
		
		this.__history.addRoute(regExp, (function(path)
		{
			var args = this.__extractParameters(regExp, path);
			
			var params = conbo.isString(route) 
				? conbo.object((route.match(/:\w+/g) || []).map(function(r) { return r.substr(1); }), args) 
				: {}
				;
			
			callback && callback.apply(this, args);
			
			var options = 
			{
				router:		this,
				route:		regExp,
				name:		name,
				parameters:	args,
				params:		params,
				path:		path
			};
			
			this.dispatchEvent(new conbo.ConboEvent('route:'+name, options));
			this.dispatchEvent(new conbo.ConboEvent(conbo.ConboEvent.ROUTE, options));
			
		}).bind(this));
		
		return this;
	},
	
	/**
	 * Sets the current path, optionally replacing the current path or silently 
	 * without triggering a route event
	 * 
	 * @param	{string}	path - The path to navigate to
	 * @param	{Object}	[options] - Object containing options: trigger (default: true) and replace (default: false)
	 */
	setPath: function(path, options) 
	{
		options = conbo.setDefaults({}, options, {trigger:true});
		
		this.__history.setPath(path, options);
		return this;
	},
	
	/**
	 * Get or set the current path using the default options
	 * @type	{string}
	 */
	get path()
	{
		return this.__history ? this.__history.getPath() : '';
	},
	
	set path(value)
	{
		return this.setPath(value);
	},
	
	toString: function()
	{
		return 'conbo.Router';
	},
	
	/**
	 * Bind all defined routes. We have to reverse the
	 * order of the routes here to support behavior where the most general
	 * routes can be defined at the bottom of the route map.
	 * 
	 * @private
	 */
	__bindRoutes: function() 
	{
		if (!this.routes) return;
		
		var route;
		var routes = conbo.keys(this.routes);
		
		while ((route = routes.pop()) != null)
		{
			this.addRoute(route, this.routes[route]);
		}
	},
	
	/**
	 * Convert a route string into a regular expression, suitable for matching
	 * against the current location hash.
	 * 
	 * @private
	 */
	__routeToRegExp: function(route) 
	{
		var rootStripper 	= /^\/+|\/+$/g;
		var optionalParam 	= /\((.*?)\)/g;
		var namedParam		= /(\(\?)?:\w+/g;
		var splatParam		= /\*\w+/g;
		var escapeRegExp	= /[\-{}\[\]+?.,\\\^$|#\s]/g;
		
		route = route
			.replace(rootStripper, '')
			.replace(escapeRegExp, '\\$&')
			.replace(optionalParam, '(?:$1)?')
			.replace(namedParam, function(match, optional)
			{
				return optional ? match : '([^\/]+)';
			})
			.replace(splatParam, '(.*?)');
		
		return new RegExp('^' + route + '$');
	},

	/**
	 * Given a route, and a URL fragment that it matches, return the array of
	 * extracted decoded parameters. Empty or unmatched parameters will be
	 * treated as `null` to normalize cross-browser behavior.
	 * 
	 * @private
	 */
	__extractParameters: function(route, fragment) 
	{
		var params = route.exec(fragment).slice(1);
		
		return conbo.map(params, function(param) 
		{
			if (param)
			{
				// Fix for Chrome's invalid URI error
				try { return decodeURIComponent(param); }
				catch (e) { return unescape(param); }
			}
			
			return null;
		});
	}
	
}).implement(conbo.IInjectable);

__denumerate(conbo.Router.prototype);


	conbo.ready(function()
	{
		!conbo.isNode && conbo.info('%c '+conbo.toString()+' ', 'font-weight:bold; background-color:#069; color:white;', 'https://conbo.mesmotronic.com');
	});

	return conbo;
});