diff --git a/merlin/static/merlin/lib/angular-filter.js b/merlin/static/merlin/lib/angular-filter.js new file mode 100644 index 0000000..e45ab7e --- /dev/null +++ b/merlin/static/merlin/lib/angular-filter.js @@ -0,0 +1,2186 @@ +/** + * Bunch of useful filters for angularJS(with no external dependencies!) + * @version v0.5.1 - 2014-11-12 * @link https://github.com/a8m/angular-filter + * @author Ariel Mashraki + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function ( window, angular, undefined ) { +/*jshint globalstrict:true*/ +'use strict'; + +var isDefined = angular.isDefined, + isUndefined = angular.isUndefined, + isFunction = angular.isFunction, + isString = angular.isString, + isNumber = angular.isNumber, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + equals = angular.equals; + + +/** + * @description + * get an object and return array of values + * @param object + * @returns {Array} + */ +function toArray(object) { + return isArray(object) ? object : + Object.keys(object).map(function(key) { + return object[key]; + }); +} + +/** + * @param value + * @returns {boolean} + */ +function isNull(value) { + return value === null; +} + +/** + * @description + * return if object contains partial object + * @param partial{object} + * @param object{object} + * @returns {boolean} + */ +function objectContains(partial, object) { + var keys = Object.keys(partial); + + return keys.map(function(el) { + return !(!object[el] || (object[el] != partial[el])); + }).indexOf(false) == -1; + +} + +/** + * @description + * search for approximate pattern in string + * @param word + * @param pattern + * @returns {*} + */ +function hasApproxPattern(word, pattern) { + if(pattern === '') + return word; + + var index = word.indexOf(pattern.charAt(0)); + + if(index === -1) + return false; + + return hasApproxPattern(word.substr(index+1), pattern.substr(1)) +} + +/** + * @description + * return the first n element of an array, + * if expression provided, is returns as long the expression return truthy + * @param array + * @param n {number} + * @param expression {$parse} + * @return array or single object + */ +function getFirstMatches(array, n, expression) { + var count = 0; + + return array.filter(function(elm) { + var rest = isDefined(expression) ? (count < n && expression(elm)) : count < n; + count = rest ? count+1 : count; + + return rest; + }); +} +/** + * Polyfill to ECMA6 String.prototype.contains + */ +if (!String.prototype.contains) { + String.prototype.contains = function() { + return String.prototype.indexOf.apply(this, arguments) !== -1; + }; +} + +/** + * @param num {Number} + * @param decimal {Number} + * @param $math + * @returns {Number} + */ +function convertToDecimal(num, decimal, $math){ + return $math.round(num * $math.pow(10,decimal)) / ($math.pow(10,decimal)); +} + +/** + * @description + * Get an object, and return an array composed of it's properties names(nested too). + * @param obj {Object} + * @param stack {Array} + * @param parent {String} + * @returns {Array} + * @example + * parseKeys({ a:1, b: { c:2, d: { e: 3 } } }) ==> ["a", "b.c", "b.d.e"] + */ +function deepKeys(obj, stack, parent) { + stack = stack || []; + var keys = Object.keys(obj); + + keys.forEach(function(el) { + //if it's a nested object + if(isObject(obj[el]) && !isArray(obj[el])) { + //concatenate the new parent if exist + var p = parent ? parent + '.' + el : parent; + deepKeys(obj[el], stack, p || el); + } else { + //create and save the key + var key = parent ? parent + '.' + el : el; + stack.push(key) + } + }); + return stack +} + +/** + * @description + * Test if given object is a Scope instance + * @param obj + * @returns {Boolean} + */ +function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; +} +/** + * @ngdoc filter + * @name a8m.angular + * @kind function + * + * @description + * reference to angular function + */ + +angular.module('a8m.angular', []) + + .filter('isUndefined', function () { + return function (input) { + return angular.isUndefined(input); + } + }) + .filter('isDefined', function() { + return function (input) { + return angular.isDefined(input); + } + }) + .filter('isFunction', function() { + return function (input) { + return angular.isFunction(input); + } + }) + .filter('isString', function() { + return function (input) { + return angular.isString(input) + } + }) + .filter('isNumber', function() { + return function (input) { + return angular.isNumber(input); + } + }) + .filter('isArray', function() { + return function (input) { + return angular.isArray(input); + } + }) + .filter('isObject', function() { + return function (input) { + return angular.isObject(input); + } + }) + .filter('isEqual', function() { + return function (o1, o2) { + return angular.equals(o1, o2); + } + }); + +/** + * @ngdoc filter + * @name a8m.conditions + * @kind function + * + * @description + * reference to math conditions + */ + angular.module('a8m.conditions', []) + + .filter({ + isGreaterThan : isGreaterThanFilter, + '>' : isGreaterThanFilter, + + isGreaterThanOrEqualTo : isGreaterThanOrEqualToFilter, + '>=' : isGreaterThanOrEqualToFilter, + + isLessThan : isLessThanFilter, + '<' : isLessThanFilter, + + isLessThanOrEqualTo : isLessThanOrEqualToFilter, + '<=' : isLessThanOrEqualToFilter, + + isEqualTo : isEqualToFilter, + '==' : isEqualToFilter, + + isNotEqualTo : isNotEqualToFilter, + '!=' : isNotEqualToFilter, + + isIdenticalTo : isIdenticalToFilter, + '===' : isIdenticalToFilter, + + isNotIdenticalTo : isNotIdenticalToFilter, + '!==' : isNotIdenticalToFilter + }); + + function isGreaterThanFilter() { + return function (input, check) { + return input > check; + }; + } + + function isGreaterThanOrEqualToFilter() { + return function (input, check) { + return input >= check; + }; + } + + function isLessThanFilter() { + return function (input, check) { + return input < check; + }; + } + + function isLessThanOrEqualToFilter() { + return function (input, check) { + return input <= check; + }; + } + + function isEqualToFilter() { + return function (input, check) { + return input == check; + }; + } + + function isNotEqualToFilter() { + return function (input, check) { + return input != check; + }; + } + + function isIdenticalToFilter() { + return function (input, check) { + return input === check; + }; + } + + function isNotIdenticalToFilter() { + return function (input, check) { + return input !== check; + }; + } +/** + * @ngdoc filter + * @name isNull + * @kind function + * + * @description + * checks if value is null or not + * @return Boolean + */ + +angular.module('a8m.is-null', []) + + .filter('isNull', function () { + return function(input) { + return isNull(input); + } + }); + +/** + * @ngdoc filter + * @name after-where + * @kind function + * + * @description + * get a collection and properties object, and returns all of the items + * in the collection after the first that found with the given properties. + * + */ + +angular.module('a8m.after-where', []) + .filter('afterWhere', function() { + return function (collection, object) { + + collection = (isObject(collection)) ? + toArray(collection) : + collection; + + if(!isArray(collection) || isUndefined(object)) + return collection; + + var index = collection.map( function( elm ) { + return objectContains(object, elm); + }).indexOf( true ); + + return collection.slice((index === -1) ? 0 : index); + } + }); + +/** + * @ngdoc filter + * @name after + * @kind function + * + * @description + * get a collection and specified count, and returns all of the items + * in the collection after the specified count. + * + */ + +angular.module('a8m.after', []) + .filter('after', function() { + return function (collection, count) { + + collection = (isObject(collection)) ? + toArray(collection) : + collection; + + return (isArray(collection)) ? + collection.slice(count) : + collection; + + } + }); + +/** + * @ngdoc filter + * @name before-where + * @kind function + * + * @description + * get a collection and properties object, and returns all of the items + * in the collection before the first that found with the given properties. + * + */ + +angular.module('a8m.before-where', []) + .filter('beforeWhere', function() { + return function (collection, object) { + + collection = (isObject(collection)) ? + toArray(collection) : + collection; + + if(!isArray(collection) || isUndefined(object)) + return collection; + + var index = collection.map( function( elm ) { + return objectContains(object, elm); + }).indexOf( true ); + + return collection.slice(0, (index === -1) ? collection.length : ++index); + } + }); + +/** + * @ngdoc filter + * @name before + * @kind function + * + * @description + * get a collection and specified count, and returns all of the items + * in the collection before the specified count. + * + */ + +angular.module('a8m.before', []) + .filter('before', function() { + return function (collection, count) { + + collection = (isObject(collection)) ? + toArray(collection) : + collection; + + return (isArray(collection)) ? + collection.slice(0, (!count) ? count : --count) : + collection; + + } + }); + +/** + * @ngdoc filter + * @name concat + * @kind function + * + * @description + * get (array/object, object/array) and return merged collection + * + */ + +angular.module('a8m.concat', []) + //TODO:unique option ? or use unique filter to filter result + .filter('concat', [function () { + return function (collection, joined) { + + if (isUndefined(joined)) { + return collection; + } + if (isArray(collection)) { + return (isObject(joined)) ? + collection.concat(toArray(joined)) : + collection.concat(joined); + } + + if (isObject(collection)) { + var array = toArray(collection); + return (isObject(joined)) ? + array.concat(toArray(joined)) : + array.concat(joined); + } + return collection; + }; + } +]); + +/** + * @ngdoc filter + * @name contains + * @kind function + * + * @description + * Checks if given expression is present in one or more object in the collection + */ + +angular.module('a8m.contains', []) + .filter({ + contains: ['$parse', containsFilter], + some: ['$parse', containsFilter] + }); + +function containsFilter( $parse ) { + return function (collection, expression) { + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return true; + } + + return collection.some( function(elm) { + + return (isObject(elm) || isFunction(expression)) ? + $parse(expression)(elm) : + elm === expression; + + }); + + } + } + +/** + * @ngdoc filter + * @name countBy + * @kind function + * + * @description + * Sorts a list into groups and returns a count for the number of objects in each group. + */ + +angular.module('a8m.count-by', []) + + .filter('countBy', [ '$parse', function ( $parse ) { + return function (collection, property) { + + var result = {}, + get = $parse(property), + prop; + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(property)) { + return collection; + } + + collection.forEach( function( elm ) { + prop = get(elm); + + if(!result[prop]) { + result[prop] = 0; + } + + result[prop]++; + }); + + return result; + } + }]); + +/** + * @ngdoc filter + * @name defaults + * @kind function + * + * @description + * defaultsFilter allows to specify a default fallback value for properties that resolve to undefined. + */ + +angular.module('a8m.defaults', []) + .filter('defaults', ['$parse', function( $parse ) { + return function(collection, defaults) { + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || !isObject(defaults)) { + return collection; + } + + var keys = deepKeys(defaults); + + collection.forEach(function(elm) { + //loop through all the keys + keys.forEach(function(key) { + var getter = $parse(key); + var setter = getter.assign; + //if it's not exist + if(isUndefined(getter(elm))) { + //get from defaults, and set to the returned object + setter(elm, getter(defaults)) + } + }); + }); + + return collection; + } + }]); +/** + * @ngdoc filter + * @name every + * @kind function + * + * @description + * Checks if given expression is present in all members in the collection + * + */ + +angular.module('a8m.every', []) + .filter('every', ['$parse', function($parse) { + return function (collection, expression) { + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return true; + } + + return collection.every( function(elm) { + + return (isObject(elm) || isFunction(expression)) ? + $parse(expression)(elm) : + elm === expression; + }); + + } + }]); + +/** + * @ngdoc filter + * @name filterBy + * @kind function + * + * @description + * filter by specific properties, avoid the rest + */ +angular.module('a8m.filter-by', []) + + .filter('filterBy', ['$parse', function( $parse ) { + return function(collection, properties, search) { + + var comparator; + + search = (isString(search) || isNumber(search)) ? + String(search).toLowerCase() : undefined; + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(search)) { + return collection; + } + + return collection.filter(function(elm) { + + return properties.some(function(prop) { + + /** + * check if there is concatenate properties + * example: + * object: { first: 'foo', last:'bar' } + * filterBy: ['first + last'] => search by full name(i.e 'foo bar') + */ + if(!~prop.indexOf('+')) { + comparator = $parse(prop)(elm) + } else { + var propList = prop.replace(new RegExp('\\s', 'g'), '').split('+'); + comparator = propList.reduce(function(prev, cur, index) { + return (index === 1) ? $parse(prev)(elm) + ' ' + $parse(cur)(elm) : + prev + ' ' + $parse(cur)(elm); + }); + } + + return (isString(comparator) || isNumber(comparator)) ? + String(comparator).toLowerCase().contains(search) : + false; + }) + + }); + + } + }]); + +/** + * @ngdoc filter + * @name first + * @kind function + * + * @description + * Gets the first element or first n elements of an array + * if callback is provided, is returns as long the callback return truthy + */ +angular.module('a8m.first', []) + + .filter('first', ['$parse', function( $parse ) { + return function(collection) { + + var n, + getter, + args; + + collection = (isObject(collection)) ? toArray(collection) : + collection; + + if(!isArray(collection)) { + return collection; + } + + args = Array.prototype.slice.call(arguments, 1); + n = (isNumber(args[0])) ? args[0] : 1; + getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined; + + return (args.length) ? getFirstMatches(collection, n,(getter) ? $parse(getter) : getter) : + collection[0]; + } + }]); + +/** + * @ngdoc filter + * @name flatten + * @kind function + * + * @description + * Flattens a nested array (the nesting can be to any depth). + * If you pass shallow, the array will only be flattened a single level + */ + +angular.module('a8m.flatten', []) + .filter('flatten', function () { + return function(collection, shallow) { + + shallow = shallow || false; + collection = (isObject(collection)) ? toArray(collection) + : collection; + + if(!isArray(collection)) { + return collection; + } + + return (!shallow) ? flatten(collection, 0) : + [].concat.apply([], collection); + } + }); + +/** + * flatten nested array (the nesting can be to any depth). + * @param array {Array} + * @param i {int} + * @returns {Array} + * @private + */ +function flatten(array, i) { + i = i || 0; + + if(i >= array.length) + return array; + + if(isArray(array[i])) { + return flatten(array.slice(0,i) + .concat(array[i], array.slice(i+1)), i); + } + return flatten(array, i+1); +} + +/** + * @ngdoc filter + * @name fuzzyByKey + * @kind function + * + * @description + * fuzzy string searching by key + */ + +angular.module('a8m.fuzzy-by', []) + .filter('fuzzyBy', ['$parse', function ( $parse ) { + return function (collection, property, search, csensitive) { + + var sensitive = csensitive || false, + prop, getter; + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(property) + || isUndefined(search)) { + return collection; + } + + getter = $parse(property); + + return collection.filter(function(elm) { + + prop = getter(elm); + if(!isString(prop)) { + return false; + } + + prop = (sensitive) ? prop : prop.toLowerCase(); + search = (sensitive) ? search : search.toLowerCase(); + + return hasApproxPattern(prop, search) !== false + }) + } + + }]); +/** + * @ngdoc filter + * @name fuzzy + * @kind function + * + * @description + * fuzzy string searching for array of strings, objects + */ + +angular.module('a8m.fuzzy', []) + .filter('fuzzy', function () { + return function (collection, search, csensitive) { + + var sensitive = csensitive || false; + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(search)) { + return collection; + } + + search = (sensitive) ? search : search.toLowerCase(); + + return collection.filter(function(elm) { + + if(isString(elm)) { + elm = (sensitive) ? elm : elm.toLowerCase(); + return hasApproxPattern(elm, search) !== false + } + + return (isObject(elm)) ? _hasApproximateKey(elm, search) : false; + + }); + + /** + * checks if object has key{string} that match + * to fuzzy search pattern + * @param object + * @param search + * @returns {boolean} + * @private + */ + function _hasApproximateKey(object, search) { + var properties = Object.keys(object), + prop, flag; + return 0 < properties.filter(function (elm) { + prop = object[elm]; + + //avoid iteration if we found some key that equal[performance] + if(flag) return true; + + if (isString(prop)) { + prop = (sensitive) ? prop : prop.toLowerCase(); + return flag = (hasApproxPattern(prop, search) !== false); + } + + return false; + + }).length; + } + + } + }); + +/** + * @ngdoc filter + * @name groupBy + * @kind function + * + * @description + * Create an object composed of keys generated from the result of running each element of a collection, + * each key is an array of the elements. + */ + +angular.module('a8m.group-by', [ 'a8m.filter-watcher' ]) + + .filter('groupBy', [ '$parse', 'filterWatcher', function ( $parse, filterWatcher ) { + return function (collection, property) { + + if(!isObject(collection) || isUndefined(property)) { + return collection; + } + + var getterFn = $parse(property); + + return filterWatcher.isMemoized('groupBy', arguments) || + filterWatcher.memoize('groupBy', arguments, this, + _groupBy(collection, getterFn)); + + /** + * groupBy function + * @param collection + * @param getter + * @returns {{}} + */ + function _groupBy(collection, getter) { + var result = {}; + var prop; + + forEach( collection, function( elm ) { + prop = getter(elm); + + if(!result[prop]) { + result[prop] = []; + } + result[prop].push(elm); + }); + return result; + } + } + }]); + +/** + * @ngdoc filter + * @name isEmpty + * @kind function + * + * @description + * get collection or string and return if it empty + */ + +angular.module('a8m.is-empty', []) + .filter('isEmpty', function () { + return function(collection) { + return (isObject(collection)) ? + !toArray(collection).length : + !collection.length; + } + }); + +/** + * @ngdoc filter + * @name last + * @kind function + * + * @description + * Gets the last element or last n elements of an array + * if callback is provided, is returns as long the callback return truthy + */ +angular.module('a8m.last', []) + + .filter('last', ['$parse', function( $parse ) { + return function(collection) { + + var n, + getter, + args, + //cuz reverse change our src collection + //and we don't want side effects + reversed = copy(collection); + + reversed = (isObject(reversed)) ? toArray(reversed) : + reversed; + + if(!isArray(reversed)) { + return reversed; + } + + args = Array.prototype.slice.call(arguments, 1); + n = (isNumber(args[0])) ? args[0] : 1; + getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined; + + return (args.length) ? + //send reversed collection as arguments, and reverse it back as result + getFirstMatches(reversed.reverse(), n,(getter) ? $parse(getter) : getter).reverse() : + //get the last element + reversed[reversed.length-1]; + } + }]); + +/** + * @ngdoc filter + * @name map + * @kind function + * + * @description + * Returns a new collection of the results of each expression execution. + */ + +angular.module('a8m.map', []) + + .filter('map', ['$parse', function($parse) { + return function (collection, expression) { + + collection = (isObject(collection)) ? + toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return collection; + } + + return collection.map(function (elm) { + + return $parse(expression)(elm); + }); + } + }]); + +/** + * @ngdoc filter + * @name omit + * @kind function + * + * @description + * filter collection by expression + */ + +angular.module('a8m.omit', []) + + .filter('omit', ['$parse', function($parse) { + return function (collection, expression) { + + collection = (isObject(collection)) ? + toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return collection; + } + + return collection.filter(function (elm) { + + return !($parse(expression)(elm)); + }); + } + }]); + +/** + * @ngdoc filter + * @name omit + * @kind function + * + * @description + * filter collection by expression + */ + +angular.module('a8m.pick', []) + + .filter('pick', ['$parse', function($parse) { + return function (collection, expression) { + + collection = (isObject(collection)) ? + toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return collection; + } + + return collection.filter(function (elm) { + + return $parse(expression)(elm); + }); + } + }]); + +/** + * @ngdoc filter + * @name removeWith + * @kind function + * + * @description + * get collection and properties object, and removed elements + * with this properties + */ + +angular.module('a8m.remove-with', []) + .filter('removeWith', function() { + return function (collection, object) { + + if(isUndefined(object)) { + return collection; + } + collection = (isObject(collection)) ? + toArray(collection) : collection; + + return collection.filter(function (elm) { + return !objectContains(object, elm); + }); + } + }); + + +/** + * @ngdoc filter + * @name remove + * @kind function + * + * @description + * remove specific members from collection + */ + +angular.module('a8m.remove', []) + + .filter('remove', function () { + return function (collection) { + + collection = (isObject(collection)) ? toArray(collection) : collection; + + var args = Array.prototype.slice.call(arguments, 1); + + if(!isArray(collection)) { + return collection; + } + + return collection.filter( function( member ) { + return !args.some(function(nest) { + return equals(nest, member); + }) + }); + + } + }); + +/** + * @ngdoc filter + * @name reverse + * @kind function + * + * @description + * Reverses a string or collection + */ + +angular.module('a8m.reverse', []) + + .filter('reverse',[ function () { + return function (input) { + + input = (isObject(input)) ? toArray(input) : input; + + if(isString(input)) { + return input.split('').reverse().join(''); + } + + return (isArray(input)) ? input.slice().reverse() : input; + } + }]); + + +/** + * @ngdoc filter + * @name searchField + * @kind function + * + * @description + * for each member, join several strings field and add them to + * new field called 'searchField' (use for search filtering) + */ + +angular.module('a8m.search-field', []) + + .filter('searchField', ['$parse', function ($parse) { + return function (collection) { + + var get, field; + + collection = (isObject(collection)) ? toArray(collection) : collection; + + var args = Array.prototype.slice.call(arguments, 1); + + if(!isArray(collection) || !args.length) { + return collection; + } + + return collection.map(function(member) { + + field = args.map(function(field) { + get = $parse(field); + return get(member); + }).join(' '); + + return extend(member, { searchField: field }); + }); + } + }]); + +/** + * @ngdoc filter + * @name toArray + * @kind function + * + * @description + * Convert objects into stable arrays. + * if addKey set to true,the filter also attaches a new property + * $key to the value containing the original key that was used in + * the object we are iterating over to reference the property + */ + +angular.module('a8m.to-array', []) + + .filter('toArray', function() { + return function (collection, addKey) { + + if(!isObject(collection)) { + return collection; + } + + return (!addKey) ? toArray(collection) : + + Object.keys(collection).map(function (key) { + return extend(collection[key], { $key: key }); + }); + } + }); + +/** + * @ngdoc filter + * @name unique/uniq + * @kind function + * + * @description + * get collection and filter duplicate members + * if uniqueFilter get a property(nested to) as argument it's + * filter by this property as unique identifier + */ + +angular.module('a8m.unique', []) + .filter({ + unique: ['$parse', uniqFilter], + uniq: ['$parse', uniqFilter] + }); + +function uniqFilter($parse) { + return function (collection, property) { + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if (!isArray(collection)) { + return collection; + } + + //store all unique identifiers + var uniqueItems = [], + get = $parse(property); + + return (isUndefined(property)) ? + //if it's kind of primitive array + collection.filter(function (elm, pos, self) { + return self.indexOf(elm) === pos; + }) : + //else compare with equals + collection.filter(function (elm) { + var prop = get(elm); + if(some(uniqueItems, prop)) { + return false; + } + uniqueItems.push(prop); + return true; + }); + + //checked if the unique identifier is already exist + function some(array, member) { + if(isUndefined(member)) { + return false; + } + return array.some(function(el) { + return equals(el, member); + }); + } + + } +} + +/** + * @ngdoc filter + * @name where + * @kind function + * + * @description + * of each element in a collection to the given properties object, + * returning an array of all elements that have equivalent property values. + * + */ + +angular.module('a8m.where', []) + .filter('where', function() { + return function (collection, object) { + + if(isUndefined(object)) { + return collection; + } + collection = (isObject(collection)) ? + toArray(collection) : collection; + + return collection.filter(function (elm) { + return objectContains(object, elm); + }); + } + }); + +/** + * @ngdoc filter + * @name xor + * @kind function + * + * @description + * Exclusive or filter by expression + */ + +angular.module('a8m.xor', []) + + .filter('xor', ['$parse', function($parse) { + return function (col1, col2, expression) { + + expression = expression || false; + + col1 = (isObject(col1)) ? toArray(col1) : col1; + col2 = (isObject(col2)) ? toArray(col2) : col2; + + if(!isArray(col1) || !isArray(col2)) return col1; + + return col1.concat(col2) + .filter(function(elm) { + return !(some(elm, col1) && some(elm, col2)); + }); + + function some(el, col) { + var getter = $parse(expression); + return col.some(function(dElm) { + return expression ? + equals(getter(dElm), getter(el)) : + equals(dElm, el); + }) + } + } + }]); + +/** + * @ngdoc filter + * @name formatBytes + * @kind function + * + * @description + * Convert bytes into appropriate display + * 1024 bytes => 1 KB + */ + +angular.module('a8m.math.byteFmt', ['a8m.math']) + + .filter('byteFmt', ['$math', function ($math) { + return function (bytes, decimal) { + + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(bytes) && isFinite(bytes)) { + + if(bytes < 1024) { // within 1 KB so B + return convertToDecimal(bytes, decimal, $math) + ' B'; + } else if(bytes < 1048576) { // within 1 MB so KB + return convertToDecimal((bytes / 1024), decimal, $math) + ' KB'; + } else if(bytes < 1073741824){ // within 1 GB so MB + return convertToDecimal((bytes / 1048576), decimal, $math) + ' MB'; + } else { // GB or more + return convertToDecimal((bytes / 1073741824), decimal, $math) + ' GB'; + } + + } else { + return "NaN"; + } + } + }]); +/** + * @ngdoc filter + * @name degrees + * @kind function + * + * @description + * Convert angle from radians to degrees + * + */ + +angular.module('a8m.math.degrees', ['a8m.math']) + + .filter('degrees', ['$math', function ($math) { + return function (radians, decimal) { + // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN" + // if degrees is not a real number, we cannot do also. quit with error "NaN" + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(radians) && isFinite(radians)) { + var degrees = (radians * 180) / $math.PI; + return $math.round(degrees * $math.pow(10,decimal)) / ($math.pow(10,decimal)); + } else { + return "NaN"; + } + } +}]); + + + +/** + * @ngdoc filter + * @name formatBytes + * @kind function + * + * @description + * Convert bytes into appropriate display + * 1024 kilobytes => 1 MB + */ + +angular.module('a8m.math.kbFmt', ['a8m.math']) + + .filter('kbFmt', ['$math', function ($math) { + return function (bytes, decimal) { + + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(bytes) && isFinite(bytes)) { + + if(bytes < 1024) { // within 1 MB so KB + return convertToDecimal(bytes, decimal, $math) + ' KB'; + } else if(bytes < 1048576) { // within 1 GB so MB + return convertToDecimal((bytes / 1024), decimal, $math) + ' MB'; + } else { + return convertToDecimal((bytes / 1048576), decimal, $math) + ' GB'; + } + + } else { + return "NaN"; + } + } +}]); +/** + * @ngdoc module + * @name math + * @description + * reference to global Math object + */ + +angular.module('a8m.math', []) + .factory('$math', ['$window', function ($window) { + + return $window.Math; + + }]); + +/** + * @ngdoc filter + * @name max + * @kind function + * + * @description + * Math.max will get an array and return the max value. if an expression + * is provided, will return max value by expression. + */ + +angular.module('a8m.math.max', ['a8m.math']) + + .filter('max', ['$math', '$parse', function ($math, $parse) { + return function (input, expression) { + + if(!isArray(input)) { + return input; + } + return isUndefined(expression) + ? $math.max.apply($math, input) + : input[indexByMax(input, expression)]; + }; + + /** + * @private + * @param array + * @param exp + * @returns {number|*|Number} + */ + function indexByMax(array, exp) { + var mappedArray = array.map(function(elm){ + return $parse(exp)(elm); + }); + return mappedArray.indexOf($math.max.apply($math, mappedArray)); + } + + }]); +/** + * @ngdoc filter + * @name min + * @kind function + * + * @description + * Math.min will get an array and return the min value. if an expression + * is provided, will return min value by expression. + */ + +angular.module('a8m.math.min', ['a8m.math']) + + .filter('min', ['$math', '$parse', function ($math, $parse) { + return function (input, expression) { + + if(!isArray(input)) { + return input; + } + return isUndefined(expression) + ? $math.min.apply($math, input) + : input[indexByMin(input, expression)]; + }; + + /** + * @private + * @param array + * @param exp + * @returns {number|*|Number} + */ + function indexByMin(array, exp) { + var mappedArray = array.map(function(elm){ + return $parse(exp)(elm); + }); + return mappedArray.indexOf($math.min.apply($math, mappedArray)); + } + + }]); +/** + * @ngdoc filter + * @name Percent + * @kind function + * + * @description + * percentage between two numbers + * + */ + +angular.module('a8m.math.percent', ['a8m.math']) + + .filter('percent', ['$math', '$window', function ($math, $window) { + + return function (input, divided, round) { + + var divider = (isString(input)) ? $window.Number(input) : input; + divided = divided || 100; + round = round || false; + + if (!isNumber(divider) || $window.isNaN(divider)) return input; + + return (round) ? $math.round((divider / divided) * 100) : + ((divider / divided) * 100); + } + + }]); + +/** + * @ngdoc filter + * @name toRadians + * @kind function + * + * @description + * Convert angle from degrees to radians + * + */ + +angular.module('a8m.math.radians', ['a8m.math']) + + .filter('radians', ['$math', function ($math) { + return function (degrees, decimal) { + // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN" + // if degrees is not a real number, we cannot do also. quit with error "NaN" + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(degrees) && isFinite(degrees)) { + var radians = (degrees * 3.14159265359) / 180; + return $math.round(radians * $math.pow(10,decimal)) / ($math.pow(10,decimal)); + } else { + return "NaN"; + } + } +}]); + + + +/** + * @ngdoc filter + * @name Radix + * @kind function + * + * @description + * converting decimal numbers to different bases(radix) + */ + +angular.module('a8m.math.radix', []) + + .filter('radix', function () { + + return function (input, radix) { + + var RANGE = /^[2-9]$|^[1-2]\d$|^3[0-6]$/; + + if(!isNumber(input) || !RANGE.test(radix)) { + return input; + } + + return input.toString(radix).toUpperCase(); + + } + + }); + +/** + * @ngdoc filter + * @name formatBytes + * @kind function + * + * @description + * Convert number into abbreviations. + * i.e: K for one thousand, M for Million, B for billion + * e.g: number of users:235,221, decimal:1 => 235.2 K + */ + +angular.module('a8m.math.shortFmt', ['a8m.math']) + + .filter('shortFmt', ['$math', function ($math) { + return function (number, decimal) { + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(number) && isFinite(number)){ + + if(number < 1e3) { + return number; + } else if(number < 1e6) { + return convertToDecimal((number / 1e3), decimal, $math) + ' K'; + } else if(number < 1e9){ + return convertToDecimal((number / 1e6), decimal, $math) + ' M'; + } else { + return convertToDecimal((number / 1e9), decimal, $math) + ' B'; + } + + }else{ + return "NaN"; + } + } +}]); +/** + * @ngdoc filter + * @name sum + * @kind function + * + * @description + * Sum up all values within an array + * + */ + +angular.module('a8m.math.sum', []) + + .filter('sum', function () { + return function (input, initial) { + + return (!isArray(input)) ? input : + input.reduce(function(prev, curr) { + return prev + curr; + }, initial || 0); + + } + + }); + +/** + * @ngdoc filter + * @name endsWith + * @kind function + * + * @description + * checks whether string ends with the ends parameter. + */ + +angular.module('a8m.ends-with', []) + + .filter('endsWith', function () { + return function (input, ends, csensitive) { + + var sensitive = csensitive || false, + position; + + if(!isString(input) || isUndefined(ends)) { + return input; + } + + input = (sensitive) ? input : input.toLowerCase(); + position = input.length - ends.length; + + return input.indexOf((sensitive) ? ends : ends.toLowerCase(), position) !== -1; + } + }); + +/** + * @ngdoc filter + * @name ltrim + * @kind function + * + * @description + * Left trim. Similar to trimFilter, but only for left side. + */ + +angular.module('a8m.ltrim', []) + + .filter('ltrim', function () { + return function(input, chars) { + + var trim = chars || '\\s'; + + if(!isString(input)) { + return input; + } + + return input.replace(new RegExp('^' + trim + '+'), ''); + } + }); + +/** + * @ngdoc filter + * @name repeat + * @kind function + * + * @description + * Repeats a string n times + */ + +angular.module('a8m.repeat', []) + + .filter('repeat',[ function () { + return function (input, n, separator) { + + var times = ~~n; + + if(!isString(input)) { + return input; + } + + return (!times) ? input : strRepeat(input, --n, separator || ''); + } + }]); + +/** + * Repeats a string n times with given separator + * @param str string to repeat + * @param n number of times + * @param sep separator + * @returns {*} + */ +function strRepeat(str, n, sep) { + if(!n) { + return str; + } + return str + sep + strRepeat(str, --n, sep); +} +/** +* @ngdoc filter +* @name rtrim +* @kind function +* +* @description +* Right trim. Similar to trimFilter, but only for right side. +*/ + +angular.module('a8m.rtrim', []) + + .filter('rtrim', function () { + return function(input, chars) { + + var trim = chars || '\\s'; + + if(!isString(input)) { + return input; + } + + return input.replace(new RegExp(trim + '+$'), ''); + } + }); + +/** + * @ngdoc filter + * @name slugify + * @kind function + * + * @description + * remove spaces from string, replace with "-" or given argument + */ + +angular.module('a8m.slugify', []) + + .filter('slugify',[ function () { + return function (input, sub) { + + var replace = (isUndefined(sub)) ? '-' : sub; + + if(isString(input)) { + return input.toLowerCase() + .replace(/\s+/g, replace); + } + + return input; + } + }]); + +/** + * @ngdoc filter + * @name startWith + * @kind function + * + * @description + * checks whether string starts with the starts parameter. + */ + +angular.module('a8m.starts-with', []) + + .filter('startsWith', function () { + return function (input, start, csensitive) { + + var sensitive = csensitive || false; + + if(!isString(input) || isUndefined(start)) { + return input; + } + + input = (sensitive) ? input : input.toLowerCase(); + + return !input.indexOf((sensitive) ? start : start.toLowerCase()); + } + }); + +/** + * @ngdoc filter + * @name stringular + * @kind function + * + * @description + * get string with {n} and replace match with enumeration values + */ + +angular.module('a8m.stringular', []) + .filter('stringular', function () { + return function(input) { + + var args = Array.prototype.slice.call(arguments, 1); + + return input.replace(/{(\d+)}/g, function (match, number) { + return isUndefined(args[number]) ? match : args[number]; + }); + + } + }); + +/** + * @ngdoc filter + * @name stripTags + * @kind function + * + * @description + * strip html tags from string + */ + +angular.module('a8m.strip-tags', []) + .filter('stripTags', function () { + return function(input) { + if(isString(input)) { + return input.replace(/<\S[^><]*>/g, ''); + } + return input; + } + }); + +/** + * @ngdoc filter + * @name trim + * @kind function + * + * @description + * Strip whitespace (or other characters) from the beginning and end of a string + */ + +angular.module('a8m.trim', []) + + .filter('trim', function () { + return function(input, chars) { + + var trim = chars || '\\s'; + + if(!isString(input)) { + return input; + } + + return input.replace(new RegExp('^' + trim + '+|' + trim + '+$', 'g'), ''); + } + }); + +/** + * @ngdoc filter + * @name truncate + * @kind function + * + * @description + * truncates a string given a specified length, providing a custom string to denote an omission. + */ + +angular.module('a8m.truncate', []) + .filter('truncate', function () { + return function(input, length, suffix, preserve) { + + length = isUndefined(length) ? input.length : length; + preserve = preserve || false; + suffix = suffix || ''; + + if(!isString(input) || (input.length <= length)) return input; + + return input.substring(0, (preserve) ? + ((input.indexOf(' ', length) === -1) ? input.length : input.indexOf(' ', length)) : + length) + suffix; + + }; + }); + +/** + * @ngdoc filter + * @name ucfirst + * @kind function + * + * @description + * ucfirst + * + */ + +angular.module('a8m.ucfirst', []) + + .filter('ucfirst', [function() { + return function(input) { + return angular.isString(input) ? input.split(' ') + .map(function (char) { + return char.charAt(0).toUpperCase() + char.substring(1); + }).join(' ') : input; + } + + }]); + +/** + * @ngdoc filter + * @name uriComponentEncode + * @kind function + * + * @description + * get string as parameter and return encoded string + */ + +angular.module('a8m.uri-component-encode', []) + + .filter('uriComponentEncode',['$window', function ($window) { + return function (input) { + + if(isString(input)) { + return $window.encodeURIComponent(input); + } + + return input; + } + }]); + +/** + * @ngdoc filter + * @name uriEncode + * @kind function + * + * @description + * get string as parameter and return encoded string + */ + +angular.module('a8m.uri-encode', []) + + .filter('uriEncode',['$window', function ($window) { + return function (input) { + + if(isString(input)) { + return $window.encodeURI(input); + } + + return input; + } + }]); + +/** + * @ngdoc filter + * @name wrap + * @kind function + * + * @description + * Wrap a string with another string + */ + +angular.module('a8m.wrap', []) + + .filter('wrap', function () { + return function(input, wrap, ends) { + + if(!isString(input) || isUndefined(wrap)) { + return input; + } + + return [wrap, input, ends || wrap].join(''); + + } + }); + +/** + * @ngdoc provider + * @name filterWatcher + * @kind function + * + * @description + * store specific filters result in $$cache, based on scope life time(avoid memory leak). + * on scope.$destroy remove it's cache from $$cache container + */ + +angular.module('a8m.filter-watcher', []) + .provider('filterWatcher', function() { + + this.$get = ['$window', '$rootScope', function($window, $rootScope) { + + /** + * Cache storing + * @type {Object} + */ + var $$cache = {}; + + /** + * Scope listeners container + * scope.$destroy => remove all cache keys + * bind to current scope. + * @type {Object} + */ + var $$listeners = {}; + + /** + * $timeout without triggering the digest cycle + * @type {function} + */ + var $$timeout = $window.setTimeout; + + /** + * @description + * get `HashKey` string based on the given arguments. + * @param fName + * @param args + * @returns {string} + */ + function getHashKey(fName, args) { + return [fName, JSON.stringify(args)] + .join('#') + .replace(/"/g,''); + } + + /** + * @description + * fir on $scope.$destroy, + * remove cache based scope from `$$cache`, + * and remove itself from `$$listeners` + * @param event + */ + function removeCache(event) { + var id = event.targetScope.$id; + forEach($$listeners[id], function(key) { + delete $$cache[key]; + }); + delete $$listeners[id]; + } + + /** + * @description + * for angular version that greater than v.1.3.0 + * if clear cache when the digest cycle end. + */ + function cleanStateless() { + $$timeout(function() { + if(!$rootScope.$$phase) + $$cache = {}; + }); + } + + /** + * @description + * Store hashKeys in $$listeners container + * on scope.$destroy, remove them all(bind an event). + * @param scope + * @param hashKey + * @returns {*} + */ + function addListener(scope, hashKey) { + var id = scope.$id; + if(isUndefined($$listeners[id])) { + scope.$on('$destroy', removeCache); + $$listeners[id] = []; + } + return $$listeners[id].push(hashKey); + } + + /** + * @description + * return the `cacheKey` or undefined. + * @param filterName + * @param args + * @returns {*} + */ + function $$isMemoized(filterName, args) { + var hashKey = getHashKey(filterName, args); + return $$cache[hashKey]; + } + + /** + * @description + * store `result` in `$$cache` container, based on the hashKey. + * add $destroy listener and return result + * @param filterName + * @param args + * @param scope + * @param result + * @returns {*} + */ + function $$memoize(filterName, args, scope, result) { + var hashKey = getHashKey(filterName, args); + //store result in `$$cache` container + $$cache[hashKey] = result; + // for angular versions that less than 1.3 + // add to `$destroy` listener, a cleaner callback + if(isScope(scope)) { + addListener(scope, hashKey); + } else { + cleanStateless(); + } + return result; + } + + return { + isMemoized: $$isMemoized, + memoize: $$memoize + } + + }]; + }); + + + + + + + + + + + + + + + + + + + + +/** + * @ngdoc module + * @name angular.filters + * @description + * Bunch of useful filters for angularJS + */ + +angular.module('angular.filter', [ + + 'a8m.ucfirst', + 'a8m.uri-encode', + 'a8m.uri-component-encode', + 'a8m.slugify', + 'a8m.strip-tags', + 'a8m.stringular', + 'a8m.truncate', + 'a8m.starts-with', + 'a8m.ends-with', + 'a8m.wrap', + 'a8m.trim', + 'a8m.ltrim', + 'a8m.rtrim', + 'a8m.repeat', + + 'a8m.to-array', + 'a8m.concat', + 'a8m.contains', + 'a8m.unique', + 'a8m.is-empty', + 'a8m.after', + 'a8m.after-where', + 'a8m.before', + 'a8m.before-where', + 'a8m.defaults', + 'a8m.where', + 'a8m.reverse', + 'a8m.remove', + 'a8m.remove-with', + 'a8m.group-by', + 'a8m.count-by', + 'a8m.search-field', + 'a8m.fuzzy-by', + 'a8m.fuzzy', + 'a8m.omit', + 'a8m.pick', + 'a8m.every', + 'a8m.filter-by', + 'a8m.xor', + 'a8m.map', + 'a8m.first', + 'a8m.last', + 'a8m.flatten', + + 'a8m.math', + 'a8m.math.max', + 'a8m.math.min', + 'a8m.math.percent', + 'a8m.math.radix', + 'a8m.math.sum', + 'a8m.math.degrees', + 'a8m.math.radians', + 'a8m.math.byteFmt', + 'a8m.math.kbFmt', + 'a8m.math.shortFmt', + + 'a8m.angular', + 'a8m.conditions', + 'a8m.is-null', + + 'a8m.filter-watcher' +]); +})( window, window.angular ); \ No newline at end of file