Last active
August 29, 2015 13:56
-
-
Save togakangaroo/8939329 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define( | |
['_underscore', 'jquery'], | |
function underscoreExtensions(_, $){ | |
//Object and collection helpers | |
_.mixin({ | |
// Create object from a callback that returns [name, value] pairs. We use this combination a lot | |
// so might as well bake it into the framework | |
mapObject: _.compose(_.object, _.map) | |
// Create a new object by removing keys from object1 which also exist in object2 | |
,differenceObject: function(obj1, obj2) { | |
return _.pick(obj1, _.difference(_.keys(obj1), _.keys(obj2)) ); | |
} | |
// Shortcut similar to _.find(collection, function(){ return x.SlideId == val}) | |
// or _.where(collection, {SlideId: val})[0] | |
// Usage: | |
// var slide3 = _.findBy(entities, 3, 'SlideId'); | |
,findBy: function (collection, val, prop) { | |
return _.find(collection, function findBy(x) { return x&&(x[prop] === val) }); | |
} | |
// Similar to find but returns the index of the matching item | |
,findIndex: function(collection, iterator) { | |
for(var i=0,len=collection.length;i<len; i++) | |
if(iterator(collection[i], i, collection)) | |
return i; | |
return -1; | |
} | |
// Pluck values out of an array of objects. | |
// Usage: | |
// arr = [ {a:'a1', b: 'b1', c: 'c1'} | |
// ,{a:'a2', b: 'b2', c: 'c2'} ]; | |
// _.pluckValues(arr, 'b', 'c') | |
// or _.pluckValues(arr, ['b', 'c']); => | |
// [ {b: 'b1', c: 'c1'} | |
// ,{b: 'b2', c: 'c2'} ]; | |
,pluckValues: function(obj, properties) { | |
properties = _.flatten(_.tail(arguments)); | |
if(_.isArray(obj)) | |
return _.map(obj, function(val){ return _.pick(val, properties)}); | |
return _.mapObject(obj, function(val, key){ | |
return [key, _.pick(val, properties)]; | |
}); | |
} | |
// Take a collection and return an object with a key for each property of each object | |
// note that this is not optimized for large collections or large objects. | |
// Usage: | |
// arr = [ {a:'a1', b: 'b1'} | |
// ,{a:'a2', c: 'c2'} ]; | |
// or _.pivotAllProperties(arr); => | |
// { a: ['a1', 'a2'], b: ['b1', undefined], c: [undefined, 'c2'] }; | |
,pivotAllProperties: function(collection) { | |
var allKeys = _.uniq(_.flatten(_.map(collection, _.keys))) | |
; | |
return _.reduce(allKeys, function(obj, k) { | |
obj[k] = _.pluck(collection, k); | |
return obj; | |
}, {}); | |
} | |
// Take all array properties of an object and turn them to an array of objects with properties corresponding to | |
// that data. | |
,unPivot: function(obj) { | |
var arrayProps = _.chain(obj).map(function(v,k){ return _.isArray(v) && k} ).reject(_.isUndefined).value() | |
,maxLength = Math.max.apply(Math, _.pluck(_.pick(obj, arrayProps), 'length') ) //of those properties that are arrays | |
; | |
return _.map(_.range(maxLength), function(i){ | |
return _.reduce(arrayProps, function(memo, p){ | |
memo[p] = obj[p][i]; | |
return memo; | |
}, {}); | |
}) | |
} | |
// return the array itself but with any functions replaced by the results of invoking them | |
,resultAll: function(collection) { | |
var args = _.tail(arguments) | |
return _.map(collection, function(v) { | |
return _.isFunction(v) ? v.apply(undefined, _.tail(args)) : v | |
}) | |
} | |
//tap each item in the collection returning the collection itself | |
,tapEach: function(collection, fn, bindTo) { | |
_.each(collection, fn, bindTo); | |
return collection; | |
} | |
//Throw an error when the given keys do not belong to the object. | |
//Useful for explicitly stating and checking non-optional keys in options objects | |
//Usage: | |
// _.ensureHasKeys(this.options, 'mode', 'language'); | |
,ensureHasKeys: function(obj) { | |
if(!obj) { | |
console.error("Expected object but recieved", obj); | |
throw new Error("Expected object but no object was recieved"); | |
} | |
var keys = _.tail(arguments); | |
_.each(keys, function(key) { | |
if (!_.has(obj, key)) { | |
console.error("Expected object ", obj, "to contain", key); | |
throw new Error("Expected object to contain " + key); | |
} | |
}); | |
} | |
//More meaningful than $.extend(true, {}, obj) - but that's what it is | |
,deepClone: function(obj) { | |
return $.extend(true, {}, obj); | |
} | |
//union while flattening through any nested arrays | |
,unionAll: _.compose(_.flatten, _.union) | |
//A very rough implementation of a random on a gaussian distribution. | |
//Inspired by http://www.protonfish.com/random.shtml | |
,pseudoGaussianRandom: function(mean, stdev) { | |
var threeStdDevs = (Math.random()*2-1)+(Math.random()*2-1)+(Math.random()*2-1); | |
return threeStdDevs*stdev+mean; | |
} | |
}); | |
// Functional helpers | |
_.mixin({ | |
//A simplistic caching function. | |
//Execute the given function with a given set of parameters no more frequently than | |
//every [duration] milliseconds. Any invocations within that window will simply return | |
//the last stored value (for the given parameter set). Useful for functions that | |
//should be memoized for a period of time such as repeated ajax lookups of data that | |
//changes infrequently. Takes an optional third parameter [hasher] function to determine | |
//a unique hash for a given set of parameters. If none is provided, default equality for | |
//the first parameter is used. | |
memoizeForDuration: function(func, duration, hasher) { | |
var memo = {}; | |
hasher || (hasher = _.identity); | |
return function memoizedFn() { | |
var key = hasher.apply(this, arguments); | |
if(_.has(memo, key)) | |
return memo[key]; | |
_.delay(function() { delete memo[key] }, duration); | |
return memo[key] = func.apply(this, arguments); | |
} | |
} | |
//Shift all arguments one position to the left, dropping the leftmost. This is useful for situations where | |
//you do not care about the first argument (for example a jquery callback's event object) | |
,leftShiftArgs: function(fn) { | |
return function() { | |
fn.apply( this, _.toArray(arguments).slice(1) ); | |
}; | |
} | |
//Start polling and execute a callback. Returns a promise that becomes resolved when | |
//polling concludes | |
,poll: function(op) { | |
_.defaults(op, { | |
until: function(){return true}, every: 200, timeout: 1500, success: $.noop, onTimeout: $.noop | |
}); | |
var deferred = $.Deferred(); | |
deferred.done(op.success).fail(op.onTimeout); | |
var startTime = new Date().getTime(); | |
check(); | |
return deferred.promise(); | |
function check() { | |
if(op.until()) | |
return deferred.resolve(); | |
if(isTimedOut()) | |
return deferred.reject("Timeout"); | |
_.delay(check, op.every); | |
} | |
function isTimedOut() { | |
return new Date().getTime() - startTime; | |
} | |
} | |
//Automatically call bindAll on all functions in the object. To lock down 'this' to the | |
//object itself | |
,bindAllFunctions: function(obj) { | |
return _.bindAll.apply(_, _.union([obj], _.functions(obj)) ); | |
} | |
//Takes a filterFunction and an execution function. Returns a new method that when | |
//run will only trigger the exection function if the filter function returns truthy. | |
,runWhen: function(fnFilter, fn) { | |
return function runWhen(){ | |
if(fnFilter.apply(this, arguments)) | |
return fn.apply(this, arguments); | |
} | |
} | |
//Simply run the given function. Useful as a default for determinging whether somethign should be defered, debounced, etc | |
,run: function(fn) { return fn.call(this) } | |
//Execute a metnod now and return it. Useful when you want to subscribe to an event with a callback AND execute it immediately eg | |
// paramContext.events.selectionsChanged.add( _.immediate(recalculateAvalableSlideSets) ) | |
,immediate: function(fn) { | |
fn.apply(null, _.tail(arguments)); | |
return fn; | |
} | |
}); | |
//GM - allow interpolation of text like this "hello {{name}}" | |
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g; | |
//Hierarchy helpers | |
_.mixin({ | |
//Visitor pattern for navigating tree-like structures assumes a default children property | |
//named 'children', otherwise, a string or function can be provided | |
visit: function(root, callback, childProperty) { | |
if(null == root) return; | |
var getChildren = getChildrenFn(childProperty); | |
callback(root) | |
_.each(getChildren(root), function(child){ | |
_.visit(child, callback, getChildren); | |
}) | |
} | |
//Visit all nodes of an object graph and for each property invoke a callback. Then descend into any object properties | |
//and repeat. Will not visit the same node twice. | |
// _.visitObject(ancestryTree, function(val, propertyName, parent) { ... }) | |
,visitObject: function(root, callback, name, visited) { | |
visited || (visited = []) | |
if(_.contains(visited, root)) | |
return; | |
_.each(root, function(val, propName) { callback(val, propName, root) }) | |
visited.push(root); | |
_.chain(root).filter(isNavigable).each(function(val, propName) { _.visitObject(val, callback, propName, visited) }); | |
} | |
//Visits all nodes in a tree-line structure and returns all nodes that match a filter test function | |
,collectTree: function(root, test, childProperty) { | |
var result = []; | |
function testNode(node) { | |
test(node) && result.push(node); | |
} | |
_.visit(root, testNode, childProperty); | |
return result; | |
} | |
//Returns all nodes from a tree-like structure | |
,collectTreeNodes: function(root, childProperty) { | |
return _.collectTree(root, function(){ return true }, childProperty) | |
} | |
}); | |
return _; | |
function getChildrenFn(prop) { | |
return _.isFunction(prop) ? prop : | |
(function getChildrenFn(x){ return x[prop|| 'children']}); | |
} | |
function isNavigable(root) { | |
return !( _.isNull(root) || _.isUndefined(root) || _.isFunction(root) || _.isNumber(root) || _.isString(root) ); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment