(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 < 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;
});