Last active
April 28, 2019 20:21
-
-
Save RobertWHurst/4080411 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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