Skip to content

Instantly share code, notes, and snippets.

@RobertWHurst
Last active April 28, 2019 20:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save RobertWHurst/4080411 to your computer and use it in GitHub Desktop.
Save RobertWHurst/4080411 to your computer and use it in GitHub Desktop.
(function(factory) {
if(typeof define === 'function' && define.amd) {
define(factory);
} else {
window.SpringJS = factory();
window.s = window.SpringJS;
}
})(function() {
var api, version, statusCodes;
/////////////////////
// POLYFILLS //
/////////////////////
//polyfills for ms's piece o' shit browsers
[].indexOf||(Array.prototype.indexOf=function(a,b,c){for(c=this.length,b=(c+~~b)%c;b<c&&(!(b in this)||this[b]!==a);b++);return b^c?b:-1;});
Date.now||(Date.now=function(){return(new Date()).getTime()});
window.requestAnimationFrame=(function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(callback, element){window.setTimeout(function(){ callback(new Date()) }, 1000 / 60);};})();
/////////////////////
// CONSTANTS //
/////////////////////
statusCodes = {
200: {'name': 'ok', 'type': 'success'},
201: {'name': 'created', 'type': 'success'},
202: {'name': 'accepted', 'type': 'success'},
204: {'name': 'nocontent', 'type': 'success'},
205: {'name': 'resetcontent', 'type': 'success'},
206: {'name': 'partialcontent', 'type': 'success'},
300: {'name': 'multiplechoices', 'type': 'redirection'},
301: {'name': 'movedpermanently', 'type': 'redirection'},
304: {'name': 'notmodified', 'type': 'redirection'},
308: {'name': 'resumeincomplete', 'type': 'redirection'},
400: {'name': 'badrequest', 'type': 'error'},
401: {'name': 'unauthorized', 'type': 'error'},
403: {'name': 'forbidden', 'type': 'error'},
404: {'name': 'notfound', 'type': 'error'},
405: {'name': 'methodnotallowed', 'type': 'error'},
406: {'name': 'notacceptable', 'type': 'error'},
407: {'name': 'proxyauthenticationrequired', 'type': 'error'},
408: {'name': 'requesttimedout', 'type': 'error'},
409: {'name': 'conflict', 'type': 'error'},
410: {'name': 'gone', 'type': 'error'},
411: {'name': 'lengthrequired', 'type': 'error'},
412: {'name': 'preconditionfailed', 'type': 'error'},
413: {'name': 'requestentitytoolarge', 'type': 'error'},
414: {'name': 'requesturitoolong', 'type': 'error'},
415: {'name': 'unsupportedmediatype', 'type': 'error'},
416: {'name': 'requestedrangenotsatisfiable', 'type': 'error'},
417: {'name': 'expectationfailed', 'type': 'error'},
418: {'name': 'iamateapot', 'type': 'error'},
428: {'name': 'preconditionrequired', 'type': 'error'},
429: {'name': 'toomanyrequests', 'type': 'error'},
431: {'name': 'requestheaderfieldstoolarge', 'type': 'error'},
444: {'name': 'noresponse', 'type': 'error'},
449: {'name': 'retrywith', 'type': 'error'},
450: {'name': 'blockedbyparentalcontrols', 'type': 'error'},
499: {'name': 'clientclosedrequest', 'type': 'error'},
500: {'name': 'clientclosedrequest', 'type': 'servererror'},
501: {'name': 'notimplemented', 'type': 'servererror'},
502: {'name': 'badgateway', 'type': 'servererror'},
503: {'name': 'serviceunavailable', 'type': 'servererror'},
504: {'name': 'gatewaytimeout', 'type': 'servererror'},
511: {'name': 'networkauthenticationrequired', 'type': 'servererror'}
};
////////////////
// INIT //
////////////////
//set spring as the base for the api
api = Spring;
//version
version = '0.2.0';
///////////////
// API //
///////////////
//version
api.SpringJS = version;
//stylesheets
api.stylesheet = StyleSheet;
//ajax
api.fetch = Fetch;
api.fetch.script = FetchScript;
api.fetch.json = fetchJSON;
api.request = Request;
api.bridge = Bridge;
//object and array
api.extend = extend;
api.clone = clone;
api.compare = compare;
api.merge = merge;
api.reduce = reduce;
api.mirror = mirror;
api.watch = watch;
api.size = objectSize;
//string methods
api.hexToRGB = hexToRGB;
api.RGBToArray = RGBToArray;
api.camelCase = camelCase;
//math methods
api.timedSequence = TimedSequence;
api.linearInterpolation = LinearInterpolation;
//flow control methods
api.loop = EventLoop;
api.funnel = Funnel;
api.queue = Queue;
api.emitter = EventEmitter;
//console methods
api.timer = Timer;
//execution related methods
api.domain = Domain;
//iteration related methods
api.iterator = Iterator;
//return the api
return api;
////////////////////////////
// SELECTOR METHODS //
////////////////////////////
/**
* spring is a jQuery-like selector and DOM element wrapper
*/
function Spring(selector, parent) {
var elements, parents, pI, subElements, sEI, sI;
//validate
if(typeof selector !== 'string' && typeof selector !== 'object') { throw new Error('Cannot select element. If given the parent element must be a string or an object.'); }
if(parent && typeof parent !== 'string' && typeof parent !== 'object') { throw new Error('Cannot select element. If given the parent element must be a string or an object'); }
//if the parent is an array then preform the select across each of them
if(typeof parent === 'object' && typeof parent.push === 'function') {
parents = parent;
elements = [];
for(pI = 0; pI < parents.length; pI += 1) {
if(parents[pI] && parents[pI].nodeType !== 1) { continue; }
subElements = Spring(selector, parents[pI]);
for(sEI = 0; sEI < subElements.length; sEI += 1) {
elements.push(subElements[sEI]);
}
}
//if the parent is a single element or a selector
} else {
//if the selector is actually a html source string
if(typeof selector === 'string' && selector.match(/<[^>]+>/g)) {
//covert the html string to a collection of dom elements
elements = htmlToArray(selector);
}
//if a selector string
else if(typeof selector === 'string') {
//finds elements matching the selector
elements = find(selector, parent);
//if wrapping an array of selectors (or whatever else SpringJS will accept)
} else if(selector !== null && typeof selector === 'object' && typeof selector.push === 'function') {
elements = [];
for(sI = 0; sI < selector.length; sI += 1) {
if(selector[sI] && selector[sI].nodeType !== 1) { continue; }
subElements = Spring(selector[sI], parent);
for(sEI = 0; sEI < subElements.length; sEI += 1) {
elements.push(subElements[sEI]);
}
}
return wrap(elements);
//if filtering nodes
} else if(typeof selector === 'object' && typeof parent === 'object') {
if(contained(selector, parent)) {
return wrap(selector);
}
//if wrapping a node
} else if(typeof selector === 'object') {
return wrap(selector);
}
}
//return the elements wrapped in a evoJS wrapper
return wrap(elements);
}
/**
* Find and retrieves an element via a css selector
* @param selectorPattern
* @param parent
* @return {*}
*/
function find(selectorPattern, parent) {
var element, elements, nodeList, eI;
//validate the selector
if(typeof selectorPattern !== 'string') { throw new Error('Cannot find any elements. The selector pattern must be a string'); }
//fetch the body tag
if(selectorPattern === 'body' && typeof parent === 'undefined') { return document.body; }
if(selectorPattern === 'head' && typeof parent === 'undefined') { return document.getElementsByTagName('head')[0]; }
parent = parent || 'body';
//validate the selector
if(parent && typeof parent !== 'string' && typeof parent !== 'object') { throw new Error('Cannot find any elements. The parent must be a string or object.'); }
//if parent is a selector
if(typeof parent === 'string') {
parent = find(parent);
}
elements = [];
if(typeof document.querySelectorAll === 'function') {
nodeList = parent.querySelectorAll(selectorPattern);
//convert the node list to an array
for(eI = 0; eI < nodeList.length; eI += 1) {
elements.push(nodeList[eI]);
}
} else {
exec(parent);
}
function exec(parent) {
var cEI;
if(!parent.childNodes) { return; }
//find matching elements within the parent element
for(cEI = 0; cEI < parent.childNodes.length; cEI += 1) {
element = parent.childNodes[cEI];
//skip if the element is of the wrong node type
if(element.nodeType !== 1) { continue; }
//check the element against the selector
if(matches(element, selectorPattern)) {
elements.push(element);
}
//if the element has children then preform a search on them
if(element.childNodes.length) {
exec(element);
}
}
}
return elements;
}
/**
* Returns true or false. True if the element is a child of the parent
* @param element
* @param parent
* @return {Boolean}
*/
function contained(element, parent) {
var current;
if(typeof element !== 'object' || element.nodeType !== 1) { throw new Error('Cannot check if the element is contained by the parent. The element must be a type 1 dom node.'); }
if(typeof parent !== 'object' || parent.nodeType !== 1) { throw new Error('Cannot check if the element is contained by the parent. The parent must be a type 1 dom node.'); }
current = element;
while(current.parentNode) {
current = current.parentNode;
if(current === parent) {
return true;
}
}
return false;
}
/**
* Checks an element against a css selector. Returns true if the selector matches or false if it does not.
* @param element
* @param selectorPattern
* @return {Boolean}
*/
function matches(element, selectorPattern) {
var selectors, selector, $parent, selectorRegex,
attributesRegex, regexResult, tagName, id, className,
attributesString, attributes, attribute, value, key,
selectorPatterns, sPI;
selectorRegex = /([a-zA-Z][a-zA-Z0-9]*)?(?:#([_\-a-zA-Z][_\-a-zA-Z0-9]*))?(?:\.([_\-a-zA-Z][_\-a-zA-Z0-9]*))?(.*)?/;
attributesRegex = /\[([a-zA-Z][a-zA-Z0-9]*)(?:="([^"]+)")?]/g;
//validate
if(typeof element !== 'object') { throw new Error('Cannot check element against selector. The element must be an object.'); }
if(typeof selectorPattern !== 'string') { throw new Error('Cannot check element against selector pattern. The selector pattern must be a string.'); }
if(selectorPattern === '*') { return true; }
//if they're are commas then split at the commas and self invoke on each chunk
if(selectorPattern.match(',')) {
selectorPatterns = selectorPattern.split(/\s*,\s*/);
for(sPI = 0; sPI < selectorPatterns.length; sPI += 1) {
if(matches(element, selectorPatterns[sPI])) {
return true;
}
}
return false;
}
//if the pattern contains a number of selectors
if(selectorPattern.match(' ')) {
//bust up the selectors
selectors = selectorPattern.split(' ').reverse();
//loop through each of the selectors and check each parent up the tree
$parent = element;
while(selectors.length) {
//if the selector doesn't match then exit
if(matches($parent, selectors[0])) {
selectors.shift();
}
if(!$parent.parentNode) {
return false;
}
//get the next parent
$parent = $parent.parentNode;
}
return true;
//if the pattern is a single selector
} else {
selector = selectorPattern;
//break up the selector
regexResult = selectorRegex.exec(selector);
tagName = regexResult[1];
id = regexResult[2];
className = regexResult[3];
attributesString = regexResult[4];
attributes = {};
//figure out the attributes
while(regexResult = attributesRegex.exec(attributesString)) {
attribute = regexResult[1];
value = regexResult[2];
attributes[attribute] = value;
}
//make sure everything lines up with the element
//element does not have tag name (text node or document object) and a tag name is required
if(!element.tagName && tagName) {
return false;
}
//the element does not have a matching tag name
if(tagName && element.tagName.toLowerCase() !== tagName.toLowerCase()) {
return false;
}
//the element id does not match
if(id && element.id !== id) {
return false;
}
//the element class names do not contain the correct class name
if(className && !element.className) {
return false;
}
if(className && element.className && element.className.split(' ').indexOf(className) === -1) {
return false;
}
for(key in attributes) {
if(!attributes.hasOwnProperty(key)) { continue; }
//if the element is missing an attribute that is required
if(typeof element[key] === 'undefined') {
return false;
}
//if the element is has an attrute with the wrong value
if(attributes[key] && element[key] !== attributes[key]) {
return false;
}
}
}
return true;
}
/**
* Converts a html string to a dom fragment containing all nodes defined in the html
* @param html
*/
function htmlToArray(html) {
var $host, $elements, cI;
$host = document.createElement('div');
$elements = [];
$host.setAttribute('data-host', '');
$host.innerHTML = html;
for(cI = 0; cI < $host.childNodes.length; cI += 1) {
$elements.push($host.childNodes[cI]);
}
return $elements;
}
////////////////////////////
// EVO OBJECT CLASS //
///////////////////////////
/**
* Wraps an element with a spring wrapper
*/
function wrap(elements) {
var eI;
elements = elements || [];
if(typeof elements.push !== 'function') {
elements = [elements];
}
if(typeof elements !== 'object' && typeof elements.push !== 'function') { throw new Error('Cannot wrap element. the element must be an array'); }
//create the emitter
EventEmitter(elements);
//pipe node events
for(eI = 0; eI < elements.length; eI += 1) {
elements.pipe(elements[eI]);
}
//version
elements.SpringJS = version;
//api
elements.filter = filter;
elements.first = first;
elements.last = last;
elements.next = next;
elements.previous = previous;
elements.parent = parent;
elements.children = children;
elements.find = find;
elements.all = all;
elements.add = add;
elements.append = append;
elements.append.to = appendTo;
elements.append.before = appendBefore;
elements.append.after = appendAfter;
elements.detached = detached;
elements.remove = remove;
elements.each = each;
elements.css = css;
elements.css.nonComputed = nonComputedCss;
elements.className = className;
elements.className.add = addClass;
elements.className.remove = removeClass;
elements.width = width;
elements.width.outer = outerWidth;
elements.height = height;
elements.height.outer = outerHeight;
elements.scrollbar = {};
elements.scrollbar.width = scrollbarWidth;
elements.scrollbar.height = scrollbarHeight;
elements.offset = offset;
elements.position = position;
elements.html = html;
elements.html.slice = htmlSlice;
elements.html.splice = htmlSplice;
elements.html.concat = htmlConcat;
elements.attribute = attribute;
elements.value = value;
listenForDomEvents();
return elements;
/**
* Triggers a DOM event on the all elements in the selection
* @return {*}
*/
function listenForDomEvents() {
if(elements.length < 1) { return false; }
elements.on('EMITTER.focus', function() {
var eI;
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].focus();
}
});
elements.on('EMITTER.blur', function() {
var eI;
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].blur();
}
});
}
/**
* Filters the current selection by selector pattern
* @param selection
* @param length
* @return {*}
*/
function filter(selection, length) {
var eI, sI, subElements, startIndex, endIndex;
if(elements.length < 1) { return wrap(); }
if(typeof selection === 'number') { startIndex = selection; selection = null; }
if(typeof selection === 'string') { selection = s(selection); }
if(typeof selection === 'object' && selection.nodeType === 1) {selection = s(selection); }
if(selection && typeof selection !== 'object' && selection.SpringJS === s.version) { throw new Error('Cannot filter decedents. The selector pattern must be a string or StringJS selection.'); }
if(startIndex && typeof startIndex !== 'number') { throw new Error('Cannot filter decedents. The index must be a number.'); }
if(length && typeof length !== 'number') { throw new Error('Cannot filter descendants. If given the length must be a number.'); }
if(selection) {
subElements = [];
for(eI = 0; eI < elements.length; eI += 1) {
for(sI = 0; sI < selection.length; sI += 1) {
if(elements[eI] === selection[sI]) {
subElements.push(elements[eI]);
}
}
}
return wrap(subElements);
} else if(typeof startIndex === 'number') {
if(typeof length === 'number') {
endIndex = startIndex + length;
}
if(startIndex < 0) {
startIndex += elements.length;
if(typeof length === 'number') {
endIndex += elements.length;
} else {
endIndex = startIndex + 1;
startIndex = 0;
}
}
return wrap(elements.slice(startIndex, endIndex));
} else {
return false;
}
}
/**
* Returns the first element in the selection
* @return {*}
*/
function first() {
if(elements.length < 1) { return wrap(); }
return wrap(elements[0]);
}
/**
* Returns the last element in the selection
* @return {*}
*/
function last() {
if(elements.length < 1) { return wrap(); }
return wrap(elements[elements.length - 1]);
}
/**
* Returns the next sibling of the first element in the selection
* @return {*}
*/
function next() {
return wrap(elements[0].nextSibling);
}
/**
* Returns the previous sibling of the first element in the selection
* @return {*}
*/
function previous() {
if(elements.length < 1) { return wrap(); }
return wrap(elements[0].previousSibling);
}
/**
* Finds the parent of each element in the selection.
* @param selector
* @return {*}
*/
function parent(selector) {
var eI, parentElements, selection;
if(elements.length < 1) { return wrap(); }
if(selector && typeof selector !== 'string') { throw new Error('Cannot select the parents of the selected elements. If given the selector must be a string.'); }
//create a new wrapper
parentElements = [];
for(eI = 0; eI < elements.length; eI += 1) {
if(elements[eI].parentNode && parentElements.indexOf(elements[eI].parentNode) === -1) {
parentElements.push(elements[eI].parentNode);
}
}
selection = wrap(parentElements);
if(selector) {
selection = selection.filter(selector);
}
return selection;
}
/**
* Finds the children, if they exist, of each element in the selection and returns them wrapped in a new selection.
* @param selector
* @return {*}
*/
function children(selector) {
var eI, cI, childElements, selection;
if(elements.length < 1) { return wrap(); }
if(selector && typeof selector !== 'string') { throw new Error('Cannot select the children of the selected elements. If given the selector must be a string.'); }
//create a new wrapper
childElements = [];
for(eI = 0; eI < elements.length; eI += 1) {
for(cI = 0; cI < elements[eI].childNodes.length; cI += 1) {
if(elements[eI].childNodes[cI] && elements[eI].childNodes[cI].nodeType === 1 && childElements.indexOf(elements[eI].childNodes[cI]) === -1) {
childElements.push(elements[eI].childNodes[cI]);
}
}
}
selection = wrap(childElements);
if(selector) {
selection = selection.filter(selector);
}
return selection;
}
/**
* finds all matching children of the selection
* @param selectorPattern
* @return {*}
*/
function find(selectorPattern) {
if(elements.length < 1) { return wrap(); }
return Spring(selectorPattern, elements);
}
/**
* Gets all nodes at every level (all children) in a selection
* @return {*}
*/
function all() {
var newSelection, eI;
newSelection = wrap([]);
(function exec(elements) {
for(eI = 0; eI < elements.length; eI += 1) {
newSelection.add(wrap(elements[eI]));
if(elements[eI].childNodes) {
exec(elements[eI].children);
}
}
})(elements);
return newSelection;
}
/**
* Adds any number of SpringJS elements to the current selection
* @return {Boolean}
*/
function add( ) {
var args, aI, nI;
args = Array.prototype.slice.apply(arguments);
for(aI = 0; aI < args.length; aI += 1) {
if(typeof args[aI] === 'undefined') { continue; }
//defaults
if(typeof args[aI] === 'string') { args[aI] = SpringJS(args[aI]); }
//validate
if(typeof args[aI] !== 'object' || args[aI].SpringJS !== version) { throw new Error('Cannot add SpringJS elements to the selection. All arguments for add must be SpringJS elements.'); }
//add each node to the selection
for(nI = 0; nI < args[aI].length; nI += 1) {
elements.push(args[aI][nI]);
elements.pipe(args[aI][nI]);
}
//trigger the add event
elements.trigger('add', args[aI]);
}
return true;
}
/**
* Appends any number of Spring elements. If an html string it will be parsed into an element then appended.
* If a selector is given, all elements matching the selector will be appended.
* @return {Boolean}
*/
function append( ) {
var args, aI, nI;
if(elements.length < 1) { return false; }
args = Array.prototype.slice.apply(arguments);
for(aI = 0; aI < args.length; aI += 1) {
//validate
if(typeof args[aI] === 'string') { args[aI] = Spring(args[aI]); }
if(typeof args[aI] !== 'object' || args[aI].SpringJS !== version) { throw new Error('Cannot append SpringJS elements to the selection. All arguments for add must be SpringJS elements.'); }
//trigger event
args[aI].trigger('append.to', elements[0]);
//add each node to the first element
for(nI = 0; nI < args[aI].length; nI += 1) {
elements[0].appendChild(args[aI][nI]);
}
}
//trigger the add event
elements.trigger('append', args[aI]);
return true;
}
/**
* Appends the current selection to a new selection via a selector
* @param selector
* @param parent
* @return {*}
*/
function appendTo(selector, parent) {
//validate
if(typeof selector !== 'string' && typeof selector !== 'object') { throw new Error('Cannot append selection to element via selector. The selector must be an object or string.'); }
//grab the new selection
if(typeof selector !== 'object' || typeof selector.append !== 'function') {
selector = SpringJS(selector, parent);
}
if(typeof selector !== 'object' || typeof selector.push !== 'function' || selector.length < 1) { return false; }
//append the current selection to the new selection
return selector.append(elements);
}
/**
* Appends the selection before/after a given selection
* @param selector
* @param parent
* @param position
* @return {Boolean}
*/
function appendRelative(selector, parent, position) {
var eI;
//validate
if(typeof selector !== 'string' && typeof selector !== 'object') { throw new Error('Cannot append selection before element via selector. The selector must be an object or string.'); }
if(typeof position !== 'string' && (position !== 'before' || position !== 'after')) { throw new Error('Cannot append selection relative to an element via selector. The selector must be an object or string.'); }
//grab the new selection
if(typeof selector !== 'object' || typeof selector.append !== 'function') {
selector = SpringJS(selector, parent);
}
if(typeof selector !== 'object' || typeof selector.push !== 'function' || selector.length < 1) { return false; }
for(eI = 0; eI < elements.length; eI += 1) {
if(position === 'before') {
selector[0].parentNode.insertBefore(elements[eI], selector[0]);
} else if(position === 'after') {
selector[0].parentNode.insertBefore(elements[eI], selector[0].nextElementSibling);
}
}
if(position === 'before') {
selector.trigger('append.before', elements);
} else if(position === 'after') {
selector.trigger('append.after', elements);
}
return true;
}
/**
* Appends the selection directly before a given selector
* @param selector
* @param parent
* @return {*}
*/
function appendBefore(selector, parent) {
return appendRelative(selector, parent, 'before');
}
/**
* Appends the selection directly after a given selector
* @param selector
* @param parent
* @return {*}
*/
function appendAfter(selector, parent) {
return appendRelative(selector, parent, 'after');
}
/**
* Returns true if all the elements in the selection are detached.
* @return {Boolean}
*/
function detached() {
var eI, element;
for(eI = 0; eI < elements.length; eI += 1) {
element = elements[eI];
while(element.parentNode) {
element = element.parentNode;
}
if(element.nodeType === 9) {
return false;
}
}
return true;
}
/**
* Remove the elements in the selection from the DOM
* @param selector
* @return {*}
*/
function remove(selector) {
var _elements, eI, detachedElements;
//validate
if(selector && typeof selector !== 'string') { throw new Error('Cannot remove elements. If given the selector must be a string.'); }
_elements = elements;
if(selector) {
_elements = elements.filter(selector);
}
detachedElements = [];
for(eI = 0; eI < _elements.length; eI += 1) {
if(_elements[eI].parentNode) {
detachedElements.push(_elements[eI].parentNode.removeChild(_elements[eI]));
}
}
elements.trigger('remove');
return wrap(detachedElements);
}
/**
* Executes a callback on each wrapped element
* @param callback
* @param endCallback
*/
function each(callback, endCallback) {
var eI;
if(elements.length < 1) { return false; }
//validate
if(typeof callback !== 'function') { throw new Error('Cannot loop through selected elements. The callback must be a function.'); }
if(endCallback && typeof endCallback !== 'function') { throw new Error('Cannot loop through selected elements. If an end callback is given it must be a function.'); }
for(eI = 0; eI < elements.length; eI += 1) {
if(callback(wrap(elements[eI]), eI) === false) {
break;
}
}
endCallback && endCallback(elements);
}
/**
* Gets or sets a non-computed css value
* @param property
* @param value
* @param duration
*/
function nonComputedCss(property, value, duration) {
return getSetCss(property, value, duration, true);
}
/**
* Gets or sets a computed css value
* @param property
* @param value
* @param duration
* @return {*}
*/
function css(property, value, duration) {
return getSetCss(property, value, duration, false);
}
/**
* Gets or sets a css value
* @param property
* @param value
* @param duration
*/
function getSetCss(property, value, duration, nonComputed) {
var returned, key, funnel, emitter, result, i;
if(elements.length < 1) { return false; }
//validate
if(typeof property !== 'string' && typeof property !== 'object') { throw new Error('Cannot get/set css. The property name must be a string or object.'); }
if(value && typeof value !== 'string' && typeof value !== 'number' && value !== false) { throw new Error('Cannot set css. The value must be a string or number or false.'); }
if(duration && typeof duration !== 'number') { throw new Error('Cannot animate css. If given the duration a number.'); }
//create the emitter
emitter = EventEmitter();
//Set multiple property
if(typeof property === 'object') {
returned = true;
//move over the duration and callback
duration = value;
value = null;
if(typeof property.length === 'undefined' && duration) {
//create a funnel
funnel = Funnel();
//animate each property value
for(key in property) {
if(!property.hasOwnProperty(key)) { continue; }
result = animateCSS(key, property[key], duration);
result.on('complete', funnel());
}
//set the funnel callback
funnel.on('complete', function() {
//fire the complete event
emitter.trigger('complete');
});
return emitter;
} else if(property.length) {
returned = {};
//apply each property value
for(i = 0; i < property.length; i += 1) {
returned[property[i]] = getCss(property[i], nonComputed);
}
return returned;
} else {
//apply each property value
for(key in property) {
if(!property.hasOwnProperty(key) || typeof property[key] === 'undefined') { continue; }
setCss(key, property[key], true);
returned = true;
}
return returned;
}
//Set a single single property
} else if(typeof property !== 'undefined' && typeof value !== 'undefined' && typeof duration !== 'undefined') {
return animateCSS(property, value, duration);
} else if(typeof property !== 'undefined' && typeof value !== 'undefined') {
return setCss(property, value, true);
} else if(typeof property !== 'undefined') {
return getCss(property, nonComputed);
} else {
return false;
}
}
/**
* Animates a css value
* @param property
* @param value
* @param duration
* @return {*}
*/
function animateCSS(property, value, duration) {
var originalValue, originalInteger, integer,
prePost, prefix, postfix, currentValue,
initialColor, initialRed, initialBlue,
initialGreen, initialAlpha, finalColor,
currentRed, currentBlue, currentGreen,
finalRed, finalBlue, finalGreen,
currentAlpha, finalAlpha, api,
startTime, running, timer, redTimer,
blueTimer, greenTimer, alphaTimer, eI,
clearOnEnd;
//get the initial value and int
originalValue = getCss(property);
originalInteger = parseFloat(originalValue.match(/[0-9]+/)[0]);
api = EventEmitter();
if(value === false || value === '') {
setCss(property, value);
value = getCss(property);
clearOnEnd = true;
}
//If a number is given then get the prefix and postfix from the original value
if(typeof value === 'number' && originalInteger !== false) {
//set the int
integer = value;
//get the pre and post
prePost = originalValue.split(originalInteger);
prefix = prePost[0] || '';
postfix = prePost[1] || '';
//set the value
value = prefix + value + postfix;
}
//if a string is given parse the int
else {
integer = parseFloat(value);
//get the pre and post
prePost = value.split(integer);
prefix = prePost[0] || '';
postfix = prePost[1] || '';
}
//set the initial value
currentValue = originalValue;
//integer value
if(!isNaN(integer) && !isNaN(originalInteger)) {
//update the value over time
timer = TimedSequence(originalInteger, integer, duration).on('update', function(value) {
currentValue = prefix + value.toFixed(3) + postfix;
});
//color value
} else if(originalValue.substr(0, 1) === '#' || originalValue.substr(0, 3) === 'rgb') {
//convert hex
if(originalValue.substr(0, 1) === '#') {
originalValue = hexToRGB(originalValue);
}
if(value.substr(0, 1) === '#') {
value = hexToRGB(value);
}
//break down the colors into their parts
initialColor = RGBToArray(originalValue);
finalColor = RGBToArray(value);
currentRed = initialRed = initialColor[0];
currentGreen = initialGreen = initialColor[1];
currentBlue = initialBlue = initialColor[2];
currentAlpha = initialAlpha = initialColor[3] || 1;
finalRed = finalColor[0];
finalGreen = finalColor[1];
finalBlue = finalColor[2];
finalAlpha = finalColor[3] || 1;
//start the sequence of animation
if(initialRed !== finalRed) {
redTimer = TimedSequence(initialRed, finalRed, duration).on('update', function(red) {
currentRed = Math.round(red);
currentValue = 'rgba(' + currentRed + ', ' + currentGreen + ', ' + currentBlue + ', ' + currentAlpha + ')';
});
}
if(initialGreen !== finalGreen) {
greenTimer = TimedSequence(initialGreen, finalGreen, duration).on('update', function(green) {
currentGreen = Math.round(green);
currentValue = 'rgba(' + currentRed + ', ' + currentGreen + ', ' + currentBlue + ', ' + currentAlpha + ')';
});
}
if(initialBlue !== finalBlue) {
blueTimer = TimedSequence(initialBlue, finalBlue, duration).on('update', function(blue) {
currentBlue = Math.round(blue);
currentValue = 'rgba(' + currentRed + ', ' + currentGreen + ', ' + currentBlue + ', ' + currentAlpha + ')';
});
}
if(initialAlpha !== finalAlpha) {
alphaTimer = TimedSequence(initialAlpha, finalAlpha, duration).on('update', function(alpha) {
currentAlpha = Math.round(alpha);
currentValue = 'rgba(' + currentRed + ', ' + currentGreen + ', ' + currentBlue + ', ' + currentAlpha + ')';
});
}
}
//setup the api
api.clear = clear;
//loop through each element
for(eI = 0; eI < elements.length; eI += 1) {
if(!elements[eI].data) { elements[eI].data = {}; }
if(!elements[eI].data.cssAnimations) { elements[eI].data.cssAnimations = {}; }
if(elements[eI].data.cssAnimations[property]) { elements[eI].data.cssAnimations[property].clear(); }
elements[eI].data.cssAnimations[property] = api;
}
//start animating
startTime = Date.now();
running = true;
(function animate() {
if(!running) { return; }
//fire the complete event
api.trigger('update');
if(typeof currentValue !== 'undefined') {
setCss(property, currentValue, true);
}
if(Date.now() < startTime + duration) {
setTimeout(animate, 0);
} else {
if(!clearOnEnd) {
setCss(property, value, true);
} else {
setCss(property, false, true);
}
//fire the complete event
api.trigger('complete');
//clear the property
for(eI = 0; eI < elements.length; eI += 1) {
delete elements[eI].data.cssAnimations[property];
}
}
})();
return api;
/**
* Stops the animation in place
*/
function clear() {
running = false;
if(timer) { timer.clear(); }
if(redTimer) { redTimer.clear(); }
if(blueTimer) { blueTimer.clear(); }
if(greenTimer) { greenTimer.clear(); }
if(alphaTimer) { alphaTimer.clear(); }
}
}
/**
* Sets the value of a single css property
* @param property
* @param value
*/
function setCss(property, value, emitUpdateEvent) {
var originalValue, originalInt, prePost,
prefix, postfix, modified, eI;
if(elements.length < 1) { return false; }
//validate
if(typeof property !== 'string') { throw new Error('Cannot set css. The property name must be a string.'); }
if(typeof value !== 'string' && typeof value !== 'number' && value !== false) { throw new Error('Cannot set css. The value must be a number or string or false.'); }
//replace a value of false with an empty string
if(value === false) { value = ''; }
//If a number is given then get the prefix and postfix from the original value
if(typeof value === 'number') {
//get the initial value and int
originalValue = getCss(property);
//if a value is set then apply the pre and post fix
if(originalValue) {
originalInt = parseFloat(originalValue);
if(!isNaN(originalInt)) {
//get the pre and post
prePost = originalValue.split(originalInt);
prefix = prePost[0] || '';
postfix = prePost[1] || '';
//set the value
value = prefix + value + postfix;
} else {
value += 'px';
}
}
}
//apply the css to each selected element
modified = false;
for(eI = 0; eI < elements.length; eI += 1) {
//don't set styles on non styled node types
if (!elements[eI] || elements[eI].nodeType !== 1 || !elements[eI].style) {
continue;
}
if(emitUpdateEvent) {
if(!elements.trigger('update', property, value)) {
return false;
}
}
//set the property
if(typeof elements[eI].style.setProperty === 'function') {
elements[eI].style.setProperty(property, value);
} else {
elements.style[camelCase(property)] = value;
}
modified = true;
}
return modified;
}
/**
* Gets the value of a single css property
* @param property
*/
function getCss(property, nonComputed) {
var displayValue, value, parent, originalParent, rules, rI;
if(elements.length < 1) { return false; }
//validate
if(typeof property !== 'string') { throw new Error('Cannot get css. The property name must be a string.'); }
if(elements[0].nodeType !== 1 || !elements[0].style) { throw new Error('Cannot get css. The selected element does not support styles.'); }
//if the item is hidden then temporarily reveal it to get its computed style
if(property !== 'display' && getCss('display') === 'none') {
displayValue = getCss('display', true) || false;
setCss('display', 'block');
}
//make sure the element is not the document object
if(elements[0].nodeType === 9) { return false; }
//get the top level element
parent = elements[0];
while(parent.parentNode) {
parent = parent.parentNode;
}
if(parent.nodeType !== 9) {
originalParent = elements[0].parentNode;
//if the node is detached then temporarily attach it.
document.body.appendChild(elements[0]);
}
//legacy Internet Explorer
if (elements[0].currentStyle && elements[0].style) {
if(!nonComputed) {
value = elements[0].currentStyle.getAttribute(camelCase(property)) || false;
} else {
value = elements[0].style.getAttribute(camelCase(property)) || false;
}
}
//Modern Browsers
else if (document.defaultView && document.defaultView.getComputedStyle && document.defaultView.getMatchedCSSRules) {
if(!nonComputed) {
value = document.defaultView.getComputedStyle(elements[0]).getPropertyValue(property) || false;
} else {
rules = document.defaultView.getMatchedCSSRules(elements[0]);
if(rules) {
for(rI = 0; rI < rules.length; rI += 1) {
value = rules[rI].style.getPropertyValue(property) || value;
}
} else {
value = false;
}
}
}
//No Method
else {
throw new Error('Cannot get css property. No supported method.');
}
if(typeof displayValue !== 'undefined') {
//reset the display method
setCss('display', displayValue);
}
//if the node was temporarily injected into the body detach it
if(parent.nodeType !== 9) {
elements[0].parentNode.removeChild(elements[0]);
}
if(originalParent) {
originalParent.appendChild(elements[0]);
}
return value;
}
/**
* Set/Get a attribute
* @param name
* @param value
*/
function attribute(name, value) {
var eI, attribute;
if(typeof name !== 'string') { throw new Error('Cannot get or set attribute. The name must be a string.'); }
if(value && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { throw new Error('Cannot set attribute. The value must be a string or number.'); }
if(typeof value !== 'undefined') {
//removing the attribute
if(value === false) {
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].removeAttribute(name);
}
return !!elements.length;
}
//setting the attribute
else {
if(value === true) { value = ''; }
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].setAttribute(name, value);
}
return !!elements.length;
}
} else {
attribute = elements[0].getAttribute(name);
if(attribute === '') { attribute = true; }
if(attribute === null) { attribute = false; }
return attribute;
}
}
/**
* Adds a class to the class name of the first element in the selection
* @param className
* @return {Boolean}
*/
function addClass(className) {
var eI, classes, cI;
if(className.match(/[\s]+/g)) {
className = className.split(' ');
}
if(typeof className === 'object' && typeof className.push === 'function') {
for(cI = 0; cI < className.length; cI += 1) {
addClass(className[cI]);
}
return true;
}
//validate
if(typeof className !== 'string') { throw new Error('Cannot add class. The class name must be a string.'); }
for(eI = 0; eI < elements.length; eI += 1) {
classes = elements[eI].className.split(' ');
if(classes.length === 1 && classes[0] === '') { classes = []; }
if(classes.indexOf(className) < 0) { classes.push(className) }
elements[eI].className = classes.join(' ');
}
return true;
}
/**
* Removes a class from the class name from the first element in the selection
* @param className
* @return {Boolean}
*/
function removeClass(className) {
var eI, classes, cI;
if(className.match(/[\s]+/g)) {
className = className.split(' ');
}
if(typeof className === 'object' && typeof className.push === 'function') {
for(cI = 0; cI < className.length; cI += 1) {
removeClass(className[cI]);
}
return true;
}
//validate
if(typeof className !== 'string') { throw new Error('Cannot remove class. The class name must be a string.'); }
for(eI = 0; eI < elements.length; eI += 1) {
classes = elements[eI].className.split(' ');
while(classes.indexOf(className) > -1) {
classes.splice(classes.indexOf(className), 1);
}
elements[eI].className = classes.join(' ');
}
return true;
}
/**
* Replaces the classes on an element
* @param className
* @return {*}
*/
function className(className) {
var eI;
//handle objects
if(typeof className === 'object' && typeof className.push === 'function') {
className = className.join(' ');
}
//validate
if(className && typeof className !== 'string') { throw new Error('Cannot set className. The className name must be a string.'); }
if(typeof className === "undefined") {
if(elements.length < 1) { return []; }
if(elements[0].hasAttribute('class')) {
return elements[0].getAttribute('class').split(' ');
} else {
return [];
}
} else {
attribute('class', className);
}
for(eI = 0; eI < elements.length; eI += 1) {
if(elements[eI].className.split(' ').indexOf(className) < 0) {
return false;
}
}
return true;
}
/**
* Sets the width of the selected elements
* @param width
*/
function width(width, duration) {
if(typeof width !== 'undefined' && typeof width !== 'boolean' && typeof width !== 'number' && typeof width !== 'string') { throw new Error('Cannot set element width. The width must be a number or string.'); }
if(duration && typeof duration !== 'number') { throw new Error('Cannot set element width. If given the duration must be a number.'); }
if(typeof width === 'number') { width += 'px'; }
if(typeof width !== 'undefined') {
if(duration) {
return animateCSS('width', width, duration);
} else {
return setCss('width', width);
}
} else {
return parseFloat(getCss('width'));
}
}
/**
* Sets the height of the selected elements
* @param height
*/
function height(height, duration) {
if(typeof height !== 'undefined' && typeof height !== 'boolean' && typeof height !== 'number' && typeof height !== 'string') { throw new Error('Cannot set element height. The height must be a number or string.'); }
if(duration && typeof duration !== 'number') { throw new Error('Cannot set element width. If given the duration must be a number.'); }
if(typeof height === 'number') { height += 'px'; }
if(typeof height !== 'undefined') {
if(duration) {
return animateCSS('height', height, duration);
} else {
return setCss('height', height);
}
} else {
return parseFloat(getCss('height'));
}
}
/**
* Gets the outer width
* @return {Number}
*/
function outerWidth(includeMargin) {
var outerWidth;
outerWidth = elements[0].offsetWidth;
if(includeMargin) {
outerWidth += parseFloat(getCss('margin-left')) || 0;
outerWidth += parseFloat(getCss('margin-right')) || 0;
}
return outerWidth;
}
/**
* Gets the outer height
* @return {Number}
*/
function outerHeight(includeMargin) {
var outerHeight;
outerHeight = elements[0].offsetHeight;
if(includeMargin) {
outerHeight += parseFloat(getCss('margin-top')) || 0;
outerHeight += parseFloat(getCss('margin-bottom')) || 0;
}
return outerHeight;
}
/**
* Returns the width of the scrollbar
* @return {*}
*/
function scrollbarWidth() {
var noScrollWidth, scrollWidth, overflowYValue;
overflowYValue = getCss('overflow-y') || false;
if(overflowYValue === 'auto' || overflowYValue === 'scroll') {
setCss('overflow-y', 'hidden');
} else {
return 0;
}
noScrollWidth = width();
if(overflowYValue) { setCss('overflow-y', overflowYValue); }
scrollWidth = width();
return noScrollWidth - scrollWidth;
}
/**
* Returns the height of the scrollbar
* @return {*}
*/
function scrollbarHeight() {
var noScrollHeight, scrollHeight, overflowYValue;
overflowYValue = getCss('overflow-x');
if(overflowYValue === 'auto' || overflowYValue === 'scroll') {
setCss('overflow-x', 'hidden');
} else {
return 0;
}
noScrollHeight = width();
if(overflowYValue) { setCss('overflow-x', overflowYValue); }
scrollHeight = width();
return noScrollHeight - scrollHeight;
}
/**
* Returns the offset of the first element in the selection
*/
function offset() {
return getElementOffset(elements[0]);
}
/**
* Returns the offset of the first element relative its parent
*/
function position() {
var elementOffset, parentOffset, x, y;
if(!elements.length) { return false; }
elementOffset = getElementOffset(elements[0]);
if(elements[0].offsetParent) {
parentOffset = getElementOffset(elements[0].offsetParent);
} else {
parentOffset = { "x": 0, "y": 0 };
}
x = elementOffset.x - parentOffset.x;
y = elementOffset.y - parentOffset.y;
if(elements[0].offsetParent && elements[0].offsetParent !== document.body) {
x += elements[0].offsetParent.scrollLeft || 0;
y += elements[0].offsetParent.scrollTop || 0;
}
x -= parseFloat(getCss('margin-left')) || 0;
y -= parseFloat(getCss('margin-top')) || 0;
x -= parseFloat(SpringJS(elements[0].offsetParent).css('border-left-width')) || 0;
y -= parseFloat(SpringJS(elements[0].offsetParent).css('border-top-width')) || 0;
return {
'x': x,
'y': y
};
}
/**
* Returns the offset of a given element
* @param element
* @return {Object}
*/
function getElementOffset(element) {
var clientRect, cX, cY, sX, sY;
clientRect = element.getBoundingClientRect();
if(!clientRect) {
return { "x": 0, "y": 0 };
}
cX = document.clientLeft || 0;
cY = document.clientTop || 0;
sX = typeof pageXOffset !== 'undefined' && pageXOffset || document.scrollLeft || 0;
sY = typeof pageYOffset !== 'undefined' && pageYOffset || document.scrollTop || 0;
return {
"x": clientRect.left + sX - cX,
"y": clientRect.top + sY - cY
};
}
/**
* Returns or sets the inner html of the first element
* @param html
*/
function html(html) {
var eI;
if(elements.length < 1) { return false; }
if(html && typeof html !== 'string') { throw new Error('Cannot set inner html. The html must be a string.'); }
if(typeof html === 'string') {
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].innerHTML = html;
}
return true;
} else {
return elements[0].innerHTML;
}
}
/**
* Inner html slice
*/
function htmlSlice( ) {
var args, eI;
args = Array.prototype.slice.apply(arguments);
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].innerHTML.slice.apply(this, args);
}
return true;
}
/**
* Splices the inner html of the selected elements
* @param start
* @param end
* @param html
*/
function htmlSplice(start, end, html) {
var eI, before, after, removed;
//validate
if(typeof start !== 'number') { throw new Error('Cannot splice html. The start index must be a number.'); }
if(typeof end !== 'undefined' && typeof end !== 'number') { throw new Error('Cannot splice html. If given the end index must be a number.'); }
if(html && typeof html !== 'string') { throw new Error('Cannot splice html. If given the html must be a string.'); }
//defaults
html = html || '';
//get the removed content from the first element
removed = elements[0].innerHTML.slice(start, end);
//update the content of each element
for(eI = 0; eI < elements.length; eI += 1) {
before = elements[eI].innerHTML.slice(0, start);
after = end && elements[eI].innerHTML.slice(end) || '';
elements[eI].innerHTML = before + html + after;
}
return removed;
}
/**
* Concat inner html
* @param html
*/
function htmlConcat(html) {
var eI;
if(typeof html !== 'string') { throw new Error('Cannot concatenate html. The html must be a string.'); }
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].innerHTML += html;
}
return true;
}
/**
* Returns or sets the inner html of the first element
* @param value
*/
function value(value) {
var eI;
if(value && typeof value !== 'string' && typeof value !== 'number') { throw new Error('Cannot set value. The value must be a string or number.'); }
if(typeof value !== 'undefined') {
if(value === false) { value = ''; }
for(eI = 0; eI < elements.length; eI += 1) {
elements[eI].value = value;
}
return true;
} else {
if(elements[0].tagName === 'form') {
} else {
if(typeof elements[0].value === "undefined") {
return false;
} else {
return elements[0].value || '';
}
}
}
}
}
//////////////////////////////
// STYLESHEET METHODS //
//////////////////////////////
function StyleSheet(rules) {
var rI, rule, css, property, pI;
//validate
if(rules === null || typeof rules !== 'object' || typeof rules.push !== 'function') { throw new Error('Cannot create stylesheet element. The rules must be an array.'); }
//create the head comment and validate the rules
css = '\n\n\t/*\n\t * Generated by SpringJS ' + version + '\n\t * \n\t * Contains:';
for(rI = 0; rI < rules.length; rI += 1) {
rule = rules[rI];
//validate
if(typeof rule !== 'object') { throw new Error('Cannot create stylesheet element. The each rule must be an object.'); }
if(typeof rule.selector !== 'string') { throw new Error('Cannot create stylesheet element. The each rule must have a selector. The selector must be a string.'); }
if(typeof rule.properties !== 'object') { throw new Error('Cannot create stylesheet element. The each rule must have properties. The properties must be an object.'); }
css += '\n\t * ' + rule.selector;
}
css += '\n\t */\n\n';
//loop through each rule
for(rI = 0; rI < rules.length; rI += 1) {
rule = rules[rI];
//add the selector to the css
css += '\t' + rule.selector + ' {\n';
//add each property
for(property in rule.properties) {
if(!rule.properties.hasOwnProperty(property)) { continue; }
if(typeof rule.properties[property] === 'object' && typeof rule.properties[property].push === 'function') {
for(pI = 0; pI < rule.properties[property].length; pI += 1) {
css += '\t\t' + property + ': ' + rule.properties[property][pI] + ';\n';
}
} else if(typeof rule.properties[property] === 'string') {
css += '\t\t' + property + ': ' + rule.properties[property] + ';\n';
}
}
//close the rule
css += '\t}\n\n';
}
return SpringJS('<style>' + css + '</style>');
}
////////////////////////
// AJAX METHODS //
////////////////////////
/**
* Fetches a file from a url and emits a success event passing its contents to a its handlers
* @param url
*/
function Fetch(url, headers, cache) {
if(typeof headers === 'boolean') {
cache = headers;
headers = null;
}
return Request(url, 'GET', null, headers, cache);
}
/**
* Fetches a script and executes it on the window. Emits a success event once done.
* @param scriptUrl
*/
function FetchScript(scriptUrl, headers, cache) {
var emitter, request;
emitter = EventEmitter();
request = Fetch(scriptUrl, headers, cache);
request.on('success', function(data, headers) {
if(emitter.trigger('success', data, headers)) {
try {
eval.call(window, data);
emitter.trigger('complete');
} catch(err) {
emitter.trigger('error', err);
}
}
});
request.script = emitter;
return request;
}
/**
* Fetches a json body and parses it.
* @param jsonUrl
*/
function fetchJSON(jsonUrl, headers, cache) {
var emitter, request, json;
emitter = EventEmitter();
request = Fetch(jsonUrl, headers, cache);
request.on('success', function(data) {
try {
json = JSON.parse(data);
} catch(err) {
emitter.trigger('error', err);
}
emitter.trigger('success', json);
});
request.json = emitter;
return request;
}
/**
* Creates a request object
* @param url
* @param method
* @param data
* @param cache
*/
function Request(url, method, data, headers, cache) {
var xhrObject, api, statusCode, dataURI, rawHeaders, responseHeaders, hI, header, key, value;
//defaults
data = data || null;
method = method.toUpperCase() || 'GET';
if(typeof method === 'object') {
cache = headers;
headers = data;
data = method;
method = 'GET';
}
if(typeof data === 'boolean') {
cache = data;
data = headers = null;
}
if(typeof headers === 'boolean') {
cache = headers;
headers = null;
}
//validate
if(typeof url !== 'string') { throw new Error('Cannot issue request. The url must be a string.'); }
if(typeof method !== 'string') { throw new Error('Cannot issue request. The method must be a string.'); }
if(data && typeof data !== 'string' && typeof data !== 'object') { throw new Error('Cannot issue request. If given the data must be a string or an object.'); }
if(headers && typeof headers !== 'object') { throw new Error('Cannot issue request. If given the headers must be an object.'); }
if(typeof cache !== 'undefined' && typeof cache !== 'boolean') { throw new Error('Cannot issue request. The cache flag is set it must be a boolean.'); }
//create an emitter
api = EventEmitter();
//attach clear
api.clear = clear;
//if no cache then append the time
if(!cache) {
if(url.indexOf('?') > -1) {
url += '&t=' + Date.now();
} else {
url += '?t=' + Date.now();
}
}
//create the XHR object
xhrObject = createXHR() || createActiveXXHR() || (function() { throw new Error('Cannot issue request. Failed to construct XHR object. The host document object model does not support AJAX.') })();
//setup an event handler
xhrObject.onreadystatechange = function(){
if(xhrObject.readyState !== 4) { return; }
//find a matching status code
statusCode = statusCodes[xhrObject.status];
if(!statusCodes[xhrObject.status]) {
statusCode = {'name': 'unknown', 'type': 'error' }
}
//parse the headers
rawHeaders = xhrObject.getAllResponseHeaders().split('\n');
responseHeaders = {};
for(hI = 0; hI < rawHeaders.length; hI += 1) {
header = rawHeaders[hI].split(':');
key = header[0];
value = header[1];
if(key && value) {
responseHeaders[key.trim()] = value.trim();
}
}
//trigger the events
api.trigger([xhrObject.status.toString(), statusCode.name, statusCode.type], xhrObject.responseText, responseHeaders);
//if a server error occurs also fire the error event
if(statusCode.type === 'servererror') {
api.trigger('error', xhrObject.responseText, responseHeaders);
}
//fire the status event
api.trigger('status', xhrObject.status, statusCode.name, statusCode.type);
};
//send the request
try {
//POST
if(method === 'POST') {
xhrObject.open(method, url, true);
if(typeof data === 'object') {
data = JSON.stringify(data);
dataURI = 'data=' + encodeURIComponent(data);
} else if(typeof data === 'string') {
dataURI = encodeURIComponent(data);
}
setHeaders();
xhrObject.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrObject.send(dataURI);
}
//PUT
else if(method === 'PUT') {
xhrObject.open(method, url, true);
if(typeof data === 'object') {
data = JSON.stringify(data);
setHeaders();
xhrObject.setRequestHeader("Content-type", "application/json");
} else {
xhrObject.setRequestHeader("Content-type", "text/plain");
setHeaders();
}
xhrObject.send(data);
} else if(method === 'GET') {
if(typeof data === 'object') { data = JSON.stringify(data); }
if(data !== 'null') {
dataURI = '?data=' + encodeURIComponent(data);
} else {
dataURI = '';
}
xhrObject.open(method, url + dataURI, true);
setHeaders();
xhrObject.send();
} else {
xhrObject.open(method, url, true);
setHeaders();
xhrObject.send();
}
} catch(err) {
api.trigger('failed', err);
}
return api;
/**
* Creates an XHR
*/
function createXHR() {
try {
return new XMLHttpRequest();
} catch(e) {}
return false;
}
/**
* Creates an ActiveX XHR
*/
function createActiveXXHR() {
try {
return new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {}
return false;
}
function setHeaders() {
var key;
if(headers) {
for(key in headers) {
if(!headers.hasOwnProperty(key)) { continue; }
xhrObject.setRequestHeader(key, headers[key]);
}
}
}
function clear() {
xhrObject.onreadystatechange = function(){};
return true;
}
}
/**
* Creates a REST pipe. The pipe will maintain a data object on the client side
* @param url
*/
function Bridge(url, interval, method) {
var api, data, api, running, firstUpdate;
//defualts
interval = interval || 1000;
firstUpdate = true;
//validate
if(typeof url !== 'string') { throw new Error('Cannot pipe REST. The url must be a string.'); }
//create an emitter
api = EventEmitter();
//create the data object
data = {};
//attack more api
api.data = data;
api.clear = clear;
running = true;
(function exec() {
var req;
if(!running) { return; }
//send and fetch the data
req = Request(url, firstUpdate && 'GET' || method || 'PUT', data, null, true);
//on success
req.on('success', function(json) {
json = json || "{}";
try {
//parse the data
json = JSON.parse(json);
//check to see if the data is different
if(!compare(data, json)) {
//merge in the data
mirror(data, json);
//fire the update event
api.trigger('update', clone(data));
if(firstUpdate) {
api.trigger('ready', data);
firstUpdate = false;
}
}
} catch(err) {
api.trigger('invalid');
}
//re-execute
setTimeout(exec, interval);
});
//on fail
req.on('error', function() {
api.trigger('error');
});
})();
return api;
function clear() {
running = false;
}
}
///////////////////////////////////
// OBJECT MODIFIER METHODS //
//////////////////////////////////
/**
* Merges any number of passed objects into a single object.
* If any objects passed are arrays the merged object will be
* an array too.
*/
function extend( ) {
var args, merged, aI, object, key;
//grab the args
args = Array.prototype.slice.call(arguments);
//look for arrays
merged = {};
for(aI = 0; aI < args.length; aI += 1) {
object = args[aI];
if(typeof object === 'undefined') { continue; }
//throw an error if an item is an invalid type
if(typeof object !== 'object') { throw new Error('Cannot extend objects. All arguments must be objects.'); }
//if we find an array then the merged object will be an array
if(typeof object.push === 'function') {
merged = [];
break;
}
}
//add the data to the merged object
for(aI = 0; aI < args.length; aI += 1) {
object = args[aI];
if(typeof object !== 'object') { continue; }
//loop through the object's properties
for(key in object) {
//the property must not be from a prototype
if(!object.hasOwnProperty(key)) { continue; }
//copy the property
if(typeof object[key] === 'object' && typeof merged[key] === 'object') {
merged[key] = extend(merged[key], object[key]);
} else if(typeof object[key] === 'object') {
merged[key] = clone(object[key]);
} else if(typeof merged.push === 'function') {
merged.push(object[key]);
} else {
merged[key] = object[key];
}
}
}
return merged;
}
/**
* Clones an object
* @param object
*/
function clone(object) {
var key, cloned, flags;
if(object === null || typeof object !== 'object') { return false; }
//create the empty clone
cloned = typeof object.push === 'function' && [] || {};
//loop through the object's properties
for(key in object) {
//the property must not be from a prototype
if(!object.hasOwnProperty(key)) { continue; }
//clone sub objects
//Regex
if(object[key] instanceof RegExp) {
flags = '';
object[key].global && (flags += 'g');
object[key].ignoreCase && (flags += 'i');
object[key].multiline && (flags += 'm');
cloned[key] = RegExp(object[key].source, flags);
}
//Event
else if(object[key] instanceof Event) {
cloned[key] = object[key];
}
//Canvas
else if(object[key] instanceof HTMLCanvasElement) {
cloned[key] = document.createElement('canvas');
cloned[key].width = object[key].width;
cloned[key].height = object[key].height;
cloned[key].getContext('2d').drawImage(object[key], 0, 0);
}
//HTML node
else if(object[key] instanceof HTMLBaseElement) {
cloned[key] = object[key];
}
//Object
else if(object[key] !== null && typeof object[key] === 'object') {
cloned[key] = clone(object[key]);
}
//Function, Number, String, Undefined, or Null
else {
cloned[key] = object[key];
}
}
return cloned;
}
/**
* Compares to variables or objects and returns true if they are the same. if they are not it will return false
* @param a
* @param b
*/
function compare(a, b) {
var equivalent, key;
//assume a and b match
equivalent = true;
//compare objects
if(typeof a === 'object' && typeof b === 'object') {
//check for additions or modifications
for(key in a) {
if(!a.hasOwnProperty(key)) { continue; }
if(!compare(a[key], b[key])) {
equivalent = false;
}
}
//check for deletions
for(key in b) {
if(!b.hasOwnProperty(key)) { continue; }
if(!compare(a[key], b[key])) {
equivalent = false;
}
}
}
//compare values
else if(typeof a === typeof b && typeof a !== 'object' && typeof a !== 'function') {
return a === b;
}
//return false on unknown
else {
return false;
}
return equivalent;
}
/**
* Takes a subject object and merges the secondary object into it.
*/
function merge( ) {
var objects, merged, oI, object, key;
//get an array of arguments
objects = Array.prototype.slice.apply(arguments);
//shift off the target object
merged = objects.shift();
//loop through the objects and merge each into the target
for(oI = 0; oI < objects.length; oI += 1) {
object = objects[oI];
if(typeof object === 'undefined') { continue; }
//validate
if(typeof object !== 'object') { throw new Error('Cannot merge objects. All arguments must be objects.'); }
for(key in object) {
//the property must not be from a prototype
if(!object.hasOwnProperty(key)) { continue; }
//copy the property
if(typeof object[key] === 'object' && typeof merged[key] === 'object') {
merge(merged[key], object[key]);
} else if(typeof object[key] === 'object') {
merged[key] = clone(object[key]);
} else if(typeof merged.push === 'function') {
merged.push(object[key]);
} else {
merged[key] = object[key];
}
}
}
}
/**
* Trims the subject object down so it only contains the properties of the model object
* @param subjectObject
* @param modelObject
*/
function reduce(subjectObject, modelObject) {
var sI, key;
if(typeof subjectObject !== 'object' || typeof modelObject !== 'object') {
throw new Error("UnityJS: While trying to reduce an object I realized that the application passed me a non object. Both the subject object and the model object must be real objects for me to preform a reduce.");
}
//if the subject is an array
if(typeof subjectObject.push === 'function') {
for(sI = 0; sI < subjectObject.length; sI += 1) {
if(typeof subjectObject[sI] === 'object' && typeof modelObject[sI] === 'object') {
reduce(subjectObject[sI], modelObject[sI]);
} else {
//check to see if the model has the same value
modelObject.indexOf(subjectObject[sI]);
if(modelObject.indexOf(subjectObject[sI]) < 0) {
subjectObject.splice(sI, 1);
sI -= 1;
}
}
}
} else {
//loop through the model
for(key in subjectObject) {
if(!subjectObject.hasOwnProperty(key)) { continue; }
if(typeof subjectObject[key] === 'object' && typeof modelObject[key] === 'object') {
reduce(subjectObject[key], modelObject[key]);
} else {
//check to see if the model has the same value
if(modelObject[key] !== subjectObject[key]) {
delete subjectObject[key];
}
}
}
}
}
/**
* Takes a model object and a mirror that will assume the models structure and values. This method is similar
* to clone accept it mutates the mirror object to resemble the model instead of producing a replacement.
* @param subjectObject
* @param modelObject
*/
function mirror(subjectObject, modelObject) {
var sPI, mPI, subjectProperty, modelProperty;
//validate
if(typeof subjectObject !== 'object' || typeof modelObject !== 'object') { throw new Error('Cannot mirror object. Both the subject object and the model object must be objects.')}
if(typeof subjectObject.push !== typeof modelObject.push) { throw new Error('Cannot mirror object. Both the subject object and the model object must be ether arrays or objects.'); }
if(typeof subjectObject.push === 'function') {
//loop through and remove old properties
for(sPI = 0; sPI < subjectObject.length; sPI += 1) {
if(modelObject.indexOf(subjectObject[sPI]) < 0) {
subjectObject.splice(sPI, 1);
sPI -= 1;
}
}
//add missing properties
for(mPI = 0; mPI < modelObject.length; mPI += 1) {
if(subjectObject.indexOf(modelObject[mPI]) < 0) {
subjectObject.push(modelObject[mPI]);
} else if(typeof modelObject[mPI] === 'object') {
sPI = subjectObject.indexOf(modelObject[mPI]);
if(typeof subjectObject[sPI] === 'object') {
mirror(subjectObject[sPI], modelObject[mPI]);
}
}
}
} else {
//loop through and remove old properties
for(subjectProperty in subjectObject) {
if(!subjectObject.hasOwnProperty(subjectProperty)) { continue; }
if(typeof modelObject[subjectProperty] === 'undefined') {
delete subjectObject[subjectProperty];
}
}
//add missing properties
for(modelProperty in modelObject) {
if(!modelObject.hasOwnProperty(modelProperty)) { continue; }
if(typeof subjectObject[modelProperty] === 'undefined') {
subjectObject[modelProperty] = modelObject[modelProperty];
} else if(typeof subjectObject[modelProperty] === 'object' && typeof modelObject[modelProperty] === 'object') {
mirror(subjectObject[modelProperty], modelObject[modelProperty]);
}
}
}
}
/**
* Watches a data structure and fires a callback when it changes
* @param data
*/
function watch(data) {
var mirrorObj, api, running;
if(!typeof onChange === 'function') { throw new Error('UnityJS: I tried to watch a data object for changes but the application gave me a invalid function as a handler.'); }
api = EventEmitter();
running = true;
api.clear = clear;
//create the mirror object and populate it with data
mirrorObj = {};
mirror(mirrorObj, data);
//register a function to compare the object to its last state every cycle
(function exec(){
if(!running) { return; }
//if the mirror doesn't match the data object then mirror it again and fire draw
if(!compare(mirrorObj, data)) {
mirror(mirrorObj, data);
api.trigger('update', data);
}
//self invoke
setTimeout(exec, 0);
})();
return api;
function clear() {
running = false;
}
}
function objectSize(data) {
var key;
if(typeof data !== 'object') { throw new Error('Cannot count the number of properties. The data must be an object'); }
for(key in data) {
if(data.hasOwnProperty(key)) {
return true;
}
}
return false;
}
///////////////////////////////////
// STRING MODIFIER METHODS //
///////////////////////////////////
/**
* Converts a hex color code to rgb
* @param colorCode
*/
function hexToRGB(colorCode) {
var red, blue, green;
if(colorCode.substr(0, 1) !== '#' && colorCode.length !== 4 && colorCode.length !== 7) { throw new Error('Cannot convert hex to rgb. ColorCode must be a valid hex color code.'); }
//lowercase the color code
colorCode = colorCode.toLowerCase();
//short
if(colorCode.length === 4) {
red = colorCode.substr(1, 1);
green = colorCode.substr(2, 1);
blue = colorCode.substr(3, 1);
red += red;
green += green;
blue += blue;
}
//full
else {
red = colorCode.substr(1, 2);
green = colorCode.substr(3, 2);
blue = colorCode.substr(5, 2);
}
red = parseInt(red, 16);
green = parseInt(green, 16);
blue = parseInt(blue, 16);
return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
}
function RGBToArray(rgbString) {
var array, i;
//validate
if(typeof rgbString !== 'string') { throw new Error('Cannot convert rgb to array. The rgb string must be a string.'); }
//get rid of the rgb( or rgba(
array = rgbString.replace(/rgba?\(/, '').replace(/\)/, '').replace(/[\s]/g, '').split(',');
for(i = 0; i < array.length; i += 1) {
array[i] = parseFloat(array[i]);
}
return array;
}
/**
* Converts a hyphen separated string to camel case
* @param string
*/
function camelCase(string) {
var words, wI;
if(typeof string !== 'string') { throw new Error('Cannot convert string to cammel case. The string must be a string.'); }
//split the text at '-'
words = string.split('-');
//capitalize each word (except for the first)
for(wI = 1; wI < words.length; wI += 1) {
words[wI] = words[wI].slice(0, 1).toUpperCase() + words[wI].slice(1);
}
return words.join('');
}
/////////////////////////////////
// MATH MODIFIER METHODS //
/////////////////////////////////
/**
* Creates a sequence number sequence that is yielded to a event over a timed duration
* @param startValue
* @param endValue
* @param duration
*/
function TimedSequence(startValue, endValue, duration) {
var diff, adjustment, running, startTime, currentTime, endTime, api;
//validate
if(typeof startValue !== 'number') { throw new Error('Cannot initialize sequence. The start value must be a number.'); }
if(typeof endValue !== 'number') { throw new Error('Cannot initialize sequence. The end value must be a number.'); }
if(typeof duration !== 'number') { throw new Error('Cannot initialize sequence. The duration must be a number.'); }
//set the running boolean to true
running = true;
//create the event emitter
api = EventEmitter();
//setup the api
api.clear = clear;
//calculate diff
diff = endValue - startValue;
//save the timer
startTime = Date.now();
endTime = startTime + duration;
//preform the animation
(function exec() {
if(!running) { return; }
//calculate the time
currentTime = Date.now();
//calculate the adjustment
adjustment = (currentTime - startTime) * diff / duration;
//fire the update event
api.trigger('update', startValue + adjustment);
if(currentTime < endTime) {
setTimeout(exec, 0);
} else {
api.trigger(['update', 'complete'], endValue);
}
})();
return api;
function clear() {
running = false;
}
}
/**
* Creates a linear interpolation object that can be queried arbitrarily
* @param p0
* @param p1
* @return {Object}
* @constructor
*/
function LinearInterpolation(p0, p1) {
if(typeof p0 !== 'object') { throw new Error('Cannot create linear interpolation. p0 must be an object.'); }
if(typeof p1 !== 'object') { throw new Error('Cannot create linear interpolation. p1 must be an object.'); }
if(typeof p0.x !== 'number') { throw new Error('Cannot create linear interpolation. p0.x must be a number.'); }
if(typeof p0.y !== 'number') { throw new Error('Cannot create linear interpolation. p0.y must be a number.'); }
if(typeof p1.x !== 'number') { throw new Error('Cannot create linear interpolation. p1.x must be a number.'); }
if(typeof p1.y !== 'number') { throw new Error('Cannot create linear interpolation. p1.y must be a number.'); }
return {
"solveX": solveX,
"solveY": solveY
};
/**
* Solves
* @param y
* @return {Number}
*/
function solveX(y) {
return p0.x + ((((y - p0.y) * p1.x) - ((y - p0.y) * p0.x)) / (p1.y - p0.y));
}
function solveY(x) {
return p0.y + ((((x - p0.x) * p1.y) - ((x - p0.x) * p0.x)) / (p1.x - p0.x));
}
}
////////////////////////////////
// FLOW CONTROL METHODS //
////////////////////////////////
/**
* Creates an event loop
* @param loopInterval
*/
function EventLoop(loopInterval, schedulerLimit) {
var api, active, lastTime, scheduledCycles, cleared;
//validate arguments
if(loopInterval && typeof loopInterval !== 'number') { throw new Error('Cannot create event loop. If given the interval must be a number.'); }
if(schedulerLimit && typeof schedulerLimit !== 'number') { throw new Error('Cannot create event loop. If given the scheduler limit must be a number.'); }
loopInterval = loopInterval || 1;
schedulerLimit = schedulerLimit || 1;
api = s.emitter();
active = lastTime = cleared = false;
scheduledCycles = 0;
api.start = start;
api.stop = stop;
api.clear = clear;
api.interval = interval;
//execute the loop
loop(Date.now());
return api;
/**
* Self executing event loop runtime (only execute once)
* @param time
*/
function loop(time) {
var batchedCycles;
if(cleared) { return; }
if(active) {
//on first cycle set the last cycle time to the current cycle time
if(!lastTime) { lastTime = time; }
//add additional scheduled cycles if the expected frames is below ten
scheduledCycles += (time - lastTime) / loopInterval;
//cap the number of cycles
scheduledCycles > schedulerLimit && (scheduledCycles = schedulerLimit);
//get the number of expected cycles
batchedCycles = Math.floor(scheduledCycles);
//if there are cycles to be executed
if(batchedCycles > 0) {
//emit the "single" event for each expected cycle
for(var i = 0; i < batchedCycles; i += 1) {
api.trigger('every', time, batchedCycles);
}
//remove the batched cycles from the scheduled cycles
scheduledCycles -= batchedCycles;
//emit the "batch" event
api.trigger('cycle', time, batchedCycles);
}
}
//save the last cycle time
lastTime = time;
//schedule the next cycle
requestAnimationFrame(loop);
}
/**
* Start the loop
*/
function start() {
active = true;
}
/**
* Stop the loop
*/
function stop() {
active = false;
}
/**
* Kill the loop
*/
function clear() {
cleared = true;
}
/**
* Set the loop interval
* @param fpms
*/
function interval(fpms) {
//validate
if(fpms && typeof fpms !== 'number') { throw new Error('Cannot set loop interval. If given the fpms must be a number.'); }
if(fpms) {
return loopInterval = fpms;
} else {
return loopInterval;
}
}
}
/**
* Creates a callback funnel.
* @return {Function}
* @constructor
*/
function Funnel() {
var emitter, deployments, spent;
emitter = EventEmitter();
deployments = [];
emitter.on('exec', function() {
var dI, deployment, data, deploymentData;
data = [];
//make sure all of the deployments have completed there expected executions.
// return if not.
for(dI = 0; dI < deployments.length; dI += 1) {
if(deployments[dI].executions.expected !== deployments[dI].executions.completed) {
return;
}
}
//map all of the data
for(dI = 0; dI < deployments.length; dI += 1) {
deployment = deployments[dI];
//if the deployment only executed once then unwrap the data
if(deployment.data.length === 1) {
deploymentData = deployment.data[0];
} else {
deploymentData = deployment.data;
}
//if the deployment is named then add its data to the data array as a property
if(deployment.name) {
data[deployment.name] = deploymentData;
//if the deployment is not named then add its data to the array
} else {
data.push(deploymentData);
}
}
spent = true;
//emit the complete event and pass the data through
emitter.set('complete', data);
});
Deployment.on = emitter.on;
Deployment.once = emitter.once;
Deployment.listeners = emitter.listeners;
//return the api
return Deployment;
/**
* Deploys a funnel instance
* @param name
* @param expectedExecutions
*/
function Deployment(name, expectedExecutions, returned) {
var deployment;
//map
if(typeof name === 'number') {
returned = expectedExecutions;
expectedExecutions = name;
name = null;
}
//defaults
expectedExecutions = expectedExecutions || 1;
//validate
if(name && typeof name !== 'string') { throw new Error('Cannot deploy funnel. If given the deployment name must be a string.'); }
if(typeof expectedExecutions !== 'number') { throw new Error('Cannot deploy funnel. If given the expected executions must be a number.'); }
//create the deployment object
deployment = {
"name": name,
"executions": {
"expected": expectedExecutions,
"completed": 0
},
"data": []
};
//add the deployment to the deployments array
deployments.push(deployment);
//return a execution capture
return (function ( ) {
if(deployment.executions.completed < deployment.executions.expected) {
deployment.executions.completed += 1;
deployment.data.push(Array.prototype.slice.apply(arguments));
if(deployment.executions.completed === deployment.executions.expected) {
emitter.trigger('exec', deployment);
}
} else {
throw new Error('Funnel overflow. A funnel deployment was executed to many times.');
}
//return the return variable
return returned;
});
}
}
/**
* Queue
* @return {Object}
* @constructor
*/
function Queue() {
var queue, active;
queue = [];
active = false;
return add;
/**
* Add a callback to the queue
* @param callback
* @return {Object}
*/
function add(callback) {
var api;
queue.push(callback);
if(!active) {
startQueue();
}
api = {
"clear": clear
};
return api;
function clear() {
queue.splice(queue.indexOf(callback), 1);
return true;
}
}
/**
* Executes the queue
*/
function startQueue() {
if(queue.length < 1) { active = false; return; }
active = true;
queue[0](function() {
queue.shift();
startQueue();
});
}
}
/**
* Creates a event emitter
*/
function EventEmitter(object) {
var api, callbacks, pipedEmitters, setEvents;
if(object && (object === null || (typeof object !== 'object' && typeof object !== 'function'))) { throw new Error('Cannot augmented object with emitter. The object must be an object or function.'); }
//vars
api = object || {};
api.on = on;
api.once = once;
api.trigger = trigger;
api.set = set;
api.pipe = pipe;
api.listeners = getListeners;
api.listeners.clear = clearListeners;
callbacks = {};
pipedEmitters = [];
setEvents = [];
//return the api
return api;
/**
* Binds functions to events
* @param event
* @param callback
*/
function on(event, callback) {
var api, pEI, sEI;
if(typeof event !== 'string') { throw new Error('Cannot bind to event emitter. The passed event is not a string.'); }
if(typeof callback !== 'function') { throw new Error('Cannot bind to event emitter. The passed callback is not a function.'); }
//return the api
api = {
"clear": clear
};
//create the event namespace if it doesn't exist
if(!callbacks[event]) { callbacks[event] = []; }
//save the callback
callbacks[event].push(callback);
//bind to piped emitters
for(pEI = 0; pEI < pipedEmitters.length; pEI += 1) {
pipedEmitters[pEI].add(event);
}
//trigger set events next tick
if(setEvents[event]) {
//execute each argument set
for(sEI = 0; sEI < setEvents[event].length; sEI += 1) {
//trigger the set event
trigger.apply(this, setEvents[event][sEI]);
clearListeners(event);
}
}
//trigger the handler event
trigger('handler', event, callback);
//return the api
return api;
/**
* Unbinds the handler
*/
function clear() {
var i;
if(callbacks[event]) {
i = callbacks[event].indexOf(callback);
callbacks[event].splice(i, 1);
if(callbacks[event].length < 1) {
delete callbacks[event];
}
return true;
}
return false;
}
}
/**
* Binds a callback to an event. Will only execute once.
* @param event
* @param callback
*/
function once(event, callback) {
var handler, completed;
if(typeof event !== 'string') { throw new Error('Cannot bind to event emitter. The passed event is not a string.'); }
if(typeof callback !== 'function') { throw new Error('Cannot bind to event emitter. The passed callback is not a function.'); }
handler = on(event, function( ) {
//if the handler has already fired then exit
if(!completed) {
//set completed
completed = true;
//fire the callback
callback.apply(this, arguments);
//clear the handler. Use setTimeout just in case the handler is called before the
// handler api is returned.
setTimeout(function() {
handler.clear();
}, 0);
}
});
return true;
}
/**
* Triggers a given event and optionally passes its handlers all additional parameters
* @param event
*/
function trigger(event ) {
var args, cI, eI, blockEventBubble;
//validate the event
if(typeof event !== 'string' && typeof event !== "object" && typeof event.push !== 'function') { throw new Error('Cannot trigger event. The passed event is not a string or an array.'); }
if(typeof event.slice(0, 4) === 'DOM.') { throw new Error('Cannot trigger event. The passed event is not a string or an array.'); }
//get the arguments
args = Array.prototype.slice.apply(arguments).splice(1);
//handle event arrays
if(typeof event === 'object' && typeof event.push === 'function') {
//for each event in the event array self invoke passing the arguments array
for(eI = 0; eI < event.length; eI += 1) {
//add the event name to the beginning of the arguments array
args.unshift(event[eI]);
//trigger the event
if(trigger.apply(this, args) === false) {
blockEventBubble = true;
}
//shift off the event name
args.shift();
}
return !blockEventBubble;
}
//if the event has callbacks then execute them
if(callbacks[event]) {
//fire the callbacks
for(cI = 0; callbacks[event] && cI < callbacks[event].length; cI += 1) {
if(callbacks[event][cI].apply(this, args) === false) {
blockEventBubble = true;
}
}
}
return !blockEventBubble;
}
/**
* Gets event listeners
* @param event
*/
function getListeners(event) {
if(event && typeof event !== 'string') { throw new Error('Cannot retrieve listeners. If given the event must be a string.'); }
//return the listeners
if(event) {
return callbacks[event];
} else {
return callbacks;
}
}
/**
* Clears the listeners
* @param event
*/
function clearListeners(event) {
if(event && typeof event !== 'string') { throw new Error('Cannot clear listeners. If given the event must be a string.'); }
//return the listeners
if(event) {
callbacks[event] = [];
} else {
callbacks = {};
}
}
/**
* Sets an event on the emitter
* @param event
*/
function set(event ) {
var api;
//validate
if(typeof event !== 'string') { throw new Error('Cannot set event. the event must be a string.')}
//trigger the event and clear existing listeners
trigger.apply(this, arguments);
clearListeners(event);
api = {
"clear": clear
};
//trigger all future binds
if(!setEvents[event]) { setEvents[event] = []; }
setEvents[event].push(Array.prototype.slice.apply(arguments));
return api;
/**
* Clears the event
*/
function clear() {
setEvents[event].splice(setEvents[event].indexOf(arguments), 1);
if(setEvents[event].length < 1) { delete setEvents[event]; }
}
}
/**
* Pipes in the events from another emitter including DOM objects
* @param emitter
*/
function pipe(emitter) {
var pipe, pipeBindings, event, eI, pipedEmitter, pipedEvents;
//validate the element
if(!emitter || typeof emitter !== 'object' || typeof emitter.on !== 'function' && typeof emitter.addEventListener !== 'function' && typeof emitter.attachEvent !== 'function') {
throw new Error('Cannot pipe events. A vaild DOM object must be provided.');
}
pipeBindings = [];
pipedEvents = [];
//check to make sure were not piping the same emitter twice
for(eI = 0; eI < pipedEmitters.length; eI += 1) {
pipedEmitter = pipedEmitters[eI];
if(pipedEmitter.emitter === emitter) {
return true;
}
}
//create the pipe
pipe = {
"emitter": emitter,
"add": addEventToPipe
};
//add the emitter to the piped array
pipedEmitters.push(pipe);
//bind existing events
for(event in callbacks) {
if(!callbacks.hasOwnProperty(event)) { continue; }
addEventToPipe(event);
}
return {
"clear": clear
};
/**
* Takes an event type and binds to that event (if possible) on the piped emitter
* If the event fires it will be piped to this emitter.
* @param event
*/
function addEventToPipe(event) {
var pipeBinding = {};
//prevent EMITTER events from being bound
if(event.slice(0, 8) === 'EMITTER.') { return; }
//check to make sure the event has not been added
if(pipedEvents.indexOf(event) >= 0) { return; }
try {
//Spring or jQuery
if(emitter.on) {
pipeBinding = emitter.on(event, handler);
//fix for jquery
if(emitter.jquery && emitter.off) {
pipeBinding.clear = function() {
emitter.off(event, handler);
};
}
//DOM
} else if(emitter.addEventListener || emitter.attachEvent) {
//check for an existing handlers array
if(!emitter.data) { emitter.data = {}; }
if(!emitter.data.eventHandlers) { emitter.data.eventHandlers = {}; }
//setup the dom handler
if(!emitter.data.eventHandlers[event]) {
//create a handler array
emitter.data.eventHandlers[event] = [domHandler];
//DOM W3C
if(emitter.addEventListener) {
emitter.addEventListener(event, executeDomHandlers, false);
pipeBinding.clear = function() {
emitter.removeEventListener(event, executeDomHandlers, false);
delete emitter.data.eventHandlers[event];
};
}
//DOM IE
else if(emitter.attachEvent){
emitter.attachEvent('on' + event, executeDomHandlers);
pipeBinding.clear = function() {
emitter.detachEvent('on' + event, executeDomHandlers);
delete emitter.data.eventHandlers[event];
};
}
}
//bind to the existing handler
else {
emitter.data.eventHandlers[event].push(domHandler);
}
}
} catch(e) {}
pipeBindings.push(pipeBinding);
pipedEvents.push(event);
/**
* A universal handler to capture an event and relay it to the emitter
*/
function handler( ) {
var args, emitterArgs, emitterBubble, bubble;
args = Array.prototype.slice.call(arguments);
emitterArgs = clone(args);
args.unshift(event);
emitterArgs.unshift('EMITTER.' + event);
bubble = trigger.apply(this, args);
emitterBubble = trigger.apply(this, emitterArgs);
return !(!bubble || !emitterBubble);
}
/**
* A dom event handler to capture an event and relay it to the emitter
*/
function domHandler(eventObj ) {
var args, domArgs, domBubble, bubble;
args = Array.prototype.slice.call(arguments);
domArgs = clone(args);
args.unshift(event);
domArgs.unshift('DOM.' + event);
bubble = trigger.apply(this, args);
domBubble = trigger.apply(this, domArgs);
if(!bubble || !domBubble) {
//modern browsers
eventObj.stopPropagation && eventObj.stopPropagation();
eventObj.preventDefault && eventObj.preventDefault();
//legacy browsers
typeof eventObj.cancelBubble !== 'undefined' && (eventObj.cancelBubble = true);
typeof eventObj.returnValue !== 'undefined' && (eventObj.returnValue = false);
}
}
/**
* Executes all dom handlers attached to the current emitter under the current event
*/
function executeDomHandlers(eventObj) {
var hI;
//exit if there are no event handlers for the current event on the current emitter
if(
!emitter.data ||
!emitter.data.eventHandlers ||
!emitter.data.eventHandlers[event] ||
emitter.data.eventHandlers[event].length < 1
) {
return;
}
for(hI = 0; hI < emitter.data.eventHandlers[event].length; hI += 1) {
emitter.data.eventHandlers[event][hI](eventObj);
}
}
}
/**
* Clears the pipe so the emitter is no longer captured
*/
function clear() {
var pI;
pipedEmitters.splice(pipedEmitters.indexOf(emitter), 1);
for(pI = 0; pI < pipeBindings.length; pI += 1) {
pipeBindings[pI].clear();
}
}
}
}
////////////////////////////
// LOGGING METHODS //
///////////////////////////
function Timer(name) {
var api, startTime;
name = name || '';
api = {
"start": start,
"finish": finish,
"log": log
};
start();
return api;
/**
* Set the start date
*/
function start() {
startTime = Date.now();
}
/**
* Get the time since the start in milliseconds and returns a result object
* @param description
*/
function finish(description) {
return {
"name": name,
"description": description,
"time": Date.now() - startTime
};
}
/**
* Get the time since the start in milliseconds and logs it to the console
* @param description
*/
function log(description) {
description = description || '';
console.log(name + ': ' + description + ' - ' + (Date.now() - startTime) + 'ms');
}
}
/**
* Creates a domain
* @param exec
* @param whiteList
* @constructor
*/
function Domain(exec, whiteList) {
var langGlobals;
whiteList = whiteList || [];
langGlobals = [
'Array', 'Boolean', 'Date',
'Function', 'Iterator',
'Number', 'Object', 'RegExp',
'String', 'ArrayBuffer',
'Float32Array',
'Float64Array', 'Int16Array',
'Int32Array', 'Int8Array',
'Uint16Array', 'Uint32Array',
'Uint8Array',
'Uint8ClampedArray'
];
(function() {
var lI, property;
for(property in window) {
if(whiteList.indexOf(property) < 0) {
eval('var ' + property + ';');
}
}
for(property in window) {
if(whiteList.indexOf(property) < 0) {
eval('var ' + property + ';');
}
}
for(lI = 0; langGlobals.length; lI += 1) {
if(whiteList.indexOf(langGlobals[lI]) < 0) {
eval('var ' + langGlobals[lI] + ';');
}
}
lI = property = whiteList = langGlobals = undefined;
eval(exec);
}).call({});
}
/**
* Creates and returns an iterator function. The iterator function
*
* @return {Function}
* @constructor
*/
function Iterator() {
var i = 0; return function() { return i += 1; }
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment