abaaso
Version:
abaaso is a modern, lightweight Enterprise class RESTful JavaScript application framework.
2,115 lines (1,840 loc) • 276 kB
JavaScript
/**
* abaaso
*
* @author Jason Mulligan <jason.mulligan@avoidwork.com>
* @copyright 2014 Jason Mulligan
* @license BSD-3 <https://raw.github.com/avoidwork/abaaso/master/LICENSE>
* @link http://abaaso.com
* @module abaaso
* @version 3.11.12
*/
( function ( global ) {
var document = global.document,
location = global.location,
navigator = global.navigator,
server = typeof exports !== "undefined",
VERSIONS = 100,
$, abaaso, http, https, url;
if ( global.abaaso !== undefined ) {
return;
}
if ( server ) {
url = require( "url" );
http = require( "http" );
https = require( "https" );
mongodb = require( "mongodb" ).MongoClient;
format = require( "util" ).format;
if ( typeof Storage === "undefined" ) {
localStorage = require( "localStorage" );
}
if ( typeof XMLHttpRequest === "undefined" ) {
XMLHttpRequest = null;
}
}
/**
* abaaso
*
* @namespace
*/
abaaso = ( function () {
"use strict";
var bootstrap, external, has, slice;
/**
* Regex patterns used through abaaso
*
* `url` was authored by Diego Perini
*
* @type {Object}
*/
var regex = {
after_space : /\s+.*/,
android : /android/i,
allow : /^allow$/i,
allow_cors : /^access-control-allow-methods$/i,
alphanum : /^[a-zA-Z0-9]+$/,
and : /^&/,
asc : /\s+asc$/ig,
auth : /\/\/(.*)\@/,
blackberry : /blackberry/i,
"boolean" : /^(true|false)?$/,
boolean_number_string : /boolean|number|string/,
cdata : /\&|<|>|\"|\'|\t|\r|\n|\@|\$/,
checked_disabled : /checked|disabled/i,
chrome : /chrome/i,
complete_loaded : /^(complete|loaded)$/i,
csv_quote : /^\s|\"|\n|,|\s$/,
del : /^del/,
decimal : /^\d+.(\d+)/,
desc : /\s+desc$/i,
domain : /^[\w.-_]+\.[A-Za-z]{2,}$/,
double_slash : /\/\//,
down : /down/,
down_up : /down|up/,
email : /^[a-zA-Z0-9.!#$%&'*+\/=?\^_`{|}~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,253}[a-zA-Z0-9])?)*$/,
endslash : /\/$/,
element_update : /innerHTML|innerText|textContent|type|src/,
firefox : /firefox/i,
get_headers : /^(head|get|options)$/,
get_remove_set : /get|remove|set/,
hash : /^\#/,
hash_bang : /^\#\!?/,
header_replace : /:.*/,
header_value_replace : /.*:\s+/,
html : /^<.*>$/,
http_body : /200|201|202|203|206/,
http_ports : /80|443/,
ie : /msie|ie/i,
input_button : /button|submit|reset/,
integer : /(^-?\d\d*$)/,
ip : /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
is_xml : /^<\?xml.*\?>/,
ios : /ipad|iphone/i,
json_maybe : /json|plain|javascript/,
json_wrap : /^[\[\{]/,
jsonp_wrap : /([a-zA-Z0-9\.]+\()(.*)(\))$/,
klass : /^\./,
linux : /linux|bsd|unix/i,
no : /no-store|no-cache/i,
not_endpoint : /.*\//,
notEmpty : /\w{1,}/,
number : /(^-?\d\d*\.\d*$)|(^-?\d\d*$)|(^-?\.\d\d*$)|number/,
number_format_1 : /.*\./,
number_format_2 : /\..*/,
number_present : /\d{1,}/,
number_string : /number|string/i,
number_string_object : /number|object|string/i,
null_undefined : /null|undefined/,
observer_allowed : /click|error|key|mousedown|mouseup|submit/i,
observer_globals : /body|document|window/i,
object_type : /\[object Object\]/,
object_undefined : /object|undefined/,
opera : /opera/i,
osx : /macintosh/i,
patch : /^patch$/i,
phone : /^([0-9\(\)\/\+ \-\.]+)$/,
playbook : /playbook/i,
plural : /s$/,
primitive : /^(boolean|function|number|string)$/,
priv : /private/,
put_post : /^(post|put)$/i,
radio_checkbox : /^(radio|checkbox)$/i,
reflect : /function\s+\w*\s*\((.*?)\)/,
root : /^\/[^\/]/,
route_nget : /^(head|options)$/i,
route_methods : /^(all|delete|get|put|post|head|options)$/i,
safari : /safari/i,
scheme : /.*\/\//,
select : /select/i,
selector_is : /^:/,
selector_many : /\:|\.|\+|\~|\[/,
selector_complex : /\s+|\>|\+|\~|\:|\[/,
selector_split : /\s+|\>|\+|\~/,
set_del : /^(set|del|delete)$/,
sort_needle : /^.*:::/,
sort_value : /:::.*$/,
space_hyphen : /\s|-/,
string_boolean : /^(true|false)$/i,
string_object : /string|object/i,
string_true : /^true$/i,
svg : /svg/,
top_bottom : /top|bottom/i,
true_undefined : /true|undefined/i,
url : /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i,
webos : /webos/i,
windows : /windows/i,
word : /^\w+$/,
xml : /xml/i
};
/** @namespace array */
var array = {
/**
* Adds 'arg' to 'obj' if it is not found
*
* @method add
* @param {Array} obj Array to receive 'arg'
* @param {Mixed} arg Argument to set in 'obj'
* @return {Array} Array that was queried
*/
add : function ( obj, arg ) {
if ( !array.contains( obj, arg ) ) {
obj.push( arg );
}
return obj;
},
/**
* Preforms a binary search on a sorted Array
*
* @method binIndex
* @param {Array} obj Array to search
* @param {Mixed} arg Value to find index of
* @return {Number} Index of `arg` within `obj`
*/
binIndex : function ( obj, arg ) {
var min = 0,
max = obj.length - 1,
idx, val;
while ( min <= max ) {
idx = Math.floor( ( min + max ) / 2 );
val = obj[idx];
if ( val < arg ) {
min = idx + 1;
}
else if ( val > arg ) {
max = idx - 1;
}
else {
return idx;
}
}
return -1;
},
/**
* Returns an Object ( NodeList, etc. ) as an Array
*
* @method cast
* @param {Object} obj Object to cast
* @param {Boolean} key [Optional] Returns key or value, only applies to Objects without a length property
* @return {Array} Object as an Array
*/
cast : function () {
if ( server || ( !client.ie || client.version > 8 ) ) {
return function ( obj, key ) {
key = ( key === true );
var o = [];
if ( !isNaN( obj.length ) ) {
o = slice.call( obj );
}
else if ( key ) {
o = array.keys( obj );
}
else {
utility.iterate( obj, function ( i ) {
o.push( i );
});
}
return o;
};
}
else {
return function ( obj, key ) {
key = ( key === true );
var o = [];
if ( !isNaN( obj.length ) ) {
try {
o = slice.call( obj );
}
catch ( e ) {
utility.iterate( obj, function ( i, idx ) {
if ( idx !== "length" ) {
o.push( i );
}
});
}
}
else if ( key ) {
o = array.keys( obj );
}
else {
utility.iterate( obj, function ( i ) {
o.push( i );
});
}
return o;
};
}
},
/**
* Transforms an Array to a 2D Array of chunks
*
* @method chunk
* @param {Array} obj Array to parse
* @param {Number} size Chunk size ( integer )
* @return {Array} Chunked Array
*/
chunk : function ( obj, size ) {
var result = [],
nth = number.round( ( obj.length / size ), "up" ),
start = 0,
i = -1;
while ( ++i < nth ) {
start = i * size;
result.push( array.limit( obj, start, size ) );
}
return result;
},
/**
* Clears an Array without destroying it
*
* @method clear
* @param {Array} obj Array to clear
* @return {Array} Cleared Array
*/
clear : function ( obj ) {
return obj.length > 0 ? array.remove( obj, 0, obj.length ) : obj;
},
/**
* Clones an Array
*
* @method clone
* @param {Array} obj Array to clone
* @return {Array} Clone of Array
*/
clone : function ( obj ) {
return obj.slice();
},
/**
* Determines if obj contains arg
*
* @method contains
* @param {Array} obj Array to search
* @param {Mixed} arg Value to look for
* @return {Boolean} True if found, false if not
*/
contains : function ( obj, arg ) {
return ( array.index( obj, arg ) > -1 );
},
/**
* Creates a new Array of the result of `fn` executed against every index of `obj`
*
* @method collect
* @param {Array} obj Array to iterate
* @param {Function} fn Function to execute against indices
* @return {Array} New Array
*/
collect : function ( obj, fn ) {
var result = [];
array.each( obj, function ( i ) {
result.push( fn( i ) );
});
return result;
},
/**
* Compacts a Array by removing `null` or `undefined` indices
*
* @method compact
* @param {Array} obj Array to compact
* @param {Boolean} diff Indicates to return resulting Array only if there's a difference
* @return {Array} Compacted copy of `obj`, or null ( if `diff` is passed & no diff is found )
*/
compact : function ( obj, diff ) {
var result = [];
result = obj.filter( function ( i ) {
return !regex.null_undefined.test( i );
});
return !diff ? result : ( result.length < obj.length ? result : null );
},
/**
* Counts `value` in `obj`
*
* @method count
* @param {Array} obj Array to search
* @param {Mixed} value Value to compare
* @return {Array} Array of counts
*/
count : function ( obj, value ) {
return obj.filter( function ( i ) {
return ( i === value );
}).length;
},
/**
* Finds the difference between array1 and array2
*
* @method diff
* @param {Array} array1 Source Array
* @param {Array} array2 Comparison Array
* @return {Array} Array of the differences
*/
diff : function ( array1, array2 ) {
var result = [];
array.each( array1, function ( i ) {
if ( !array.contains( array2, i ) ) {
array.add( result, i );
}
});
array.each( array2, function ( i ) {
if ( !array.contains( array1, i ) ) {
array.add( result, i );
}
});
return result;
},
/**
* Iterates obj and executes fn
*
* Parameters for fn are 'value', 'index'
*
* @method each
* @param {Array} obj Array to iterate
* @param {Function} fn Function to execute on index values
* @param {Boolean} async [Optional] Asynchronous iteration
* @param {Number} size [Optional] Batch size for async iteration, default is 10
* @return {Array} Array
*/
each : function ( obj, fn, async, size ) {
var nth = obj.length,
i, offset;
if ( async !== true ) {
for ( i = 0; i < nth; i++ ) {
if ( fn.call( obj, obj[i], i ) === false ) {
break;
}
}
}
else {
size = size || 10;
offset = 0;
if ( size > nth ) {
size = nth;
}
utility.repeat( function () {
var i = 0,
idx;
for ( i = 0; i < size; i++ ) {
idx = i + offset;
if ( idx === nth || fn.call( obj, obj[idx], idx ) === false ) {
return false;
}
}
offset += size;
if ( offset >= nth ) {
return false;
}
}, undefined, undefined, false );
}
return obj;
},
/**
* Determines if an Array is empty
*
* @method empty
* @param {Array} obj Array to inspect
* @return {Boolean} `true` if there's no indices
*/
empty : function ( obj ) {
return ( obj.length === 0 );
},
/**
* Determines if `a` is equal to `b`
*
* @method equal
* @param {Array} a Array to compare
* @param {Array} b Array to compare
* @return {Boolean} `true` if the Arrays match
*/
equal : function ( a, b ) {
return ( json.encode( a ) === json.encode( b ) );
},
/**
* Fibonacci generator
*
* @method fib
* @param {Number} arg [Optional] Amount of numbers to generate, default is 100
* @return {Array} Array of numbers
*/
fib : function ( arg ) {
var result = [1, 1],
first = result[0],
second = result[1],
sum;
// Subtracting 1 to account for `first` & `second`
arg = ( arg || 100 ) - 1;
if ( isNaN( arg ) || arg < 2 ) {
throw new Error( label.error.invalidArguments );
}
while ( --arg ) {
sum = first + second;
first = second;
second = sum;
result.push( sum );
}
return result;
},
/**
* Fills `obj` with the evalution of `arg`, optionally from `start` to `offset`
*
* @method fill
* @param {Array} obj Array to fill
* @param {Mixed} arg String, Number of Function to fill with
* @param {Number} start [Optional] Index to begin filling at
* @param {Number} end [Optional] Offset from start to stop filling at
* @return {Array} Filled Array
*/
fill : function ( obj, arg, start, offset ) {
var fn = typeof arg === "function",
l = obj.length,
i = !isNaN( start ) ? start : 0,
nth = !isNaN( offset ) ? i + offset : l - 1;
if ( nth > ( l - 1) ) {
nth = l - 1;
}
while ( i <= nth ) {
obj[i] = fn ? arg( obj[i] ) : arg;
i++;
}
return obj;
},
/**
* Returns the first Array node
*
* @method first
* @param {Array} obj The array
* @return {Mixed} The first node of the array
*/
first : function ( obj ) {
return obj[0];
},
/**
* Flattens a 2D Array
*
* @method flat
* @param {Array} obj 2D Array to flatten
* @return {Array} Flatten Array
*/
flat : function ( obj ) {
var result = [];
result = obj.reduce( function ( a, b ) {
return a.concat( b );
}, result );
return result;
},
/**
* Creates a 2D Array from an Object
*
* @method fromObject
* @param {Object} obj Object to convert
* @return {Array} 2D Array
*/
fromObject : function ( obj ) {
return array.mingle( array.keys( obj ), array.cast( obj ) );
},
/**
* Facade to indexOf for shorter syntax
*
* @method index
* @param {Array} obj Array to search
* @param {Mixed} arg Value to find index of
* @return {Number} The position of arg in instance
*/
index : function ( obj, arg ) {
return obj.indexOf( arg );
},
/**
* Returns an Associative Array as an Indexed Array
*
* @method indexed
* @param {Array} obj Array to index
* @return {Array} Indexed Array
*/
indexed : function ( obj ) {
var indexed = [];
utility.iterate( obj, function ( v ) {
indexed.push( v );
});
return indexed;
},
/**
* Finds the intersections between array1 and array2
*
* @method intersect
* @param {Array} array1 Source Array
* @param {Array} array2 Comparison Array
* @return {Array} Array of the intersections
*/
intersect : function ( array1, array2 ) {
var a = array1.length > array2.length ? array1 : array2,
b = ( a === array1 ? array2 : array1 );
return a.filter( function ( key ) {
return array.contains( b, key );
});
},
/**
* Keeps every element of `obj` for which `fn` evaluates to true
*
* @method keepIf
* @param {Array} obj Array to iterate
* @param {Function} fn Function to test indices against
* @return {Array} Array
*/
keepIf : function ( obj, fn ) {
if ( typeof fn !== "function" ) {
throw new Error( label.error.invalidArguments );
}
var result = [],
remove = [];
result = obj.filter( fn );
remove = array.diff( obj, result );
array.each( remove, function ( i ) {
array.remove( obj, array.index( obj, i ) );
});
return obj;
},
/**
* Sorts an Array based on key values, like an SQL ORDER BY clause
*
* @method sort
* @param {Array} obj Array to sort
* @param {String} query Sort query, e.g. "name, age desc, country"
* @param {String} sub [Optional] Key which holds data, e.g. "{data: {}}" = "data"
* @return {Array} Sorted Array
*/
keySort : function ( obj, query, sub ) {
query = query.replace( /\s*asc/ig, "" ).replace( /\s*desc/ig, " desc" );
var queries = string.explode( query ).map( function ( i ) { return i.split( " " ); }),
sorts = [];
if ( sub && sub !== "" ) {
sub = "." + sub;
}
else {
sub = "";
}
array.each( queries, function ( i ) {
var desc = i[1] === "desc";
if ( !desc ) {
sorts.push( "if ( a" + sub + "[\"" + i[0] + "\"] < b" + sub + "[\"" + i[0] + "\"] ) return -1;" );
sorts.push( "if ( a" + sub + "[\"" + i[0] + "\"] > b" + sub + "[\"" + i[0] + "\"] ) return 1;" );
}
else {
sorts.push( "if ( a" + sub + "[\"" + i[0] + "\"] < b" + sub + "[\"" + i[0] + "\"] ) return 1;" );
sorts.push( "if ( a" + sub + "[\"" + i[0] + "\"] > b" + sub + "[\"" + i[0] + "\"] ) return -1;" );
}
});
sorts.push( "else return 0;" );
return obj.sort( new Function( "a", "b", sorts.join( "\n" ) ) );
},
/**
* Returns the keys in an "Associative Array"
*
* @method keys
* @param {Mixed} obj Array or Object to extract keys from
* @return {Array} Array of the keys
*/
keys : function () {
if ( typeof Object.keys === "function" ) {
return function ( obj ) {
return Object.keys( obj );
};
}
else {
return function ( obj ) {
var keys = [];
utility.iterate( obj, function ( v, k ) {
keys.push( k );
});
return keys;
};
}
}(),
/**
* Returns the last index of the Array
*
* @method last
* @param {Array} obj Array
* @param {Number} arg [Optional] Negative offset from last index to return
* @return {Mixed} Last index( s ) of Array
*/
last : function ( obj, arg ) {
var n = obj.length - 1;
if ( arg >= ( n + 1 ) ) {
return obj;
}
else if ( isNaN( arg ) || arg === 1 ) {
return obj[n];
}
else {
return array.limit( obj, ( n - ( --arg ) ), n );
}
},
/**
* Returns a limited range of indices from the Array
*
* @method limit
* @param {Array} obj Array to iterate
* @param {Number} start Starting index
* @param {Number} offset Number of indices to return
* @return {Array} Array of indices
*/
limit : function ( obj, start, offset ) {
var result = [],
i = start - 1,
nth = start + offset,
max = obj.length;
if ( max > 0 ) {
while ( ++i < nth && i < max ) {
result.push( obj[i] );
}
}
return result;
},
/**
* Finds the maximum value in an Array
*
* @method max
* @param {Array} obj Array to parse
* @return {Mixed} Number, String, etc.
*/
max : function ( obj ) {
return array.last( obj.sort( array.sort ) );
},
/**
* Finds the mean of an Array ( of numbers )
*
* @method mean
* @param {Array} obj Array to parse
* @return {Number} Mean of the Array ( float or integer )
*/
mean : function ( obj ) {
return obj.length > 0 ? ( array.sum( obj ) / obj.length ) : undefined;
},
/**
* Finds the median value of an Array ( of numbers )
*
* @method median
* @param {Array} obj Array to parse
* @return {Number} Median number of the Array ( float or integer )
*/
median : function ( obj ) {
var nth = obj.length,
mid = number.round( nth / 2, "down" ),
sorted = obj.sort( array.sort );
return number.odd( nth ) ? sorted[mid] : ( ( sorted[mid - 1] + sorted[mid] ) / 2 );
},
/**
* Merges `arg` into `obj`, excluding duplicate indices
*
* @method merge
* @param {Array} obj Array to receive indices
* @param {Array} arg Array to merge
* @return {Array} obj
*/
merge : function ( obj, arg ) {
array.each( arg, function ( i ) {
array.add( obj, i );
});
return obj;
},
/**
* Finds the minimum value in an Array
*
* @method min
* @param {Array} obj Array to parse
* @return {Mixed} Number, String, etc.
*/
min : function ( obj ) {
return obj.sort( array.sort )[0];
},
/**
* Mingles Arrays and returns a 2D Array
*
* @method mingle
* @param {Array} obj1 Array to mingle
* @param {Array} obj2 Array to mingle
* @return {Array} 2D Array
*/
mingle : function ( obj1, obj2 ) {
var result;
result = obj1.map( function ( i, idx ) {
return [i, obj2[idx]];
});
return result;
},
/**
* Finds the mode value of an Array
*
* @method mode
* @param {Array} obj Array to parse
* @return {Mixed} Mode value of the Array
*/
mode : function ( obj ) {
var values = {},
count = 0,
nth = 0,
mode = [],
result;
// Counting values
array.each( obj, function ( i ) {
if ( !isNaN( values[i] ) ) {
values[i]++;
}
else {
values[i] = 1;
}
});
// Finding the highest occurring count
count = array.max( array.cast( values ) );
// Finding values that match the count
utility.iterate( values, function ( v, k ) {
if ( v === count ) {
mode.push( number.parse( k ) );
}
});
// Determining the result
nth = mode.length;
if ( nth > 0 ) {
result = nth === 1 ? mode[0] : mode;
}
return result;
},
/**
* Creates an Array of percentages from an Array of Numbers (ints/floats)
*
* @method percents
* @param {Array} obj Array to iterate
* @param {Number} precision [Optional] Rounding precision
* @param {Number} total [Optional] Value to compare against
* @return {Array} Array of percents
*/
percents : function ( obj, precision, total ) {
var result = [],
custom = false,
last, padding, sum;
precision = precision || 0;
if ( total === undefined ) {
total = array.sum( obj );
}
else {
custom = true;
}
array.each( obj, function ( i ) {
result.push( number.parse( ( ( i / total ) * 100 ).toFixed( precision ) ) );
} );
// Dealing with the awesomeness of JavaScript "integers"
if ( !custom ) {
sum = array.sum( result );
if ( sum < 100 ) {
padding = number.parse( number.diff( sum, 100 ).toFixed( precision ) );
last = array.last( result ) + padding;
result[result.length - 1] = last;
}
else if ( sum > 100 ) {
padding = number.parse( number.diff( sum, 100 ).toFixed( precision ) );
last = number.parse( ( array.last( result ) - padding ).toFixed( precision ) );
result[result.length - 1] = last;
}
}
return result;
},
/**
* Finds the range of the Array ( of numbers ) values
*
* @method range
* @param {Array} obj Array to parse
* @return {Number} Range of the array ( float or integer )
*/
range : function ( obj ) {
return array.max( obj ) - array.min( obj );
},
/**
* Searches a 2D Array `obj` for the first match of `arg` as a second index
*
* @method rassoc
* @param {Array} obj 2D Array to search
* @param {Mixed} arg Primitive to find
* @return {Mixed} Array or undefined
*/
rassoc : function ( obj, arg ) {
var result;
array.each( obj, function ( i, idx ) {
if ( i[1] === arg ) {
result = obj[idx];
return false;
}
});
return result;
},
/**
* Returns Array containing the items in `obj` for which `fn()` is not true
*
* @method reject
* @param {Array} obj Array to iterate
* @param {Function} fn Function to execute against `obj` indices
* @return {Array} Array of indices which fn() is not true
*/
reject : function ( obj, fn ) {
return array.diff( obj, obj.filter( fn ) );
},
/**
* Replaces the contents of `obj` with `arg`
*
* @method replace
* @param {Array} obj Array to modify
* @param {Array} arg Array to become `obj`
* @return {Array} New version of `obj`
*/
replace : function ( obj, arg ) {
array.remove( obj, 0, obj.length );
array.each( arg, function ( i ) {
obj.push( i );
});
return obj;
},
/**
* Removes indices from an Array without recreating it
*
* @method remove
* @param {Array} obj Array to remove from
* @param {Mixed} start Starting index, or value to find within obj
* @param {Number} end [Optional] Ending index
* @return {Array} Modified Array
*/
remove : function ( obj, start, end ) {
if ( isNaN( start ) ) {
start = obj.index( start );
if ( start === -1 ) {
return obj;
}
}
else {
start = start || 0;
}
var length = obj.length,
remaining = obj.slice( ( end || start ) + 1 || length );
obj.length = start < 0 ? ( length + start ) : start;
obj.push.apply( obj, remaining );
return obj;
},
/**
* Deletes every element of `obj` for which `fn` evaluates to true
*
* @method removeIf
* @param {Array} obj Array to iterate
* @param {Function} fn Function to test indices against
* @return {Array} Array
*/
removeIf : function ( obj, fn ) {
var remove;
if ( typeof fn !== "function" ) {
throw new Error( label.error.invalidArguments );
}
remove = obj.filter( fn );
array.each( remove, function ( i ) {
array.remove( obj, array.index ( obj, i ) );
});
return obj;
},
/**
* Deletes elements of `obj` until `fn` evaluates to false
*
* @method removeWhile
* @param {Array} obj Array to iterate
* @param {Function} fn Function to test indices against
* @return {Array} Array
*/
removeWhile : function ( obj, fn ) {
if ( typeof fn !== "function" ) {
throw new Error( label.error.invalidArguments );
}
var remove = [];
array.each( obj, function ( i ) {
if ( fn( i ) !== false ) {
remove.push( i );
}
else {
return false;
}
});
array.each( remove, function ( i ) {
array.remove( obj, array.index( obj, i) );
});
return obj;
},
/**
* Returns the "rest" of `obj` from `arg`
*
* @method rest
* @param {Array} obj Array to parse
* @param {Number} arg [Optional] Start position of subset of `obj` ( positive number only )
* @return {Array} Array of a subset of `obj`
*/
rest : function ( obj, arg ) {
arg = arg || 1;
if ( arg < 1 ) {
arg = 1;
}
return array.limit( obj, arg, obj.length );
},
/**
* Finds the last index of `arg` in `obj`
*
* @method rindex
* @param {Array} obj Array to search
* @param {Mixed} arg Primitive to find
* @return {Mixed} Index or undefined
*/
rindex : function ( obj, arg ) {
var result = -1;
array.each( obj, function ( i, idx ) {
if ( i === arg ) {
result = idx;
}
});
return result;
},
/**
* Returns new Array with `arg` moved to the first index
*
* @method rotate
* @param {Array} obj Array to rotate
* @param {Number} arg Index to become the first index, if negative the rotation is in the opposite direction
* @return {Array} Newly rotated Array
*/
rotate : function ( obj, arg ) {
var nth = obj.length,
result;
if ( arg === 0 ) {
result = obj;
}
else {
if ( arg < 0 ) {
arg += nth;
}
else {
arg--;
}
result = array.limit( obj, arg, nth );
result = result.concat( array.limit( obj, 0, arg ) );
}
return result;
},
/**
* Generates a series Array
*
* @method series
* @param {Number} start Start value the series
* @param {Number} end [Optional] The end of the series
* @param {Number} offset [Optional] Offset for indices, default is 1
* @return {Array} Array of new series
*/
series : function ( start, end, offset ) {
start = start || 0;
end = end || start;
offset = offset || 1;
var result = [],
n = -1,
nth = Math.max( 0, Math.ceil( ( end - start ) / offset ) );
while ( ++n < nth ) {
result[n] = start;
start += offset;
}
return result;
},
/**
* Splits an Array by divisor
*
* @method split
* @param {Array} obj Array to parse
* @param {Number} divisor Integer to divide the Array by
* @return {Array} Split Array
*/
split : function ( obj, divisor ) {
var result = [],
total = obj.length,
nth = Math.ceil( total / divisor ),
low = Math.floor( total / divisor ),
lower = Math.ceil( total / nth ),
lowered = false,
start = 0,
i = -1;
// Finding the fold
if ( number.diff( total, ( divisor * nth ) ) > nth ) {
lower = total - ( low * divisor ) + low - 1;
}
else if ( total % divisor > 0 && lower * nth >= total ) {
lower--;
}
while ( ++i < divisor ) {
if ( i > 0 ) {
start = start + nth;
}
if ( !lowered && lower < divisor && i === lower ) {
--nth;
lowered = true;
}
result.push( array.limit( obj, start, nth ) );
}
return result;
},
/**
* Sorts the Array by parsing values
*
* @method sort
* @param {Mixed} a Argument to compare
* @param {Mixed} b Argument to compare
* @return {Number} Number indicating sort order
*/
sort : function ( a, b ) {
var types = {a: typeof a, b: typeof b},
c, d, result;
if ( types.a === "number" && types.b === "number" ) {
result = a - b;
}
else {
c = a.toString();
d = b.toString();
if ( c < d ) {
result = -1;
}
else if ( c > d ) {
result = 1;
}
else if ( types.a === types.b ) {
result = 0;
}
else if ( types.a === "boolean" ) {
result = -1;
}
else {
result = 1;
}
}
return result;
},
/**
* Sorts `obj` using `array.sort`
*
* @method sorted
* @param {Array} obj Array to sort
* @return {Array} Sorted Array
*/
sorted : function ( obj ) {
return obj.sort( array.sort );
},
/**
* Finds the standard deviation of an Array ( of numbers )
*
* @method stddev
* @param {Array} obj Array to parse
* @return {Number} Standard deviation of the Array ( float or integer )
*/
stddev : function ( obj ) {
return Math.sqrt( array.variance( obj ) );
},
/**
* Gets the summation of an Array of numbers
*
* @method sum
* @param {Array} obj Array to sum
* @return {Number} Summation of Array
*/
sum : function ( obj ) {
var result = 0;
if ( obj.length > 0 ) {
result = obj.reduce( function ( prev, cur ) {
return prev + cur;
});
}
return result;
},
/**
* Takes the first `arg` indices from `obj`
*
* @method take
* @param {Array} obj Array to parse
* @param {Number} arg Offset from 0 to return
* @return {Array} Subset of `obj`
*/
take : function ( obj, arg ) {
return array.limit( obj, 0, arg );
},
/**
* Gets the total keys in an Array
*
* @method total
* @param {Array} obj Array to find the length of
* @return {Number} Number of keys in Array
*/
total : function ( obj ) {
return array.indexed( obj ).length;
},
/**
* Casts an Array to Object
*
* @method toObject
* @param {Array} ar Array to transform
* @return {Object} New object
*/
toObject : function ( ar ) {
var obj = {},
i = ar.length;
while ( i-- ) {
obj[i.toString()] = ar[i];
}
return obj;
},
/**
* Returns an Array of unique indices of `obj`
*
* @method unique
* @param {Array} obj Array to parse
* @return {Array} Array of unique indices
*/
unique : function ( obj ) {
var result = [];
array.each( obj, function ( i ) {
array.add( result, i );
});
return result;
},
/**
* Finds the variance of an Array ( of numbers )
*
* @method variance
* @param {Array} obj Array to parse
* @return {Number} Variance of the Array ( float or integer )
*/
variance : function ( obj ) {
var nth = obj.length,
n = 0,
mean;
if ( nth > 0 ) {
mean = array.mean( obj );
array.each( obj, function ( i ) {
n += math.sqr( i - mean );
} );
return n / nth;
}
else {
return n;
}
},
/**
* Converts any arguments to Arrays, then merges elements of `obj` with corresponding elements from each argument
*
* @method zip
* @param {Array} obj Array to transform
* @param {Mixed} args Argument instance or Array to merge
* @return {Array} Array
*/
zip : function ( obj, args ) {
var result = [];
// Preparing args
if ( !(args instanceof Array) ) {
args = typeof args === "object" ? array.cast( args ) : [args];
}
array.each( args, function ( i, idx ) {
if ( !( i instanceof Array ) ) {
this[idx] = [i];
}
});
// Building result Array
array.each( obj, function ( i, idx ) {
result[idx] = [i];
array.each( args, function ( x ) {
result[idx].push( x[idx] || null );
});
});
return result;
}
};
/** @namespace cache */
var cache = {
// Collection URIs
items : {},
/**
* Garbage collector for the cached items
*
* @method clean
* @private
* @return {Undefined} undefined
*/
clean : function () {
return utility.iterate( cache.items, function ( v, k ) {
if ( cache.expired( k ) ) {
cache.expire( k, true );
}
});
},
/**
* Expires a URI from the local cache
*
* Events: expire Fires when the URI expires
*
* @method expire
* @private
* @param {String} uri URI of the local representation
* @param {Boolean} silent [Optional] If 'true', the event will not fire
* @return {Undefined} undefined
*/
expire : function ( uri, silent ) {
silent = ( silent === true );
if ( cache.items[uri] !== undefined ) {
delete cache.items[uri];
if ( !silent ) {
observer.fire( uri, "beforeExpire, expire, afterExpire" );
}
return true;
}
else {
return false;
}
},
/**
* Determines if a URI has expired
*
* @method expired
* @private
* @param {Object} uri Cached URI object
* @return {Boolean} True if the URI has expired
*/
expired : function ( uri ) {
var item = cache.items[uri];
return item !== undefined && item.expires !== undefined && item.expires < new Date();
},
/**
* Returns the cached object {headers, response} of the URI or false
*
* @method get
* @private
* @param {String} uri URI/Identifier for the resource to retrieve from cache
* @param {Boolean} expire [Optional] If 'false' the URI will not expire
* @param {Boolean} silent [Optional] If 'true', the event will not fire
* @return {Mixed} URI Object {headers, response} or False
*/
get : function ( uri, expire ) {
uri = utility.parse( uri ).href;
expire = ( expire !== false );
if ( cache.items[uri] === undefined ) {
return false;
}
if ( expire && cache.expired( uri ) ) {
cache.expire( uri );
return false;
}
return utility.clone( cache.items[uri], true );
},
/**
* Sets, or updates an item in cache.items
*
* @method set
* @private
* @param {String} uri URI to set or update
* @param {String} property Property of the cached URI to set
* @param {Mixed} value Value to set
* @return {Mixed} URI Object {headers, response} or undefined
*/
set : function ( uri, property, value ) {
uri = utility.parse( uri ).href;
if ( cache.items[uri] === undefined ) {
cache.items[uri] = {};
cache.items[uri].permission = 0;
}
if ( property === "permission" ) {
cache.items[uri].permission |= value;
}
else if ( property === "!permission" ) {
cache.items[uri].permission &= ~value;
}
else {
cache.items[uri][property] = value;
}
return cache.items[uri];
}
};
/**
* Channel factory
*
* @method channel
* @return {Object} Channel instance
*/
var channel = function () {
return new Channel();
};
/**
* Channel
*
* @constructor
* @return {Object} Channel instance
*/
function Channel () {
this.queue = [];
}
// Setting constructor loop
Channel.prototype.constructor = Channel;
/**
* Puts an item into the Channel
*
* @method put
* @param {Mixed} arg Item
* @return {Object} Deferred instance
*/
Channel.prototype.put = function ( arg ) {
var defer = deferred();
if ( this.queue.length === 0 ) {
this.queue.push( arg );
defer.resolve( ["continue", null] );
}
else {
defer.resolve( ["pause", null] );
}
return defer;
};
/**
* Takes an item from the Channel
*
* @method take
* @return {Object} Deferred instance
*/
Channel.prototype.take = function () {
var defer = deferred();
if ( this.queue.length === 0 ) {
defer.resolve( ["pause", null] );
}
else {
defer.resolve( ["continue", this.queue.pop()] );
}
return defer;
};
/** @namespace client */
var client = {
/**
* ActiveX support
*
* @type {Boolean}
*/
activex : function () {
var result = false,
obj;
if ( typeof ActiveXObject !== "undefined" ) {
try {
obj = new ActiveXObject( "Microsoft.XMLHTTP" );
result = true;
}
catch ( e ) {}
}
return result;
}(),
/**
* Android platform
*
* @type {Boolean}
*/
android : function () {
return !server && regex.android.test( navigator.userAgent );
}(),
/**
* Blackberry platform
*
* @type {Boolean}
*/
blackberry : function () {
return !server && regex.blackberry.test( navigator.userAgent );
}(),
/**
* Chrome browser
*
* @type {Boolean}
*/
chrome : function () {
return !server && regex.chrome.test( navigator.userAgent );
}(),
/**
* Firefox browser
*
* @type {Boolean}
*/
firefox : function () {
return !server && regex.firefox.test( navigator.userAgent );
}(),
/**
* Internet Explorer browser
*
* @type {Boolean}
*/
ie : function () {
return !server && regex.ie.test( navigator.userAgent );
}(),
/**
* iOS platform
*
* @type {Boolean}
*/
ios : function () {
return !server && regex.ios.test( navigator.userAgent );
}(),
/**
* Linux Platform
*
* @type {Boolean}
*/
linux : function () {
return !server && regex.linux.test( navigator.userAgent );
}(),
/**
* Mobile platform
*
* @type {Boolean}
*/
mobile : function () {
var size;
if ( server ) {
return false;
}
else {
size = client.size();
return ( /blackberry|iphone|webos/i.test( navigator.userAgent ) || ( regex.android.test( navigator.userAgent ) && ( size[0] < 720 || size[1] < 720 ) ) );
}
},
/**
* Playbook platform
*
* @type {Boolean}
*/
playbook: function () {
return !server && regex.playbook.test( navigator.userAgent );
}(),
/**
* Opera browser
*
* @type {Boolean}
*/
opera : function () {
return !server && regex.opera.test( navigator.userAgent );
}(),
/**
* OSX platform
*
* @type {Boolean}
*/
osx : function () {
return !server && regex.osx.test( navigator.userAgent );
}(),
/**
* Safari browser
*
* @type {Boolean}
*/
safari : function () {
return !server && regex.safari.test( navigator.userAgent.replace(/chrome.*/i, "") );
}(),
/**
* Tablet platform
*
* Modern smartphone resolution makes this a hit/miss scenario
*
* @type {Boolean}
*/
tablet : function () {
var size;
if ( server ) {
return false;
}
else {
size = client.size();
return ( /ipad|playbook|webos/i.test( navigator.userAgent ) || ( regex.android.test( navigator.userAgent ) && ( size[0] >= 720 || size[1] >= 720 ) ) );
}
},
/**
* WebOS platform
*
* @type {Boolean}
*/
webos : function () {
return !server && regex.webos.test( navigator.userAgent );
}(),
/**
* Windows platform
*
* @type {Boolean}
*/
windows : function () {
return !server && regex.windows.test( navigator.userAgent );
}(),
/**
* Client version
*
* @type {Boolean}
*/
version : function () {
var version = 0;
if ( this.chrome ) {
version = navigator.userAgent.replace( /(.*chrome\/|safari.*)/gi, "" );
}
else if ( this.firefox ) {
version = navigator.userAgent.replace( /(.*firefox\/)/gi, "" );
}
else if ( this.ie ) {
version = navigator.userAgent.replace(/(.*msie|;.*)/gi, "");
}
else if ( this.opera ) {
version = navigator.userAgent.replace( /(.*version\/|\(.*)/gi, "" );
}
else if ( this.safari ) {
version = navigator.userAgent.replace( /(.*version\/|safari.*)/gi, "" );
}
else {
version = navigator.appVersion || "0";
}
version = number.parse( string.trim( version ) );
if ( isNaN( version ) ) {
version = 0;
}
if ( this.ie && document.documentMode && document.documentMode < version ) {
version = document.documentMode;
}
return version;
},
/**
* Quick way to see if a URI allows a specific verb
*
* @method allows
* @param {String} uri URI to query
* @param {String} verb HTTP verb
* @return {Boolean} `true` if the verb is allowed, undefined if unknown
*/
allows : function ( uri, verb ) {
if ( string.isEmpty( uri ) || string.isEmpty( verb ) ) {
throw new Error( label.error.invalidArguments );
}
uri = utility.parse( uri ).href;
verb = verb.toLowerCase();
var result = false,
bit = 0;
if ( !cache.get( uri, false ) ) {
result = undefined;
}
else {
if ( regex.del.test( verb ) ) {
bit = 1;
}
else if ( regex.get_headers.test( verb ) ) {
bit = 4;
}
else if ( regex.put_post.test( verb ) ) {
bit = 2;
}
else if ( regex.patch.test( verb ) ) {
bit = 8;
}
result = Boolean( client.permissions( uri, verb ).bit & bit );
}
return result;
},
/**
* Gets bit value based on args
*
* @method bit
* @param {Array} args Array of commands the URI accepts
* @return {Number} To be set as a bit
*/
bit : function ( args ) {
var result = 0;
array.each( args, function ( verb ) {
verb = verb.toLowerCase();
if ( regex.get_headers.test( verb ) ) {
result |= 4;
}
else if ( regex.put_post.test( verb ) ) {
result |= 2;
}
else if ( regex.patch.test( verb ) ) {
result |= 8;
}
else if ( regex.del.test( verb ) ) {
result |= 1;
}
});
return result;
},
/**
* Determines if a URI is a CORS end point
*
* @method cors
* @param {String} uri URI to parse
* @return {Boolean} True if CORS
*/
cors : function ( uri ) {
return ( !server && uri.indexOf( "//" ) > -1 && uri.indexOf( "//" + location.host ) === -1 );
},
/**
* Caches the headers from the XHR response
*
* @method headers
* @param {Object} xhr XMLHttpRequest Object
* @param {String} uri URI to request
* @param {String} type Type of request
* @return {Object} Cached URI representation
*/
headers : function ( xhr, uri, type ) {
var headers = string.trim( xhr.getAllResponseHeaders() ).split( "\n" ),
items = {},
o = {},
allow = null,
expires = new Date(),
cors = client.cors( uri );
array.each( headers, function ( i ) {
var header, value;
value = i.replace( regex.header_value_replace, "" );
header = i.replace( regex.header_replace, "" );
header = string.unhyphenate( header, true ).replace( /\s+/g, "-" );
items[header] = value;
if ( allow === null ) {
if ( ( !cors && regex.allow.test( header) ) || ( cors && regex.allow_cors.test( header) ) ) {
allow = value;
}
}
});
if ( regex.no.test( items["Cache-Control"] ) ) {
// Do nothing
}
else if ( items["Cache-Control"] !== undefined && regex.number_present.test( items["Cache-Control"] ) ) {
expires = expires.setSeconds( expires.getSeconds() + number.parse( regex.number_present.exec( items["Cache-Control"] )[0], 10 ) );
}
else if ( items.Expires !== undefined ) {
expires = new Date( items.Expires );
}
else {
expires = expires.setSeconds( expires.getSeconds() + $.expires );
}
o.expires = expires;
o.headers = items;
o.permission = client.bit( allow !== null ? string.explode( allow ) : [type] );
if ( type === "get" ) {
cache.set( uri, "expires", o.expires );
cache.set( uri, "headers", o.headers );
cache.set( uri, "permission", o.permission );
}
return o;
},
/**
* Parses an XHR response
*
* @method parse
* @param {Object} xhr XHR Object
* @param {String} type [Optional] Content-Type header value
* @return {Mixed} Array, Boolean, Document, Number, Object or String
*/
parse : function ( xhr, type ) {
type = type || "";
var result, obj;
if ( ( regex.json_maybe.test( type ) || string.isEmpty( type ) ) && ( regex.json_wrap.test( xhr.responseText ) && Boolean( obj = json.decode( xhr.responseText, true ) ) ) ) {
result = obj;
}
else if ( regex.xml.test( type ) ) {
if ( type !== "text/xml" ) {
xhr.overrideMimeType( "text/xml" );
}
result = xhr.responseXML;
}
else if ( type === "text/plain" && regex.is_xml.test( xhr.responseText) && xml.valid( xhr.responseText ) ) {
result = xml.decode( xhr.responseText );
}
else {
result = xhr.responseText;
}
return result;
},
/**
* Returns the permission of the cached URI
*
* @method permissions
* @param {String} uri URI to query
* @return {Object} Contains an Array of available commands, the permission bit and a map
*/
permissions : function ( uri ) {
var cached = cache.get( uri, false ),
bit = !cached ? 0 : cached.permission,
result = {allows: [], bit: bit, map: {partial: 8, read: 4, write: 2, "delete": 1, unknown: 0}};
if ( bit & 1) {
result.allows.push( "DELETE" );
}
if ( bit & 2) {
result.allows.push( "POST" );
result.allows.push( "PUT" );
}
if ( bit & 4) {
result.allows.push( "GET" );
}
if ( bit & 8) {
result.allows.push( "PATCH" );
}
return result;
},
/**
* Creates a JSONP request
*
* @method jsonp
* @param {String} uri URI to request
* @param {Function} success A handler function to execute when an appropriate response been received
* @param {Function} failure [Optional] A handler function to execute on error
* @param {Mixed} args Custom JSONP handler parameter name, default is "callback"; or custom headers for GET request ( CORS )
* @return {Object} Deferred
*/
jsonp : function ( uri, success, failure, args ) {
var defer = deferred(),
callback = "callback", cbid, s;
if ( external === undefined ) {
if ( global.abaaso === undefined ) {
utility.define( "abaaso.callback", {}, global );
}
external = "abaaso";
}
if ( args instanceof Object && args.callback !== undefined ) {
callback = args.callback;
}
defer.then( function (arg ) {
if ( typeof success === "function") {
success( arg );
}
}, function ( e ) {
if ( typeof failure === "function") {
failure( e );
}
throw e;
});
do {
cbid = utility.genId().slice( 0, 10 );
}
while ( global.abaaso.callback[cbid] !== undefined );
uri = uri.replace( callback + "=?", callback + "=" + external + ".callback." + cbid );
global.abaaso.callback[cbid] = function ( arg ) {
clearTimeout( utility.timer[cbid] );
delete utility.timer[cbid];
delete global.abaaso.callback[cbid];
defer.resolve( arg );
element.destroy( s );
};
s = element.create( "script", {src: uri, type: "text/javascript"}, utility.$( "head" )[0] );
utility.defer( function () {
defer.reject( undefined );
}, 30000, cbid );
return defer;
},
/**
* Creates an XmlHttpRequest to a URI ( aliased to multiple methods )
*
* The returned Deferred will have an .xhr property decorated
*
* Events: before[type] Fires before the XmlHttpRequest is made, type specific
* failed[type] Fires on error
* progress[type] Fires on progress
* progressUpload[type] Fires on upload progress
* received[type] Fires on XHR readystate 2
* timeout[type] Fires when XmlHttpRequest times out
*
* @method request
* @param {String} uri URI to query
* @param {String} type Type of request ( DELETE/GET/POST/PUT/HEAD )
* @param {Function} success A handler function to execute when an appropriate response been received
* @param {Function} failure [Optional] A handler function to execute on error
* @param {Mixed} args [Optional] Data to send with the request
* @param {Object} headers [Optional] Custom request headers ( can be used to set withCredentials )
* @param {Number} timeout [Optional] Timeout in milliseconds, default is 30000
* @return {Object} Deferred
*/
request : function ( uri, type, success, failure, args, headers, timeout ) {
timeout = timeout || 30000;
var cors, xhr, payload, cached, typed, contentType, doc, ab, blob, defer;
if ( ( regex.put_post.test( type ) || regex.patch.test( type ) ) && args === undefined ) {
throw new Error( label.error.invalidArguments );
}
uri = utility.parse( uri ).href;
type = type.toLowerCase();
headers = headers instanceof Object ? headers : null;
cors = client.cors( uri );
xhr = ( client.ie && client.version < 10 && cors ) ? new XDomainRequest() : ( !client.ie || ( client.version > 8 || type !== "patch") ? new XMLHttpRequest() : new ActiveXObject( "Microsoft.XMLHTTP" ) );
payload = ( regex.put_post.test( type ) || regex.patch.test( type ) ) && args !== undefined ? args : null;
cached = type === "get" ? cache.get( uri ) : false;
typed = type.capitalize();
contentType = null;
doc = ( typeof Document !== "undefined" );
ab = ( typeof ArrayBuffer !== "undefined" );
blob = ( typeof Blob !==