/** @class Function @desc Native function prototype. @borrows Function#context as Function#bind */ /** @class String @desc Native string prototype. */ /** @class Array @desc Native array prototype. */ /** @class Object @desc Native object prototype. */ /** @class Boolean @desc Native boolean prototype. */ /** @class Number @desc Native number prototype. */ /** @class Date @desc Native date prototype. */ /** @class RegExp @desc Native regular expression prototype. */ /** @method Function#context @readonly @desc Binds "this" context of function to a specific object. @arg {Object} context - Object to act as parent (this) of function call. @returns {Function} Wrapper function. @see {@link Function#bind} */ !Function.prototype.context && (Function.prototype.context = function (context) { var fn = this; return function () { return fn.apply(context, arguments); }; }); !Function.prototype.bind && (Function.prototype.bind = Function.prototype.context); /** @method Array#keySort @readonly @chainable @desc Sort a list of objects by a specific property. @arg {String} property - Value of each object to compare. @arg {Number} [order=1] - Positive for ascending, negative for descending. @returns {Array} Sorted list. @see {@link http://en.wikipedia.org/wiki/Schwartzian_transform} @see {@link http://blog.rodneyrehm.de/archives/14-Sorting-Were-Doing-It-Wrong.html} @todo Use a schwartzian transform to cache values, preventing accesses for every comparison. Will have varying speed improvements depending on which comparison method the runtime uses, some make vastly more comparisons. */ !Array.prototype.keySort && (Array.prototype.keySort = function (property, order) { if (property) { // sanitize typeof order === 'string' && (order = Number(order)); (!order || isNaN(order)) && (order = 1); // default to ascending this.sort(function (a, b) { if (order < 0) // descending? swap a & b { var temp = a; a = b; b = temp; } // init var aValue = a ? a[property] : null, bValue = b ? b[property] : null; // sanitize if (!exists(aValue) && !exists(bValue)) { return 0; } // compare if (aValue) { if (typeof aValue === 'number' && typeof bValue === 'number') { return aValue == bValue ? 0 : (aValue > bValue ? 1 : -1); } return aValue.localeCompare(bValue); } else { return bValue.localeCompare(aValue); } }); } else { return this.sort(); } return this; }); /** @method Array#elementAttributeSort @readonly @chainable @desc Sort a list of elements by attribute(s). @arg {String} properties - Properties of each object to compare. @arg {Number} [order=1] - Positive for ascending, negative for descending. @returns {Array} Sorted list. @see {@link http://en.wikipedia.org/wiki/Schwartzian_transform} @see {@link http://blog.rodneyrehm.de/archives/14-Sorting-Were-Doing-It-Wrong.html} @todo Concatenating won't work with property values of different lengths: foo.a = 'ab'; foo.b = 'z'; bar.a = 'abc'; bar.b = 'y'; foo's a + b -> 'abz' bar's a + b -> 'abcy' ...foo will sort after bar though that's not what we want. Either: - Calculate the longest value (.toString() if it's not) length and right-pad them all with the appropriate number of spaces to make them equal. - (Faster) enforce the primary sort first because in most cases a sub-comparison isn't necessary. Also use a schwartzian transform to cache values, preventing accesses for every comparison. Will have varying speed improvements depending on which comparison method the runtime uses, some make vastly more comparisons. */ !Array.prototype.elementAttributeSort && (Array.prototype.elementAttributeSort = function (properties, order) { if (properties) { // sanitize typeof order === 'string' && (order = Number(order)); (!order || isNaN(order)) && (order = 1); // default to ascending properties = properties.split(/\s*,\s*/); this.sort(function (a, b) { // descending? swap a & b if (order < 0) { var temp = a; a = b; b = temp; } // init var aValue = '', bValue = ''; // concatenate values for (var i = 0; i < properties.length ; i++) { var aAttribute = a.attributes ? a.attributes[properties[i]] : null, bAttribute = b.attributes ? b.attributes[properties[i]] : null; aValue += aAttribute ? aAttribute.value : ''; bValue += bAttribute ? bAttribute.value : ''; } return aValue.localeCompare(bValue); }); } return this; }); /** @method Array#distinct @readonly @chainable @desc Filter a list to contain only unique values. @returns {Array} List of unique values. @todo Use [].filter(). As it iterates the array, items can be excluded if they match (strict, ===) any previous indexes. May or may not be more efficient, but would provide finer matching control than a plain indexOf lookup. */ !Array.prototype.distinct && (Array.prototype.distinct = function () { var distinct = []; for (var i = 0; i < this.length; i++) { var value = this[i]; !~distinct.indexOf(value) && distinct.push(value); } return distinct; }); /** @method Array#pushEach @readonly @desc Concatenates this list with another. @arg {Array} other - List to concatenate to this one. @returns {Number} Count of all values. @todo It would be more useful to return this (the array we're operating on) because the method could be chained. Length is easy to grab at any time. This method would be no different than native [...].concat([...]). */ !Array.prototype.pushEach && (Array.prototype.pushEach = function (other) { if (!Array.isArray(other)) { return this.length; } for (var i = 0; i < other.length; i++) { this.push(other[i]); } return this.length; }); /** @method String#format @readonly @desc Replaces numbered placeholders in a string with arguments provided. Placeholders are of the form "{#}" where "#" is the zero-based index of the argument to substitute. @arg {...Mixed} arguments - Any number of values referenced by placeholders in this string. @returns {String} Formatted string. */ !String.prototype.format && (String.prototype.format = function () { var args = arguments; return this.replace(/{(\d+)}/g, function(match, number) { return exists(args[number]) ? args[number] : match; }); }); /** @method Array#njoin @readonly @desc {@link Array#join}, skipping falsy values. @example ['a', '', 'c'].njoin(', ') -> 'a, c' @arg {String} separator - Characters to use as delimiters. @returns {String} Delimited string. @todo This will exclude ANY falsy values, e.g. '' or 0, not just null as originally described. Is this desired? Also use [].filter() to exclude, followed by a native join. */ !Array.prototype.njoin && (Array.prototype.njoin = function (separator) { !separator && (separator = ''); var output = ''; for (var i = 0; i < this.length; i++) { var value = this[i]; value && (output += value.toString() + separator); } return output.length ? output.substring(0, output.length - separator.length) : output; }); /** @method Array#njoin @readonly @desc {@link Array#split}, skipping resulting empty strings. @example 'a, , c'.nsplit(', ') -> ['a', 'c'] @arg {String} separator - Characters to detect as delimiters. @arg {Function} callback - Function to process indexes. @returns {Array} Resulting strings. @todo Use native filter after native split. Also callback functionality can be accomplished with filter on the returned array and needn't be duplicated here. */ !String.prototype.nsplit && (String.prototype.nsplit = function (separator, eachFunction) { var splitString = this.split(separator); for (var i = 0; i < splitString.length;) { if (!splitString[i]) { splitString.splice(i, 1); } else { eachFunction && (splitString[i] = eachFunction(splitString[i])); i++; } } return splitString; }); /** @method Object.getOwnValues @readonly @desc Queries object for non-inherited property values and returns a list of them. @arg {Object} object - Item to query for non-inherited property values. @returns {Array} Object's own values. @todo Change to instance method. */ !Object.getOwnValues && (Object.getOwnValues = function (object) { var keys = Object.getOwnPropertyNames(object), values = []; for (var i = 0; i < keys.length; i++) { var value = object[keys[i]]; typeof value !== 'function' && values.push(value); } return values; }); /** @method Date#format @readonly @desc Recreation of PHP's date function. @arg {String} format - Sequence of symbols to be replaced with corresponding date/time values. @returns {String} Formetted date/time string. @see {@link Date~replaceChars} @see {@link http://php.net/manual/en/function.date.php} */ !Date.prototype.format && (Date.prototype.format = function(format) { var returnStr = '', replace = Date.replaceChars; for (var i = 0; i < format.length; i++) { var curChar = format.charAt(i); if (i - 1 >= 0 && format.charAt(i - 1) == "\\") { returnStr += curChar; } else if (replace[curChar]) { returnStr += replace[curChar].call(this); } else if (curChar != "\\") { returnStr += curChar; } } return returnStr; }); /** @member {Object} Date~replaceChars @readonly @desc Index of symbols and corresponding replacement functions. @see {@link Date#format} @see {@link http://php.net/manual/en/function.date.php} */ !Date.replaceChars && (Date.replaceChars = { // display names 'shortMonths': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 'longMonths' : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 'shortDays' : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 'longDays' : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // day 'd': function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); }, 'D': function() { return Date.replaceChars.shortDays[this.getDay()]; }, 'j': function() { return this.getDate(); }, 'l': function() { return Date.replaceChars.longDays[this.getDay()]; }, 'N': function() { return this.getDay() + 1; }, 'S': function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); }, 'w': function() { return this.getDay(); }, 'z': function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now // week 'W': function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now // month 'F': function() { return Date.replaceChars.longMonths[this.getMonth()]; }, 'm': function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); }, 'M': function() { return Date.replaceChars.shortMonths[this.getMonth()]; }, 'n': function() { return this.getMonth() + 1; }, 't': function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate() }, // Fixed now, gets #days of date // year 'L': function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now 'o': function() { var d = new Date(this.valueOf()); d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now 'Y': function() { return this.getFullYear(); }, 'y': function() { return ('' + this.getFullYear()).substr(2); }, // time 'a': function() { return this.getHours() < 12 ? 'am' : 'pm'; }, 'A': function() { return this.getHours() < 12 ? 'AM' : 'PM'; }, 'B': function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now 'g': function() { return this.getHours() % 12 || 12; }, 'G': function() { return this.getHours(); }, 'h': function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); }, 'H': function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); }, 'i': function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); }, 's': function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); }, 'u': function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; }, // zone 'e': function() { return 'Not Yet Supported'; }, 'I': function() { var DST = null; for (var i = 0; i < 12; ++i) { var d = new Date(this.getFullYear(), i, 1), offset = d.getTimezoneOffset(); if (DST === null) { DST = offset; } else if (offset < DST) { DST = offset; break; } else if (offset > DST) { break; } } return (this.getTimezoneOffset() == DST) | 0; }, 'O': function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; }, 'P': function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // fixed now 'T': function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;}, 'Z': function() { return -this.getTimezoneOffset() * 60; }, // full date/time 'c': function() { return this.format('Y-m-d\\TH:i:sP'); }, // fixed now 'r': function() { return this.toString(); }, 'U': function() { return this.getTime() / 1000; } }); /** @method String#parseUrl @readonly @desc Break URL into components. @returns {Object} Index of URL components. */ !String.prototype.parseUrl && (String.prototype.parseUrl = function () { var anchor = $('<a></a>').attr('href', this)[0], properties = ['href', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin'], renames = {'href': 'url', 'pathname': 'path', 'search': 'query'}, parsed = {}, query = {}; // retrieve properties properties.forEach(function (key) { parsed[renames[key] || key] = anchor[key]; }); // split query parsed.query.substr(1).split('&').forEach(function (param) { param = param.split('='); query[param[0]] = param[1]; }); // reformat $.extend(parsed, { 'protocol': parsed.protocol.substr(0, parsed.protocol.length - 1), 'port' : Number(parsed.port) || null, 'path' : parsed.path.substr(1), 'query' : query, 'hash' : parsed.hash.substr(1) }); return parsed; }); /** @method String#capitalize @readonly @desc Capitalizes the first character in a string. @returns {String} Capitalized string. */ !String.prototype.capitalize && (String.prototype.capitalize = function () { return this.charAt(0).toUpperCase() + this.slice(1); }); /** @method Object#toArray @readonly @desc Lists property values of an object. Useful for converting indexed data to a loopable list. @returns {Object} Index of URL components. @see {@link dataObjectToArray} */ !Object.prototype.toArray && Object.defineProperty(Object.prototype, 'toArray', { 'enumerable' : false, 'configurable': false, 'writable' : false, 'value' : function () { var arr = []; for (prop in this) { this.hasOwnProperty(prop) && arr.push(this[prop]); } return arr; } }); /** @method Object#extend @readonly @desc Merge contents of one or more objects into this object. @arg {...Object} arguments - Objects to merge into this one. @returns {Object} Merge result. @see {@link Object#extend(2)} @see {@link http://api.jquery.com/jQuery.extend/} */ /** @method Object#extend(2) @readonly @desc Recursively merge contents of one or more objects into this object. @arg {Boolean} deep - Recursive flag. @arg {...Object} arguments - Objects to merge into this one. @returns {Object} Merge result. @see {@link Object#extend} @see {@link http://api.jquery.com/jQuery.extend/} */ !Object.prototype.extend && Object.defineProperty(Object.prototype, 'extend', { 'enumerable' : false, 'configurable': false, 'writable' : false, 'value' : function () { var deep = arguments[0] === true, objs = Array.prototype.slice.call(arguments).splice(deep ? 1 : 0, 0, this); return $.extend.apply($, objs); } }); /** @method Object#renameProperty @readonly @desc Changes the name of a property. @arg {String} keyOld - Current name of property. @arg {String} keyNew - Desired name of property. @returns {Object} Altered object. */ !Object.prototype.renameProperty && Object.defineProperty(Object.prototype, 'renameProperty', { 'enumerable' : false, 'configurable': false, 'writable' : false, 'value' : function (keyOld, keyNew) { if (this.hasOwnProperty(keyOld) && keyOld !== keyNew) { this[keyNew] = this[keyOld]; delete this[keyOld]; } return this; } }); // Number = function () // { // //$.extend(this, Number.apply(window, arguments)); // this.prototype = Number.prototype; // var hz = null; // Object.defineProperty(this.prototype, 'hz', // { // 'enumerable' : true, // 'configurable': true, // //'writable' : true, // 'get' : function () { console.log(this, arguments); return 5; }, // 'set' : function (rate) { console.log(this, arguments, rate); hz = Math.round(1000 / rate); } // }); // }; /** @method String#toBoolean @readonly @desc Detects if a string === 'true'. Useful for working with attribute or query string values. @arg {String} prop - Name of property value to retrieve from each object. @returns {Array} List of property values. */ !String.prototype.toBoolean && (String.prototype.toBoolean = function () { return /^true|yes|on$/i.test(this); }); /** @method Array#pluck @readonly @desc Queries an array of objects for a specific property and returns a list of those values. @arg {String} prop - Name of property value to retrieve from each object. @returns {Array} List of property values. @see {@link http://documentcloud.github.io/underscore/#pluck} */ !Array.prototype.pluck && (Array.prototype.pluck = function (prop) { return this.map(function (obj, index) { return obj[prop] }); }); // in development !Object.prototype.pluck && Object.defineProperty(Object.prototype, 'pluck', { 'enumerable' : false, 'configurable': false, 'writable' : false, 'value' : function () { // init var properties = [].concat.apply([], [].slice.call(arguments)), propertiesLength = properties.length, obj = {}; // validate if (!propertiesLength) { console.log(L_WARN, 'Object.prototype.pluck', 'no arguments supplied'); return {}; } for (var index = 0; index < propertiesLength; index++) { var property = properties[index]; if (typeof property !== 'string') { console.log(L_WARN, 'Object.prototype.pluck', 'invalid argument found', property); return {}; } else { obj[property] = this[property]; } // add } return obj; } }); // in development !Array.prototype.flatten && (Array.prototype.flatten = function (prop) { return this.map(function (obj, index) { return obj[prop] }); });