From 2b465bdf820d147806f873d07c944bd27316c08b Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 1 May 2015 21:00:50 -0700 Subject: [PATCH 01/72] Update QUnit dev dep. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e30bd2f42..2610da53c9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "jquery": "~1.11.0", "platform": "~1.3.0", "qunit-extras": "~1.4.0", - "qunitjs": "~1.17.0", + "qunitjs": "~1.18.0", "requirejs": "~2.1.0" }, "volo": { From 7b80a780c712afc7d8fc4397f2b04b8556c0ffdb Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 1 May 2015 22:16:59 -0700 Subject: [PATCH 02/72] Remove old Opera from Sauce tests. --- test/saucelabs.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/saucelabs.js b/test/saucelabs.js index 84a91e12e4..3811cd9969 100644 --- a/test/saucelabs.js +++ b/test/saucelabs.js @@ -116,8 +116,6 @@ var platforms = [ ['Windows 7', 'internet explorer', '8'], ['Windows XP', 'internet explorer', '7'], ['Windows XP', 'internet explorer', '6'], - ['Windows 7', 'opera', '12'], - ['Windows 7', 'opera', '11'], ['OS X 10.9', 'ipad', '8.1'], ['OS X 10.6', 'ipad', '4'], ['OS X 10.10', 'safari', '8'], From 13fe88c03f8153732d2385133e9bd1ed2112d999 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 2 May 2015 23:00:57 -0700 Subject: [PATCH 03/72] Cleanup `baseIsMatch` and `equalArrays`. --- lodash.src.js | 78 ++++++++++++++++++++++----------------------------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index ee0fa43af2..d4f74bd794 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -2438,11 +2438,11 @@ * @returns {boolean} Returns `true` if `object` is a match, else `false`. */ function baseIsMatch(object, props, values, strictCompareFlags, customizer) { - var index = -1, - length = props.length, + var length = props.length, + index = length, noCustomizer = !customizer; - while (++index < length) { + while (index--) { if ((noCustomizer && strictCompareFlags[index]) ? values[index] !== object[props[index]] : !(props[index] in object) @@ -2450,23 +2450,21 @@ return false; } } - index = -1; while (++index < length) { var key = props[index], objValue = object[key], srcValue = values[index]; if (noCustomizer && strictCompareFlags[index]) { - var result = objValue !== undefined || (key in object); + if (objValue === undefined && !(key in object)) { + return false; + } } else { - result = customizer ? customizer(objValue, srcValue, key) : undefined; - if (result === undefined) { - result = baseIsEqual(srcValue, objValue, customizer, true); + var result = customizer ? customizer(objValue, srcValue, key) : undefined; + if (!(result === undefined ? baseIsEqual(srcValue, objValue, customizer, true) : result)) { + return false; } } - if (!result) { - return false; - } } return true; } @@ -2562,7 +2560,7 @@ } return object[key] === value ? (value !== undefined || (key in object)) - : baseIsEqual(value, object[key], null, true); + : baseIsEqual(value, object[key], undefined, true); }; } @@ -3263,11 +3261,11 @@ customizer = bindCallback(customizer, thisArg, 5); length -= 2; } else { - customizer = typeof thisArg == 'function' ? thisArg : null; + customizer = typeof thisArg == 'function' ? thisArg : undefined; length -= (customizer ? 1 : 0); } if (guard && isIterateeCall(sources[0], sources[1], guard)) { - customizer = length < 3 ? null : customizer; + customizer = length < 3 ? undefined : customizer; length = 1; } while (++index < length) { @@ -3919,40 +3917,32 @@ function equalArrays(array, other, equalFunc, customizer, isLoose, stackA, stackB) { var index = -1, arrLength = array.length, - othLength = other.length, - result = true; + othLength = other.length; if (arrLength != othLength && !(isLoose && othLength > arrLength)) { return false; } - // Deep compare the contents, ignoring non-numeric properties. - while (result && ++index < arrLength) { + // Ignore non-index properties. + while (++index < arrLength) { var arrValue = array[index], - othValue = other[index]; - - result = undefined; - if (customizer) { - result = isLoose - ? customizer(othValue, arrValue, index) - : customizer(arrValue, othValue, index); - } - if (result === undefined) { - // Recursively compare arrays (susceptible to call stack limits). - if (isLoose) { - var othIndex = othLength; - while (othIndex--) { - othValue = other[othIndex]; - result = (arrValue && arrValue === othValue) || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB); - if (result) { - break; - } - } - } else { - result = (arrValue && arrValue === othValue) || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB); + othValue = other[index], + result = customizer ? customizer(isLoose ? othValue : arrValue, isLoose ? arrValue : othValue, index) : undefined; + + if (result !== undefined && !result) { + return false; + } + // Recursively compare arrays (susceptible to call stack limits). + if (isLoose) { + if (!arraySome(other, function(othValue) { + return arrValue === othValue || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB); + })) { + return false; } + } else if (!(arrValue === othValue || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB))) { + return false; } } - return !!result; + return true; } /** @@ -8567,7 +8557,7 @@ customizer = isDeep; isDeep = false; } - customizer = typeof customizer == 'function' && bindCallback(customizer, thisArg, 1); + customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 1) : undefined; return baseClone(value, isDeep, customizer); } @@ -8617,7 +8607,7 @@ * // => 20 */ function cloneDeep(value, customizer, thisArg) { - customizer = typeof customizer == 'function' && bindCallback(customizer, thisArg, 1); + customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 1) : undefined; return baseClone(value, true, customizer); } @@ -8817,7 +8807,7 @@ * // => true */ function isEqual(value, other, customizer, thisArg) { - customizer = typeof customizer == 'function' && bindCallback(customizer, thisArg, 3); + customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 3) : undefined; if (!customizer && isStrictComparable(value) && isStrictComparable(other)) { return value === other; } @@ -8976,7 +8966,7 @@ if (object == null) { return false; } - customizer = typeof customizer == 'function' && bindCallback(customizer, thisArg, 3); + customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 3) : undefined; object = toObject(object); if (!customizer && length == 1) { var key = props[0], From 7e4ed7c1a9129a1325ab243da5e42066b2f9a10c Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 3 May 2015 13:47:40 -0700 Subject: [PATCH 04/72] Ensure `baseCreate` works in ExtendScript. --- lodash.src.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index d4f74bd794..2b078feec3 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -1975,14 +1975,14 @@ * @returns {Object} Returns the new object. */ var baseCreate = (function() { - function Object() {} + function object() {} return function(prototype) { if (isObject(prototype)) { - Object.prototype = prototype; - var result = new Object; - Object.prototype = null; + object.prototype = prototype; + var result = new object; + object.prototype = null; } - return result || context.Object(); + return result || {}; }; }()); From 14651d8ea813091edc71bec32d264392a091d295 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 3 May 2015 17:20:51 -0700 Subject: [PATCH 05/72] Remove `customizer` assignment from `clone` and `cloneDeep`. --- lodash.src.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 2b078feec3..52be9e686b 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -8557,8 +8557,9 @@ customizer = isDeep; isDeep = false; } - customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 1) : undefined; - return baseClone(value, isDeep, customizer); + return typeof customizer == 'function' + ? baseClone(value, isDeep, bindCallback(customizer, thisArg, 1)) + : baseClone(value, isDeep); } /** @@ -8607,8 +8608,9 @@ * // => 20 */ function cloneDeep(value, customizer, thisArg) { - customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 1) : undefined; - return baseClone(value, true, customizer); + return typeof customizer == 'function' + ? baseClone(value, true, bindCallback(customizer, thisArg, 1)) + : baseClone(value, true); } /** From ee182df533f30a282111bae1e5dfa7236c3dee7e Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 3 May 2015 19:38:58 -0700 Subject: [PATCH 06/72] Optimize object comparisons in `_.isEqual`. --- lodash.src.js | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 52be9e686b..ed496ecb6a 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -4007,29 +4007,22 @@ if (objLength != othLength && !isLoose) { return false; } - var skipCtor = isLoose, - index = -1; - + var index = objLength; + while (index--) { + var key = objProps[index]; + if (!(isLoose ? key in other : hasOwnProperty.call(other, key))) { + return false; + } + } + var skipCtor = isLoose; while (++index < objLength) { - var key = objProps[index], - result = isLoose ? key in other : hasOwnProperty.call(other, key); + key = objProps[index]; + var objValue = object[key], + othValue = other[key], + result = customizer ? customizer(isLoose ? othValue : objValue, isLoose? objValue : othValue, key) : undefined; - if (result) { - var objValue = object[key], - othValue = other[key]; - - result = undefined; - if (customizer) { - result = isLoose - ? customizer(othValue, objValue, key) - : customizer(objValue, othValue, key); - } - if (result === undefined) { - // Recursively compare objects (susceptible to call stack limits). - result = (objValue && objValue === othValue) || equalFunc(objValue, othValue, customizer, isLoose, stackA, stackB); - } - } - if (!result) { + // Recursively compare objects (susceptible to call stack limits). + if (!(result === undefined ? equalFunc(objValue, othValue, customizer, isLoose, stackA, stackB) : result)) { return false; } skipCtor || (skipCtor = key == 'constructor'); From d82593741118a0b61021514cc803a70c94400107 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 3 May 2015 22:57:45 -0700 Subject: [PATCH 07/72] Ensure `customizer` results are respected by `_.isEqual`. --- lodash.src.js | 5 ++++- test/test.js | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index ed496ecb6a..b42720f849 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3928,7 +3928,10 @@ othValue = other[index], result = customizer ? customizer(isLoose ? othValue : arrValue, isLoose ? arrValue : othValue, index) : undefined; - if (result !== undefined && !result) { + if (result !== undefined) { + if (result) { + continue; + } return false; } // Recursively compare arrays (susceptible to call stack limits). diff --git a/test/test.js b/test/test.js index d96262d784..24804e7878 100644 --- a/test/test.js +++ b/test/test.js @@ -7749,15 +7749,26 @@ strictEqual(_.isEqual('a', 'a', _.noop), true); }); + test('should not handle comparisons if `customizer` returns `true`', 3, function() { + var customizer = function(value) { + return _.isString(value) || undefined; + }; + + strictEqual(_.isEqual('a', 'b', customizer), true); + strictEqual(_.isEqual(['a'], ['b'], customizer), true); + strictEqual(_.isEqual({ '0': 'a' }, { '0': 'b' }, customizer), true); + }); + test('should return a boolean value even if `customizer` does not', 2, function() { - var actual = _.isEqual('a', 'a', _.constant('a')); + var actual = _.isEqual('a', 'b', _.constant('c')); strictEqual(actual, true); - var expected = _.map(falsey, _.constant(false)); + var values = _.without(falsey, undefined), + expected = _.map(values, _.constant(false)); actual = []; - _.each(falsey, function(value) { - actual.push(_.isEqual('a', 'b', _.constant(value))); + _.each(values, function(value) { + actual.push(_.isEqual('a', 'a', _.constant(value))); }); deepEqual(actual, expected); From 95b1455b625b7c71c2375afc4ce32ebabfebe76b Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 4 May 2015 09:06:06 -0700 Subject: [PATCH 08/72] Consistently use `callback` as the variable to store `getCallback()` results. --- lodash.src.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index b42720f849..1a1ba0716d 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3432,12 +3432,12 @@ if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { iteratee = null; } - var func = getCallback(), + var callback = getCallback(), noIteratee = iteratee == null; - if (!(func === baseCallback && noIteratee)) { + if (!(noIteratee && callback === baseCallback)) { noIteratee = false; - iteratee = func(iteratee, thisArg, 3); + iteratee = callback(iteratee, thisArg, 3); } if (noIteratee) { var isArr = isArray(collection); @@ -3828,10 +3828,10 @@ */ function createSortedIndex(retHighest) { return function(array, value, iteratee, thisArg) { - var func = getCallback(iteratee); - return (func === baseCallback && iteratee == null) + var callback = getCallback(iteratee); + return (iteratee == null && callback === baseCallback) ? binaryIndex(array, value, retHighest) - : binaryIndexBy(array, value, func(iteratee, thisArg, 1), retHighest); + : binaryIndexBy(array, value, callback(iteratee, thisArg, 1), retHighest); }; } @@ -5890,9 +5890,9 @@ iteratee = isIterateeCall(array, isSorted, thisArg) ? null : isSorted; isSorted = false; } - var func = getCallback(); - if (!(func === baseCallback && iteratee == null)) { - iteratee = func(iteratee, thisArg, 3); + var callback = getCallback(); + if (!(iteratee == null && callback === baseCallback)) { + iteratee = callback(iteratee, thisArg, 3); } return (isSorted && getIndexOf() == baseIndexOf) ? sortedUniq(array, iteratee) @@ -11909,12 +11909,12 @@ if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { iteratee = null; } - var func = getCallback(), + var callback = getCallback(), noIteratee = iteratee == null; - if (!(func === baseCallback && noIteratee)) { + if (!(noIteratee && callback === baseCallback)) { noIteratee = false; - iteratee = func(iteratee, thisArg, 3); + iteratee = callback(iteratee, thisArg, 3); } return noIteratee ? arraySum(isArray(collection) ? collection : toIterable(collection)) From 1afcfa440666bf24277f431523b5b6c2daeeafad Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 4 May 2015 21:53:12 -0700 Subject: [PATCH 09/72] Use precomputed values for `MAX_ARRAY_LENGTH` and `MAX_SAFE_INTEGER`. --- lodash.src.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 1a1ba0716d..8e235d437e 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -835,7 +835,7 @@ POSITIVE_INFINITY = Number.POSITIVE_INFINITY; /** Used as references for the maximum length and index of an array. */ - var MAX_ARRAY_LENGTH = Math.pow(2, 32) - 1, + var MAX_ARRAY_LENGTH = 4294967295, MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; @@ -846,7 +846,7 @@ * Used as the [maximum length](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.max_safe_integer) * of an array-like value. */ - var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; + var MAX_SAFE_INTEGER = 9007199254740991; /** Used to store function metadata. */ var metaMap = WeakMap && new WeakMap; From ca5fa9e84dd5553435993a9e0e565824850b81cd Mon Sep 17 00:00:00 2001 From: jdalton Date: Tue, 5 May 2015 21:31:36 -0700 Subject: [PATCH 10/72] Add support for an immutable Map to `_.memoize.Cache`. --- lodash.src.js | 6 ++-- test/test.js | 84 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 8e235d437e..9370dd5d42 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -8154,14 +8154,14 @@ } var memoized = function() { var args = arguments, - cache = memoized.cache, - key = resolver ? resolver.apply(this, args) : args[0]; + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; if (cache.has(key)) { return cache.get(key); } var result = func.apply(this, args); - cache.set(key, result); + memoized.cache = cache.set(key, result); return result; }; memoized.cache = new memoize.Cache; diff --git a/test/test.js b/test/test.js index 24804e7878..47a58d2edf 100644 --- a/test/test.js +++ b/test/test.js @@ -10295,11 +10295,10 @@ }); test('should check cache for own properties', 1, function() { - var actual = [], - memoized = _.memoize(_.identity); + var memoized = _.memoize(_.identity); - _.each(shadowProps, function(value) { - actual.push(memoized(value)); + var actual = _.map(shadowProps, function(value) { + return memoized(value); }); deepEqual(actual, shadowProps); @@ -10307,9 +10306,13 @@ test('should expose a `cache` object on the `memoized` function which implements `Map` interface', 18, function() { _.times(2, function(index) { - var resolver = index ? _.identity : null, - memoized = _.memoize(function(value) { return 'value:' + value; }, resolver), - cache = memoized.cache; + var resolver = index ? _.identity : null; + + var memoized = _.memoize(function(value) { + return 'value:' + value; + }, resolver); + + var cache = memoized.cache; memoized('a'); @@ -10349,7 +10352,7 @@ }); }); - test('should allow `_.memoize.Cache` to be customized', 4, function() { + test('should allow `_.memoize.Cache` to be customized', 5, function() { var oldCache = _.memoize.Cache function Cache() { @@ -10382,27 +10385,72 @@ }, 'set': function(key, value) { this.__data__.push({ 'key': key, 'value': value }); + return this; } }; _.memoize.Cache = Cache; var memoized = _.memoize(function(object) { - return '`id` is "' + object.id + '"'; + return 'value:' + object.id; }); - var actual = memoized({ 'id': 'a' }); - strictEqual(actual, '`id` is "a"'); + var cache = memoized.cache, + key1 = { 'id': 'a' }, + key2 = { 'id': 'b' }; - var key = { 'id': 'b' }; - actual = memoized(key); - strictEqual(actual, '`id` is "b"'); + strictEqual(memoized(key1), 'value:a'); + strictEqual(cache.has(key1), true); - var cache = memoized.cache; - strictEqual(cache.has(key), true); + strictEqual(memoized(key2), 'value:b'); + strictEqual(cache.has(key2), true); + + cache['delete'](key2); + strictEqual(cache.has(key2), false); + + _.memoize.Cache = oldCache; + }); + + test('should works with an immutable `_.memoize.Cache` ', 2, function() { + var oldCache = _.memoize.Cache + + function Cache() { + this.__data__ = []; + } + + Cache.prototype = { + 'get': function(key) { + return _.find(this.__data__, function(entry) { + return key === entry.key; + }).value; + }, + 'has': function(key) { + return _.some(this.__data__, function(entry) { + return key === entry.key; + }); + }, + 'set': function(key, value) { + var result = new Cache; + result.__data__ = this.__data__.concat({ 'key': key, 'value': value }); + return result; + } + }; + + _.memoize.Cache = Cache; + + var memoized = _.memoize(function(object) { + return object.id; + }); + + var key1 = { 'id': 'a' }, + key2 = { 'id': 'b' }; - cache['delete'](key); - strictEqual(cache.has(key), false); + memoized(key1); + memoized(key2); + + var cache = memoized.cache; + strictEqual(cache.has(key1), true); + strictEqual(cache.has(key2), true); _.memoize.Cache = oldCache; }); From 7dfd7ad5b95442e34de18e2ba0a0845d355c9cac Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 6 May 2015 01:37:56 -0700 Subject: [PATCH 11/72] Minor adjustments to param docs for `baseCompareAscending` and `matchesProperty`. [ci skip] --- lodash.src.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 9370dd5d42..12aff8aefb 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -292,8 +292,8 @@ * sorts them in ascending order without guaranteeing a stable sort. * * @private - * @param {*} value The value to compare to `other`. - * @param {*} other The value to compare to `value`. + * @param {*} value The value to compare. + * @param {*} other The other value to compare. * @returns {number} Returns the sort order indicator for `value`. */ function baseCompareAscending(value, other) { @@ -11378,7 +11378,7 @@ * @memberOf _ * @category Utility * @param {Array|string} path The path of the property to get. - * @param {*} value The value to compare. + * @param {*} value The value to match. * @returns {Function} Returns the new function. * @example * From db67ae12eca7c784533fac358a32de68bc205382 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 6 May 2015 01:39:02 -0700 Subject: [PATCH 12/72] Optimize `_.max` and `_.min` when invoked with iteratees and add `_.gt`, `_.gte`, `_.lt`, `_.lte`, & `_.eq`. --- lodash.src.js | 258 ++++++++++++++++++++++++++++++-------------------- test/test.js | 5 +- 2 files changed, 156 insertions(+), 107 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 12aff8aefb..a781300878 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -923,30 +923,31 @@ * `filter`, `flatten`, `flattenDeep`, `flow`, `flowRight`, `forEach`, * `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `functions`, * `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, `invoke`, `keys`, - * `keysIn`, `map`, `mapValues`, `matches`, `matchesProperty`, `memoize`, - * `merge`, `mixin`, `negate`, `omit`, `once`, `pairs`, `partial`, `partialRight`, - * `partition`, `pick`, `plant`, `pluck`, `property`, `propertyOf`, `pull`, - * `pullAt`, `push`, `range`, `rearg`, `reject`, `remove`, `rest`, `reverse`, - * `shuffle`, `slice`, `sort`, `sortBy`, `sortByAll`, `sortByOrder`, `splice`, - * `spread`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, - * `throttle`, `thru`, `times`, `toArray`, `toPlainObject`, `transform`, - * `union`, `uniq`, `unshift`, `unzip`, `values`, `valuesIn`, `where`, - * `without`, `wrap`, `xor`, `zip`, and `zipObject` + * `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`, + * `memoize`, `merge`, `method`, `methodOf`, `mixin`, `negate`, `omit`, `once`, + * `pairs`, `partial`, `partialRight`, `partition`, `pick`, `plant`, `pluck`, + * `property`, `propertyOf`, `pull`, `pullAt`, `push`, `range`, `rearg`, + * `reject`, `remove`, `rest`, `restParam`, `reverse`, `set`, `shuffle`, + * `slice`, `sort`, `sortBy`, `sortByAll`, `sortByOrder`, `splice`, `spread`, + * `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, `throttle`, + * `thru`, `times`, `toArray`, `toPlainObject`, `transform`, `union`, `uniq`, + * `unshift`, `unzip`, `unzipWith`, `values`, `valuesIn`, `where`, `without`, + * `wrap`, `xor`, `zip`, `zipObject`, `zipWith` * * The wrapper methods that are **not** chainable by default are: * `add`, `attempt`, `camelCase`, `capitalize`, `clone`, `cloneDeep`, `deburr`, * `endsWith`, `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, - * `findLast`, `findLastIndex`, `findLastKey`, `findWhere`, `first`, `has`, - * `identity`, `includes`, `indexOf`, `inRange`, `isArguments`, `isArray`, - * `isBoolean`, `isDate`, `isElement`, `isEmpty`, `isEqual`, `isError`, `isFinite` - * `isFunction`, `isMatch`, `isNative`, `isNaN`, `isNull`, `isNumber`, `isObject`, - * `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `isTypedArray`, - * `join`, `kebabCase`, `last`, `lastIndexOf`, `max`, `min`, `noConflict`, - * `noop`, `now`, `pad`, `padLeft`, `padRight`, `parseInt`, `pop`, `random`, - * `reduce`, `reduceRight`, `repeat`, `result`, `runInContext`, `shift`, `size`, - * `snakeCase`, `some`, `sortedIndex`, `sortedLastIndex`, `startCase`, `startsWith`, - * `sum`, `template`, `trim`, `trimLeft`, `trimRight`, `trunc`, `unescape`, - * `uniqueId`, `value`, and `words` + * `findLast`, `findLastIndex`, `findLastKey`, `findWhere`, `first`, `get`, + * `gt`, `gte`, `has`, `identity`, `includes`, `indexOf`, `inRange`, `isArguments`, + * `isArray`, `isBoolean`, `isDate`, `isElement`, `isEmpty`, `isEqual`, `isError`, + * `isFinite` `isFunction`, `isMatch`, `isNative`, `isNaN`, `isNull`, `isNumber`, + * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, + * `isTypedArray`, `join`, `kebabCase`, `last`, `lastIndexOf`, `lt`, `lte`, + * `max`, `min`, `noConflict`, `noop`, `now`, `pad`, `padLeft`, `padRight`, + * `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, `repeat`, `result`, + * `runInContext`, `shift`, `size`, `snakeCase`, `some`, `sortedIndex`, + * `sortedLastIndex`, `startCase`, `startsWith`, `sum`, `template`, `trim`, + * `trimLeft`, `trimRight`, `trunc`, `unescape`, `uniqueId`, `value`, and `words` * * The wrapper method `sample` will return a wrapped value when `n` is provided, * otherwise an unwrapped value is returned. @@ -1568,6 +1569,35 @@ return true; } + /** + * A specialized version of `baseExtremum` for arrays whichs invokes `iteratee` + * with one argument: (value). + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} comparator The function used to compare values. + * @param {*} exValue The initial extremum value. + * @returns {*} Returns the extremum value. + */ + function arrayExtremum(array, iteratee, comparator, exValue) { + var index = -1, + length = array.length, + computed = exValue, + result = computed; + + while (++index < length) { + var value = array[index], + current = +iteratee(value); + + if (comparator(current, computed)) { + computed = current; + result = value; + } + } + return result; + } + /** * A specialized version of `_.filter` for arrays without support for callback * shorthands and `this` binding. @@ -1612,48 +1642,6 @@ return result; } - /** - * A specialized version of `_.max` for arrays without support for iteratees. - * - * @private - * @param {Array} array The array to iterate over. - * @returns {*} Returns the maximum value. - */ - function arrayMax(array) { - var index = -1, - length = array.length, - result = NEGATIVE_INFINITY; - - while (++index < length) { - var value = array[index]; - if (value > result) { - result = value; - } - } - return result; - } - - /** - * A specialized version of `_.min` for arrays without support for iteratees. - * - * @private - * @param {Array} array The array to iterate over. - * @returns {*} Returns the minimum value. - */ - function arrayMin(array) { - var index = -1, - length = array.length, - result = POSITIVE_INFINITY; - - while (++index < length) { - var value = array[index]; - if (value < result) { - result = value; - } - } - return result; - } - /** * A specialized version of `_.reduce` for arrays without support for callback * shorthands and `this` binding. @@ -2091,6 +2079,32 @@ return result; } + /** + * Gets the extremum value of `collection` invoking `iteratee` for each value + * in `collection` to generate the criterion by which the value is ranked. + * The `iteratee` is invoked with three arguments: (value, index|key, collection). + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} comparator The function used to compare values. + * @param {*} exValue The initial extremum value. + * @returns {*} Returns the extremum value. + */ + function baseExtremum(collection, iteratee, comparator, exValue) { + var computed = exValue, + result = computed; + + baseEach(collection, function(value, index, collection) { + var current = +iteratee(value, index, collection); + if (comparator(current, computed) || (current === exValue && current === result)) { + computed = current; + result = value; + } + }); + return result; + } + /** * The base implementation of `_.fill` without an iteratee call guard. * @@ -3427,27 +3441,26 @@ * extremum value. * @returns {Function} Returns the new extremum function. */ - function createExtremum(arrayFunc, isMin) { + function createExtremum(comparator, exValue) { return function(collection, iteratee, thisArg) { if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { iteratee = null; } var callback = getCallback(), - noIteratee = iteratee == null; + noIteratee = iteratee == null, + isArr = isArray(collection); - if (!(noIteratee && callback === baseCallback)) { - noIteratee = false; - iteratee = callback(iteratee, thisArg, 3); - } - if (noIteratee) { - var isArr = isArray(collection); - if (!isArr && isString(collection)) { - iteratee = charAtCallback; - } else { - return arrayFunc(isArr ? collection : toIterable(collection)); + iteratee = (noIteratee && callback === baseCallback && !isArr && isString(collection)) + ? charAtCallback + : callback(iteratee, thisArg, 3); + + if (noIteratee || (isArr && iteratee.length == 1)) { + var result = arrayExtremum(isArr ? collection : toIterable(collection), iteratee, comparator, exValue); + if (noIteratee || !(length && result === exValue)) { + return result; } } - return extremumBy(collection, iteratee, isMin); + return baseExtremum(collection, iteratee, comparator, exValue); }; } @@ -4045,34 +4058,6 @@ return true; } - /** - * Gets the extremum value of `collection` invoking `iteratee` for each value - * in `collection` to generate the criterion by which the value is ranked. - * The `iteratee` is invoked with three arguments: (value, index, collection). - * - * @private - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {boolean} [isMin] Specify returning the minimum, instead of the - * maximum, extremum value. - * @returns {*} Returns the extremum value. - */ - function extremumBy(collection, iteratee, isMin) { - var exValue = isMin ? POSITIVE_INFINITY : NEGATIVE_INFINITY, - computed = exValue, - result = computed; - - baseEach(collection, function(value, index, collection) { - var current = iteratee(value, index, collection); - if ((isMin ? (current < computed) : (current > computed)) || - (current === exValue && current === result)) { - computed = current; - result = value; - } - }); - return result; - } - /** * Gets the appropriate "callback" function. If the `_.callback` method is * customized this function returns the custom method, otherwise it returns @@ -8609,6 +8594,35 @@ : baseClone(value, true); } + + /** + * Checks if `value` is greater than `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than `other`, else `false`. + */ + function gt(value, other) { + return value > other; + } + + /** + * Checks if `value` is greater than or equal to `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than or equal to `other`, else `false`. + */ + function gte(value, other) { + return value >= other; + } + /** * Checks if `value` is classified as an `arguments` object. * @@ -8776,6 +8790,7 @@ * * @static * @memberOf _ + * @alias eq * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. @@ -9209,6 +9224,34 @@ return value === undefined; } + /** + * Checks if `value` is less than `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than `other`, else `false`. + */ + function lt(value, other) { + return value < other; + } + + /** + * Checks if `value` is less than or equal to `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than or equal to `other`, else `false`. + */ + function lte(value, other) { + return value <= other; + } + /** * Converts `value` to an array. * @@ -11822,7 +11865,7 @@ * _.max(users, 'age'); * // => { 'user': 'fred', 'age': 40 } */ - var max = createExtremum(arrayMax); + var max = createExtremum(gt, -Infinity); /** * Gets the minimum value of `collection`. If `collection` is empty or falsey @@ -11871,7 +11914,7 @@ * _.min(users, 'age'); * // => { 'user': 'barney', 'age': 36 } */ - var min = createExtremum(arrayMin, true); + var min = createExtremum(lt, Infinity); /** * Gets the sum of the values in `collection`. @@ -12093,6 +12136,8 @@ lodash.findWhere = findWhere; lodash.first = first; lodash.get = get; + lodash.gt = gt; + lodash.gte = gte; lodash.has = has; lodash.identity = identity; lodash.includes = includes; @@ -12122,6 +12167,8 @@ lodash.kebabCase = kebabCase; lodash.last = last; lodash.lastIndexOf = lastIndexOf; + lodash.lt = lt; + lodash.lte = lte; lodash.max = max; lodash.min = min; lodash.noConflict = noConflict; @@ -12158,6 +12205,7 @@ lodash.all = every; lodash.any = some; lodash.contains = includes; + lodash.eq = isEqual; lodash.detect = find; lodash.foldl = reduce; lodash.foldr = reduceRight; diff --git a/test/test.js b/test/test.js index 47a58d2edf..09a4eb2cb5 100644 --- a/test/test.js +++ b/test/test.js @@ -17816,7 +17816,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 220, function() { + test('should accept falsey arguments', 225, function() { var emptyArrays = _.map(falsey, _.constant([])), isExposed = '_' in root, oldDash = root._; @@ -17982,8 +17982,9 @@ }); test('should not contain minified method names (test production builds)', 1, function() { + var shortNames = ['at', 'eq', 'gt', 'lt']; ok(_.every(_.functions(_), function(methodName) { - return methodName.length > 2 || methodName === 'at'; + return methodName.length > 2 || _.includes(shortNames, methodName); })); }); }()); From 5b5e29cb7b3859ca6b076c73d579c5cb893c1e53 Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Wed, 6 May 2015 17:29:19 -0400 Subject: [PATCH 13/72] Fixed `iteratees` doc typos. [ci skip] --- lodash.src.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index a781300878..359275d7ad 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -6895,7 +6895,7 @@ * callback returns `true` for elements that have the properties of the given * object, else `false`. * - * Many lodash methods are guarded to work as interatees for methods like + * Many lodash methods are guarded to work as iteratees for methods like * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. * * The guarded methods are: @@ -7039,7 +7039,7 @@ * value. The `iteratee` is bound to `thisArg` and invoked with four arguments: * (accumulator, value, index|key, collection). * - * Many lodash methods are guarded to work as interatees for methods like + * Many lodash methods are guarded to work as iteratees for methods like * `_.reduce`, `_.reduceRight`, and `_.transform`. * * The guarded methods are: From 421df0dff374d94d11374bc5e9bf048859b6e688 Mon Sep 17 00:00:00 2001 From: Len Smith Date: Mon, 4 May 2015 10:37:19 -0400 Subject: [PATCH 14/72] Make `null` sorted right behind `undefined` and `NaN`. --- lodash.src.js | 25 ++++++++++++++++++++----- test/test.js | 19 +++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 359275d7ad..2d39b359f8 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -298,13 +298,22 @@ */ function baseCompareAscending(value, other) { if (value !== other) { - var valIsReflexive = value === value, + var valIsNull = value === null, + valIsUndef = value === undefined, + valIsReflexive = value === value; + + var othIsNull = other === null, + othIsUndef = other === undefined, othIsReflexive = other === other; - if (value > other || !valIsReflexive || (value === undefined && othIsReflexive)) { + if ((value > other && !othIsNull) || !valIsReflexive || + (valIsNull && !othIsUndef && othIsReflexive) || + (valIsUndef && othIsReflexive)) { return 1; } - if (value < other || !othIsReflexive || (other === undefined && valIsReflexive)) { + if ((value < other && !valIsNull) || !othIsReflexive || + (othIsNull && !valIsUndef && valIsReflexive) || + (othIsUndef && valIsReflexive)) { return -1; } } @@ -3037,7 +3046,7 @@ var mid = (low + high) >>> 1, computed = array[mid]; - if (retHighest ? (computed <= value) : (computed < value)) { + if ((retHighest ? (computed <= value) : (computed < value)) && computed !== null) { low = mid + 1; } else { high = mid; @@ -3067,17 +3076,23 @@ var low = 0, high = array ? array.length : 0, valIsNaN = value !== value, + valIsNull = value === null, valIsUndef = value === undefined; while (low < high) { var mid = floor((low + high) / 2), computed = iteratee(array[mid]), + isDef = computed !== undefined, isReflexive = computed === computed; if (valIsNaN) { var setLow = isReflexive || retHighest; + } else if (valIsNull) { + setLow = isReflexive && isDef && (retHighest || computed != null); } else if (valIsUndef) { - setLow = isReflexive && (retHighest || computed !== undefined); + setLow = isReflexive && (retHighest || isDef); + } else if (computed == null) { + setLow = false; } else { setLow = retHighest ? (computed <= value) : (computed < value); } diff --git a/test/test.js b/test/test.js index 09a4eb2cb5..3802fb859d 100644 --- a/test/test.js +++ b/test/test.js @@ -14415,9 +14415,12 @@ deepEqual(actual, [3, 1, 2]); }); - test('should move `undefined` and `NaN` values to the end', 1, function() { - var array = [NaN, undefined, 4, 1, undefined, 3, NaN, 2]; - deepEqual(_.sortBy(array), [1, 2, 3, 4, undefined, undefined, NaN, NaN]); + test('should move `null`, `undefined`, and `NaN` values to the end', 2, function() { + var array = [NaN, undefined, null, 4, null, 1, undefined, 3, NaN, 2]; + deepEqual(_.sortBy(array), [1, 2, 3, 4, null, null, undefined, undefined, NaN, NaN]); + + array = [NaN, undefined, null, 'd', null, 'a', undefined, 'c', NaN, 'b']; + deepEqual(_.sortBy(array), ['a', 'b', 'c', 'd', null, null, undefined, undefined, NaN, NaN]); }); test('should treat number values for `collection` as empty', 1, function() { @@ -14615,16 +14618,16 @@ }); test('`_.' + methodName + '` should align with `_.sortBy`', 8, function() { - var expected = [1, '2', {}, undefined, NaN, NaN]; + var expected = [1, '2', {}, null, undefined, NaN, NaN]; _.each([ - [NaN, 1, '2', {}, NaN, undefined], - ['2', 1, NaN, {}, NaN, undefined] + [NaN, null, 1, '2', {}, NaN, undefined], + ['2', null, 1, NaN, {}, NaN, undefined] ], function(array) { deepEqual(_.sortBy(array), expected); strictEqual(func(expected, 3), 2); - strictEqual(func(expected, undefined), isSortedIndex ? 3 : 4); - strictEqual(func(expected, NaN), isSortedIndex ? 4 : 6); + strictEqual(func(expected, undefined), isSortedIndex ? 4 : 5); + strictEqual(func(expected, NaN), isSortedIndex ? 5 : 7); }); }); From b5d5bef6787f44a9ed4bb4126b247ca7c234f1f0 Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 7 May 2015 00:38:22 -0700 Subject: [PATCH 15/72] Avoid undefined `length` variable use in `createExtremum`. --- lodash.src.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 2d39b359f8..5799da8efc 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3470,8 +3470,9 @@ : callback(iteratee, thisArg, 3); if (noIteratee || (isArr && iteratee.length == 1)) { - var result = arrayExtremum(isArr ? collection : toIterable(collection), iteratee, comparator, exValue); - if (noIteratee || !(length && result === exValue)) { + collection = isArr ? collection : toIterable(collection); + var result = arrayExtremum(collection, iteratee, comparator, exValue); + if (noIteratee || !(collection.length && result === exValue)) { return result; } } From a1b15df6489f47498edda244552c9804e046a45d Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 7 May 2015 01:05:16 -0700 Subject: [PATCH 16/72] Update tested Rhino to 1.7.6. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4afba8d9fd..e0ba4ef5c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,8 @@ env: - BUILD="modern" - BUILD="modern" ISTANBUL=true - BIN="phantomjs" - - BIN="rhino" - - BIN="rhino" OPTION="-require" + - BIN="rhino" OPTION="-opt -1" + - BIN="rhino" OPTION="-opt -1 -require" - BIN="ringo" matrix: include: @@ -52,8 +52,8 @@ before_install: - "npm i -g npm@\"$NPM_VERSION\"" - "[ $SAUCE_LABS == false ] || npm i chalk@\"^1.0.0\" ecstatic@\"0.7.3\" request@\"^2.0.0\" sauce-tunnel@\"2.2.3\"" - "[ $ISTANBUL == false ] || (npm i -g coveralls@\"^2.0.0\" && npm i istanbul@\"0.3.13\")" - - "[ $BIN != 'rhino' ] || (sudo mkdir /opt/rhino-1.7R5 && sudo wget --no-check-certificate -O $_/js.jar https://lodash.com/_travis/rhino-1.7R5.jar)" - - "[ $BIN != 'rhino' ] || (echo -e '#!/bin/sh\\njava -jar /opt/rhino-1.7R5/js.jar $@' | sudo tee /usr/local/bin/rhino && sudo chmod +x /usr/local/bin/rhino)" + - "[ $BIN != 'rhino' ] || (sudo mkdir /opt/rhino-1.7.6 && sudo wget --no-check-certificate -O $_/js.jar https://lodash.com/_travis/rhino-1.7.6.jar)" + - "[ $BIN != 'rhino' ] || (echo -e '#!/bin/sh\\njava -jar /opt/rhino-1.7.6/js.jar $@' | sudo tee /usr/local/bin/rhino && sudo chmod +x /usr/local/bin/rhino)" - "[ $BIN != 'ringo' ] || (wget --no-check-certificate https://lodash.com/_travis/ringojs-0.11.zip && sudo unzip ringojs-0.11 -d /opt && rm ringojs-0.11.zip)" - "[ $BIN != 'ringo' ] || (sudo ln -s /opt/ringojs-0.11/bin/ringo /usr/local/bin/ringo && sudo chmod +x $_)" - "perl -pi -e 's|\"lodash\"|\"lodash-compat\"|' ./package.json" From 004aaed7830509c8b002e9c340383675f6d088d6 Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 7 May 2015 23:53:38 -0700 Subject: [PATCH 17/72] Remove odd string support from `createExtremum`. --- lodash.src.js | 15 ++++----------- test/test.js | 15 ++++----------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 5799da8efc..b62e11d33f 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3461,18 +3461,11 @@ if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { iteratee = null; } - var callback = getCallback(), - noIteratee = iteratee == null, - isArr = isArray(collection); - - iteratee = (noIteratee && callback === baseCallback && !isArr && isString(collection)) - ? charAtCallback - : callback(iteratee, thisArg, 3); - - if (noIteratee || (isArr && iteratee.length == 1)) { - collection = isArr ? collection : toIterable(collection); + iteratee = getCallback(iteratee, thisArg, 3); + if (iteratee.length == 1) { + collection = toIterable(collection); var result = arrayExtremum(collection, iteratee, comparator, exValue); - if (noIteratee || !(collection.length && result === exValue)) { + if (!(collection.length && result === exValue)) { return result; } } diff --git a/test/test.js b/test/test.js index 3802fb859d..0c44d211b0 100644 --- a/test/test.js +++ b/test/test.js @@ -11076,26 +11076,19 @@ strictEqual(actual, isMax ? 3 : 1); }); - test('`_.' + methodName + '` should iterate a string', 2, function() { - _.each(['abc', Object('abc')], function(value) { - var actual = func(value); - strictEqual(actual, isMax ? 'c' : 'a'); - }); - }); - test('`_.' + methodName + '` should work with extremely large arrays', 1, function() { var array = _.range(0, 5e5); strictEqual(func(array), isMax ? 499999 : 0); }); - test('`_.' + methodName + '` should work as an iteratee for methods like `_.map`', 3, function() { + test('`_.' + methodName + '` should work as an iteratee for methods like `_.map`', 2, function() { var arrays = [[2, 1], [5, 4], [7, 8]], objects = [{ 'a': 2, 'b': 1 }, { 'a': 5, 'b': 4 }, { 'a': 7, 'b': 8 }], expected = isMax ? [2, 5, 8] : [1, 4, 7]; - deepEqual(_.map(arrays, func), expected); - deepEqual(_.map(objects, func), expected); - deepEqual(_.map('abc', func), ['a', 'b', 'c']); + _.each([arrays, objects], function(values) { + deepEqual(_.map(values, func), expected); + }); }); test('`_.' + methodName + '` should work when chaining on an array with only one value', 1, function() { From 172eca60814f95c491e7c305a13cf2f6d3124bc4 Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 7 May 2015 23:59:51 -0700 Subject: [PATCH 18/72] Avoid using `require` in source because browserify does a quick regexp match for \brequire\b before deciding whether to build an AST to get require calls. --- lodash.src.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index b62e11d33f..445b498620 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -10509,7 +10509,7 @@ * use a third-party library like [_he_](https://mths.be/he). * * Though the ">" character is escaped for symmetry, characters like - * ">" and "/" don't require escaping in HTML and have no special meaning + * ">" and "/" don't need escaping in HTML and have no special meaning * unless they're part of a tag or unquoted attribute value. * See [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) * (under "semi-related fun fact") for more details. @@ -11524,9 +11524,6 @@ * }); * } * - * // use `_.runInContext` to avoid conflicts (esp. in Node.js) - * var _ = require('lodash').runInContext(); - * * _.mixin({ 'vowels': vowels }); * _.vowels('fred'); * // => ['e'] @@ -12511,7 +12508,7 @@ if (moduleExports) { (freeModule.exports = _)._ = _; } - // Export for Narwhal or Rhino -require. + // Export for Narwhal or Rhino with CommonJS. else { freeExports._ = _; } From 542dd67892fa04a133a1d0bd90fcf7a00a3cde2f Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 8 May 2015 00:17:48 -0700 Subject: [PATCH 19/72] Update param docs for `createExtremum`. [ci skip] --- lodash.src.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 445b498620..4bec0ff101 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3451,9 +3451,8 @@ * Creates a `_.max` or `_.min` function. * * @private - * @param {Function} arrayFunc The function to get the extremum value from an array. - * @param {boolean} [isMin] Specify returning the minimum, instead of the maximum, - * extremum value. + * @param {Function} comparator The function used to compare values. + * @param {*} exValue The initial extremum value. * @returns {Function} Returns the new extremum function. */ function createExtremum(comparator, exValue) { From a61bde5b7837e2fe1c0a6d70cd06d90672121028 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 8 May 2015 09:49:54 -0700 Subject: [PATCH 20/72] Ensure `_.bind` works with ES6 class constructors. [closes #1193] --- lodash.src.js | 17 ++++++++++++++--- test/test.js | 27 ++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 4bec0ff101..14a9d3eacd 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -828,8 +828,8 @@ }()); /* Native method references for those with the same name as other `lodash` methods. */ - var nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, - nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, + var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, + nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, nativeIsFinite = context.isFinite, nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys, nativeMax = Math.max, @@ -3419,8 +3419,19 @@ */ function createCtorWrapper(Ctor) { return function() { + // Use a `switch` statement to work with class constructors. + // See https://people.mozilla.org/~jorendorff/es6-draft.html#sec-ecmascript-function-objects-call-thisargument-argumentslist + // for more details. + var args = arguments; + switch (args.length) { + case 0: return new Ctor; + case 1: return new Ctor(args[0]); + case 2: return new Ctor(args[0], args[1]); + case 3: return new Ctor(args[0], args[1], args[2]); + case 4: return new Ctor(args[0], args[1], args[2], args[3]); + } var thisBinding = baseCreate(Ctor.prototype), - result = Ctor.apply(thisBinding, arguments); + result = Ctor.apply(thisBinding, args); // Mimic the constructor's `return` behavior. // See https://es5.github.io/#x13.2.2 for more details. diff --git a/test/test.js b/test/test.js index 0c44d211b0..19472c21fc 100644 --- a/test/test.js +++ b/test/test.js @@ -1494,7 +1494,7 @@ deepEqual(bound(['b'], 'c'), [object, 'a', ['b'], 'c']); }); - test('should rebind functions', 3, function() { + test('should not rebind functions', 3, function() { var object1 = {}, object2 = {}, object3 = {}; @@ -1508,6 +1508,31 @@ deepEqual(bound3(), [object1, 'b']); }); + test('should not error when calling bound class constructors', 1, function() { + var createCtor = _.attempt(Function, '"use strict";return class A{}'); + if (typeof createCtor == 'function') { + var bound = _.bind(createCtor()), + expected = _.times(5, _.constant(true)); + + var actual = _.times(5, function(index) { + try { + switch (index) { + case 0: return !!(new bound); + case 1: return !!(new bound(1)); + case 2: return !!(new bound(1, 2)); + case 3: return !!(new bound(1, 2, 3)); + case 4: return !!(new bound(1, 2, 3, 4)); + } + } catch(e) {} + }); + + deepEqual(actual, expected); + } + else { + skipTest(); + } + }); + test('should return a wrapped value when chaining', 2, function() { if (!isNpm) { var object = {}, From 569b4b29aaf8111064d64853149df6fe097cc790 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 8 May 2015 11:30:26 -0700 Subject: [PATCH 21/72] Add case of 5 to `createCtorWrapper` to align with `bindCallback`. --- lodash.src.js | 1 + test/test.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 14a9d3eacd..93566bcf2f 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3429,6 +3429,7 @@ case 2: return new Ctor(args[0], args[1]); case 3: return new Ctor(args[0], args[1], args[2]); case 4: return new Ctor(args[0], args[1], args[2], args[3]); + case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); } var thisBinding = baseCreate(Ctor.prototype), result = Ctor.apply(thisBinding, args); diff --git a/test/test.js b/test/test.js index 19472c21fc..46440dacb6 100644 --- a/test/test.js +++ b/test/test.js @@ -1510,11 +1510,12 @@ test('should not error when calling bound class constructors', 1, function() { var createCtor = _.attempt(Function, '"use strict";return class A{}'); + if (typeof createCtor == 'function') { var bound = _.bind(createCtor()), - expected = _.times(5, _.constant(true)); + expected = _.times(6, _.constant(true)); - var actual = _.times(5, function(index) { + var actual = _.times(6, function(index) { try { switch (index) { case 0: return !!(new bound); @@ -1522,6 +1523,7 @@ case 2: return !!(new bound(1, 2)); case 3: return !!(new bound(1, 2, 3)); case 4: return !!(new bound(1, 2, 3, 4)); + case 5: return !!(new bound(1, 2, 3, 4, 5)); } } catch(e) {} }); From 9efb73f592faba4b064e03411517e7cb1e9c24b2 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 8 May 2015 11:21:11 -0700 Subject: [PATCH 22/72] Remove `nativeAssign` and `getOwnPropertySymbols` use. --- lodash.src.js | 53 +++++++------------------------------------------ test/index.html | 15 -------------- test/test.js | 42 +++------------------------------------ 3 files changed, 10 insertions(+), 100 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 93566bcf2f..4188a964ea 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -781,7 +781,6 @@ ceil = Math.ceil, clearTimeout = context.clearTimeout, floor = Math.floor, - getOwnPropertySymbols = isNative(getOwnPropertySymbols = Object.getOwnPropertySymbols) && getOwnPropertySymbols, getPrototypeOf = isNative(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf, push = arrayProto.push, preventExtensions = isNative(preventExtensions = Object.preventExtensions) && preventExtensions, @@ -804,29 +803,6 @@ return result; }()); - /** Used as `baseAssign`. */ - var nativeAssign = (function() { - // Avoid `Object.assign` in Firefox 34-37 which have an early implementation - // with a now defunct try/catch behavior. See https://bugzilla.mozilla.org/show_bug.cgi?id=1103344 - // for more details. - // - // Use `Object.preventExtensions` on a plain object instead of simply using - // `Object('x')` because Chrome and IE fail to throw an error when attempting - // to assign values to readonly indexes of strings. - var func = preventExtensions && isNative(func = Object.assign) && func; - try { - if (func) { - var object = preventExtensions({ '1': 0 }); - object[0] = 1; - } - } catch(e) { - // Only attempt in strict mode. - try { func(object, 'xo'); } catch(e) {} - return !object[1] && func; - } - return false; - }()); - /* Native method references for those with the same name as other `lodash` methods. */ var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, @@ -1781,10 +1757,8 @@ * @returns {Object} Returns `object`. */ function assignWith(object, source, customizer) { - var props = keys(source); - push.apply(props, getSymbols(source)); - var index = -1, + props = keys(source), length = props.length; while (++index < length) { @@ -1809,11 +1783,11 @@ * @param {Object} source The source object. * @returns {Object} Returns `object`. */ - var baseAssign = nativeAssign || function(object, source) { + function baseAssign(object, source) { return source == null ? object - : baseCopy(source, getSymbols(source), baseCopy(source, keys(source), object)); - }; + : baseCopy(source, keys(source), object); + } /** * The base implementation of `_.at` without support for string collections @@ -2603,11 +2577,9 @@ if (!isObject(object)) { return object; } - var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)); - if (!isSrcArr) { - var props = keys(source); - push.apply(props, getSymbols(source)); - } + var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), + props = !isSrcArr && keys(source); + arrayEach(props || source, function(srcValue, key) { if (props) { key = srcValue; @@ -4162,17 +4134,6 @@ */ var getLength = baseProperty('length'); - /** - * Creates an array of the own symbols of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of symbols. - */ - var getSymbols = !getOwnPropertySymbols ? constant([]) : function(object) { - return getOwnPropertySymbols(toObject(object)); - }; - /** * Gets the view, applying any `transforms` to the `start` and `end` positions. * diff --git a/test/index.html b/test/index.html index a525c7df79..71972267bd 100644 --- a/test/index.html +++ b/test/index.html @@ -90,15 +90,6 @@ setProperty(Date, '_now', Date.now); setProperty(Date, 'now', noop); - setProperty(Object, '_getOwnPropertySymbols', Object.getOwnPropertySymbols); - setProperty(Object, 'getOwnPropertySymbols', (function() { - function getOwnPropertySymbols() { - return []; - } - setProperty(getOwnPropertySymbols, 'toString', createToString('getOwnPropertySymbols')); - return getOwnPropertySymbols; - }())); - setProperty(Object, '_getPrototypeOf', Object.getPrototypeOf); setProperty(Object, 'getPrototypeOf', noop); @@ -204,11 +195,6 @@ } else { delete Date.now; } - if (Object._getOwnPropertySymbols) { - setProperty(Object, 'getOwnPropertySymbols', Object._getOwnPropertySymbols); - } else { - delete Object.getOwnPropertySymbols; - } if (Object._getPrototypeOf) { setProperty(Object, 'getPrototypeOf', Object._getPrototypeOf); } else { @@ -255,7 +241,6 @@ delete Array._isArray; delete Date._now; - delete Object._getOwnPropertySymbols; delete Object._getPrototypeOf; delete Object._keys; delete funcProto._method; diff --git a/test/test.js b/test/test.js index 46440dacb6..327fa2d30f 100644 --- a/test/test.js +++ b/test/test.js @@ -460,15 +460,6 @@ var _now = Date.now; setProperty(Date, 'now', _.noop); - var _getOwnPropertySymbols = Object.getOwnPropertySymbols; - setProperty(Object, 'getOwnPropertySymbols', (function() { - function getOwnPropertySymbols() { - return []; - } - setProperty(getOwnPropertySymbols, 'toString', createToString('getOwnPropertySymbols')); - return getOwnPropertySymbols; - }())); - var _getPrototypeOf = Object.getPrototypeOf; setProperty(Object, 'getPrototypeOf', _.noop); @@ -561,7 +552,6 @@ // Restore built-in methods. setProperty(Array, 'isArray', _isArray); setProperty(Date, 'now', _now); - setProperty(Object, 'getOwnPropertySymbols', _getOwnPropertySymbols); setProperty(Object, 'getPrototypeOf', _getPrototypeOf); setProperty(Object, 'keys', _keys); @@ -708,7 +698,7 @@ } }); - test('should avoid overwritten native methods', 13, function() { + test('should avoid overwritten native methods', 12, function() { function Foo() {} function message(lodashMethod, nativeMethod) { @@ -734,13 +724,6 @@ } ok(typeof actual == 'number', message('_.now', 'Date.now')); - try { - actual = lodashBizarro.merge({}, object); - } catch(e) { - actual = null; - } - deepEqual(actual, object, message('_.merge', 'Object.getOwnPropertySymbols')); - try { actual = [lodashBizarro.isPlainObject({}), lodashBizarro.isPlainObject([])]; } catch(e) { @@ -809,7 +792,7 @@ } } else { - skipTest(13); + skipTest(12); } }); }()); @@ -5862,25 +5845,6 @@ deepEqual(func({}, new Foo), { 'a': 1 }); }); - test('`_.' + methodName + '` should assign own symbols', 2, function() { - if (Symbol) { - var symbol1 = Symbol('a'), - symbol2 = Symbol('b'); - - var Foo = function() { - this[symbol1] = 1; - }; - Foo.prototype[symbol2] = 2; - - var actual = func({}, new Foo); - strictEqual(actual[symbol1], 1); - strictEqual(actual[symbol2], undefined); - } - else { - skipTest(2); - } - }); - test('`_.' + methodName + '` should assign problem JScript properties (test in IE < 9)', 1, function() { var object = { 'constructor': '0', @@ -16486,8 +16450,8 @@ }); test('should work with large arrays of well-known symbols', 1, function() { + // See https://people.mozilla.org/~jorendorff/es6-draft.html#sec-well-known-symbols. if (Symbol) { - // See https://people.mozilla.org/~jorendorff/es6-draft.html#sec-well-known-symbols. var expected = [ Symbol.hasInstance, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.replace, Symbol.search, Symbol.species, From f0b3c1a9ab7842a6df057669063a4d32a9a88b3b Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 8 May 2015 18:55:57 -0700 Subject: [PATCH 23/72] Cleanup `createFlow`. --- lodash.src.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 4188a964ea..150f5edada 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -3515,11 +3515,8 @@ */ function createFlow(fromRight) { return function() { - var length = arguments.length; - if (!length) { - return function() { return arguments[0]; }; - } var wrapper, + length = arguments.length, index = fromRight ? length : -1, leftIndex = 0, funcs = Array(length); @@ -3529,15 +3526,17 @@ if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } - var funcName = wrapper ? '' : getFuncName(func); - wrapper = funcName == 'wrapper' ? new LodashWrapper([]) : wrapper; + if (!wrapper && getFuncName(func) == 'wrapper') { + wrapper = new LodashWrapper([]); + } } index = wrapper ? -1 : length; while (++index < length) { func = funcs[index]; - funcName = getFuncName(func); - var data = funcName == 'wrapper' ? getData(func) : null; + var funcName = getFuncName(func), + data = funcName == 'wrapper' ? getData(func) : null; + if (data && isLaziable(data[0]) && data[1] == (ARY_FLAG | CURRY_FLAG | PARTIAL_FLAG | REARG_FLAG) && !data[4].length && data[9] == 1) { wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); } else { @@ -3550,7 +3549,7 @@ return wrapper.plant(args[0]).value(); } var index = 0, - result = funcs[index].apply(this, args); + result = length ? funcs[index].apply(this, args) : args[0]; while (++index < length) { result = funcs[index].call(this, result); From d77ace6dc37ae880442bf275adadc60592090809 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 8 May 2015 18:56:20 -0700 Subject: [PATCH 24/72] Upate tested Chrome version in sauce.js. --- test/saucelabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/saucelabs.js b/test/saucelabs.js index 3811cd9969..a2d1451421 100644 --- a/test/saucelabs.js +++ b/test/saucelabs.js @@ -108,8 +108,8 @@ var platforms = [ ['Windows 8.1', 'firefox', '37'], ['Windows 8.1', 'firefox', '36'], ['Windows 8.1', 'firefox', '20'], + ['Windows 8.1', 'chrome', '42'], ['Windows 8.1', 'chrome', '41'], - ['Windows 8.1', 'chrome', '40'], ['Windows 8.1', 'internet explorer', '11'], ['Windows 8', 'internet explorer', '10'], ['Windows 7', 'internet explorer', '9'], From dc5268b0c0fc5ee14217863f3f11213823593d11 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 9 May 2015 12:25:31 -0700 Subject: [PATCH 25/72] Add `_.gt`, `_.gte`, `_.lt`, `_.lte`, & `_.eq` unit tests. --- test/test.js | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/test.js b/test/test.js index 327fa2d30f..5379b4c2b5 100644 --- a/test/test.js +++ b/test/test.js @@ -6184,6 +6184,42 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.gt'); + + (function() { + test('should return `true` if `value` is greater than `other`', 2, function() { + strictEqual(_.gt(3, 1), true); + strictEqual(_.gt('def', 'abc'), true); + }); + + test('should return `false` if `value` is less than or equal to `other`', 4, function() { + strictEqual(_.gt(1, 3), false); + strictEqual(_.gt(3, 3), false); + strictEqual(_.gt('abc', 'def'), false); + strictEqual(_.gt('def', 'def'), false); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.gte'); + + (function() { + test('should return `true` if `value` is greater than or equal to `other`', 4, function() { + strictEqual(_.gte(3, 1), true); + strictEqual(_.gte(3, 3), true); + strictEqual(_.gte('def', 'abc'), true); + strictEqual(_.gte('def', 'def'), true); + }); + + test('should return `false` if `value` is less than `other`', 2, function() { + strictEqual(_.gte(1, 3), false); + strictEqual(_.gte('abc', 'def'), false); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.has'); (function() { @@ -7896,6 +7932,10 @@ skipTest(); } }); + + test('should be aliased', 1, function() { + strictEqual(_.eq, _.isEqual); + }); }()); /*--------------------------------------------------------------------------*/ @@ -9277,6 +9317,42 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.lt'); + + (function() { + test('should return `true` if `value` is less than `other`', 2, function() { + strictEqual(_.lt(1, 3), true); + strictEqual(_.lt('abc', 'def'), true); + }); + + test('should return `false` if `value` is greater than or equal to `other`', 4, function() { + strictEqual(_.lt(3, 1), false); + strictEqual(_.lt(3, 3), false); + strictEqual(_.lt('def', 'abc'), false); + strictEqual(_.lt('def', 'def'), false); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.lte'); + + (function() { + test('should return `true` if `value` is less than or equal to `other`', 4, function() { + strictEqual(_.lte(1, 3), true); + strictEqual(_.lte(3, 3), true); + strictEqual(_.lte('abc', 'def'), true); + strictEqual(_.lte('def', 'def'), true); + }); + + test('should return `false` if `value` is greater than `other`', 2, function() { + strictEqual(_.lt(3, 1), false); + strictEqual(_.lt('def', 'abc'), false); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.lastIndexOf'); (function() { From 311334c9e18d36f67a277ab1f2d12afe6efab239 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 9 May 2015 12:40:07 -0700 Subject: [PATCH 26/72] Add doc examples to `_.gt`, `_.gte`, `_.lt`, & `_.lte`. [ci skip] --- lodash.src.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lodash.src.js b/lodash.src.js index 150f5edada..f19e8e643a 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -8584,6 +8584,16 @@ * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is greater than `other`, else `false`. + * @example + * + * _.gt(3, 1); + * // => true + * + * _.gt(3, 3); + * // => false + * + * _.gt(1, 3); + * // => false */ function gt(value, other) { return value > other; @@ -8598,6 +8608,16 @@ * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is greater than or equal to `other`, else `false`. + * @example + * + * _.gte(3, 1); + * // => true + * + * _.gte(3, 3); + * // => true + * + * _.gte(1, 3); + * // => false */ function gte(value, other) { return value >= other; @@ -9213,6 +9233,16 @@ * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is less than `other`, else `false`. + * @example + * + * _.lt(1, 3); + * // => true + * + * _.lt(3, 3); + * // => false + * + * _.lt(3, 1); + * // => false */ function lt(value, other) { return value < other; @@ -9227,6 +9257,16 @@ * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is less than or equal to `other`, else `false`. + * @example + * + * _.lte(1, 3); + * // => true + * + * _.lte(3, 3); + * // => true + * + * _.lte(3, 1); + * // => false */ function lte(value, other) { return value <= other; From 879c1e4992f0209a274f4013318da566cb3882a4 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 9 May 2015 18:02:09 -0700 Subject: [PATCH 27/72] Increase test coverage. --- test/test.js | 99 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/test/test.js b/test/test.js index 5379b4c2b5..c95678581b 100644 --- a/test/test.js +++ b/test/test.js @@ -1453,11 +1453,36 @@ var bound = _.bind(Foo, { 'a': 1 }), newBound = new bound; - strictEqual(newBound.a, undefined); strictEqual(bound().a, 1); + strictEqual(newBound.a, undefined); ok(newBound instanceof Foo); }); + test('should handle a number of arguments when called with the `new` operator', 1, function() { + function Foo() { + return this; + } + + var bound = _.bind(Foo, { 'a': 1 }), + expected = _.times(7, _.constant(undefined)); + + var actual = _.times(7, function(index) { + try { + switch (index) { + case 0: return (new bound).a; + case 1: return (new bound(1)).a; + case 2: return (new bound(1, 2)).a; + case 3: return (new bound(1, 2, 3)).a; + case 4: return (new bound(1, 2, 3, 4)).a; + case 5: return (new bound(1, 2, 3, 4, 5)).a; + case 6: return (new bound(1, 2, 3, 4, 5, 6)).a; + } + } catch(e) {} + }); + + deepEqual(actual, expected); + }); + test('ensure `new bound` is an instance of `func`', 2, function() { function Foo(value) { return value && object; @@ -1491,7 +1516,7 @@ deepEqual(bound3(), [object1, 'b']); }); - test('should not error when calling bound class constructors', 1, function() { + test('should not error when calling bound class constructors with the `new` operator', 1, function() { var createCtor = _.attempt(Function, '"use strict";return class A{}'); if (typeof createCtor == 'function') { @@ -7772,8 +7797,10 @@ strictEqual(actual, true); }); - test('should handle comparisons if `customizer` returns `undefined`', 1, function() { + test('should handle comparisons if `customizer` returns `undefined`', 3, function() { strictEqual(_.isEqual('a', 'a', _.noop), true); + strictEqual(_.isEqual(['a'], ['a'], _.noop), true); + strictEqual(_.isEqual({ '0': 'a' }, { '0': 'a' }, _.noop), true); }); test('should not handle comparisons if `customizer` returns `true`', 3, function() { @@ -7786,6 +7813,16 @@ strictEqual(_.isEqual({ '0': 'a' }, { '0': 'b' }, customizer), true); }); + test('should not handle comparisons if `customizer` returns `false`', 3, function() { + var customizer = function(value) { + return _.isString(value) ? false : undefined; + }; + + strictEqual(_.isEqual('a', 'a', customizer), false); + strictEqual(_.isEqual(['a'], ['a'], customizer), false); + strictEqual(_.isEqual({ '0': 'a' }, { '0': 'a' }, customizer), false); + }); + test('should return a boolean value even if `customizer` does not', 2, function() { var actual = _.isEqual('a', 'b', _.constant('c')); strictEqual(actual, true); @@ -8318,7 +8355,7 @@ deepEqual(actual, [objects[0]]); }); - test('should handle a `source` with `undefined` values', 2, function() { + test('should handle a `source` with `undefined` values', 3, function() { var objects = [{ 'a': 1 }, { 'a': 1, 'b': 1 }, { 'a': 1, 'b': undefined }], source = { 'b': undefined }, predicate = function(object) { return _.isMatch(object, source); }, @@ -8327,24 +8364,14 @@ deepEqual(actual, expected); - source = { 'a': { 'c': undefined } }; - objects = [{ 'a': { 'b': 1 } }, { 'a':{ 'b':1, 'c': 1 } }, { 'a': { 'b': 1, 'c': undefined } }]; + source = { 'a': 1, 'b': undefined }; actual = _.map(objects, predicate); deepEqual(actual, expected); - }); - - test('should handle a `source` with `undefined` values', 2, function() { - var matches = _.matches({ 'b': undefined }), - objects = [{ 'a': 1 }, { 'a': 1, 'b': 1 }, { 'a': 1, 'b': undefined }], - actual = _.map(objects, matches), - expected = [false, false, true]; - deepEqual(actual, expected); - - matches = _.matches({ 'a': { 'c': undefined } }); - objects = [{ 'a': { 'b': 1 } }, { 'a': { 'b':1, 'c': 1 } }, { 'a': { 'b': 1, 'c': undefined } }]; - actual = _.map(objects, matches); + objects = [{ 'a': { 'b': 1 } }, { 'a':{ 'b':1, 'c': 1 } }, { 'a': { 'b': 1, 'c': undefined } }]; + source = { 'a': { 'c': undefined } }; + actual = _.map(objects, predicate); deepEqual(actual, expected); }); @@ -9938,16 +9965,21 @@ deepEqual(actual, [objects[0]]); }); - test('should handle a `source` with `undefined` values', 2, function() { - var matches = _.matches({ 'b': undefined }), - objects = [{ 'a': 1 }, { 'a': 1, 'b': 1 }, { 'a': 1, 'b': undefined }], + test('should handle a `source` with `undefined` values', 3, function() { + var objects = [{ 'a': 1 }, { 'a': 1, 'b': 1 }, { 'a': 1, 'b': undefined }], + matches = _.matches({ 'b': undefined }), actual = _.map(objects, matches), expected = [false, false, true]; deepEqual(actual, expected); - matches = _.matches({ 'a': { 'c': undefined } }); + matches = _.matches({ 'a': 1, 'b': undefined }); + actual = _.map(objects, matches); + + deepEqual(actual, expected); + objects = [{ 'a': { 'b': 1 } }, { 'a': { 'b':1, 'c': 1 } }, { 'a': { 'b': 1, 'c': undefined } }]; + matches = _.matches({ 'a': { 'c': undefined } }); actual = _.map(objects, matches); deepEqual(actual, expected); @@ -10232,18 +10264,24 @@ deepEqual(actual, [objects[0]]); }); - test('should handle a `value` with `undefined` values', 2, function() { - var matches = _.matchesProperty('b', undefined), - objects = [{ 'a': 1 }, { 'a': 1, 'b': 1 }, { 'a': 1, 'b': undefined }], - actual = _.map(objects, matches); + test('should handle a `value` with `undefined` values', 3, function() { + var objects = [{ 'a': 1 }, { 'a': 1, 'b': 1 }, { 'a': 1, 'b': undefined }], + matches = _.matchesProperty('b', undefined), + actual = _.map(objects, matches), + expected = [false, false, true]; - deepEqual(actual, [false, false, true]); + deepEqual(actual, expected); - matches = _.matchesProperty('a', { 'b': undefined }); objects = [{ 'a': { 'a': 1 } }, { 'a': { 'a': 1, 'b': 1 } }, { 'a': { 'a': 1, 'b': undefined } }]; + matches = _.matchesProperty('a', { 'a': 1, 'b': undefined }); actual = _.map(objects, matches); - deepEqual(actual, [false, false, true]); + deepEqual(actual, expected); + + matches = _.matchesProperty('a', { 'b': undefined }); + actual = _.map(objects, matches); + + deepEqual(actual, expected); }); test('should work with a function for `value`', 1, function() { @@ -14677,7 +14715,7 @@ strictEqual(actual, 1); }); - test('`_.' + methodName + '` should align with `_.sortBy`', 8, function() { + test('`_.' + methodName + '` should align with `_.sortBy`', 10, function() { var expected = [1, '2', {}, null, undefined, NaN, NaN]; _.each([ @@ -14686,6 +14724,7 @@ ], function(array) { deepEqual(_.sortBy(array), expected); strictEqual(func(expected, 3), 2); + strictEqual(func(expected, null), isSortedIndex ? 3 : 4); strictEqual(func(expected, undefined), isSortedIndex ? 4 : 5); strictEqual(func(expected, NaN), isSortedIndex ? 5 : 7); }); From 5c40f93ca36c88c06e8a67bbf35cd93a3c054d59 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 10 May 2015 15:29:25 -0700 Subject: [PATCH 28/72] Fix AMD tests in PhantomJS. --- test/test.js | 98 +++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/test/test.js b/test/test.js index c95678581b..49b23cb4c9 100644 --- a/test/test.js +++ b/test/test.js @@ -135,59 +135,11 @@ /** Detect if lodash is in strict mode. */ var isStrict = ui.isStrict; - /** Used to test Web Workers. */ - var Worker = !(ui.isForeign || ui.isSauceLabs || isModularize) && (document && document.origin != 'null') && root.Worker; - - /** Used to test host objects in IE. */ - try { - var xml = new ActiveXObject('Microsoft.XMLDOM'); - } catch(e) {} - - /** Use a single "load" function. */ - var load = (typeof require == 'function' && !amd) - ? require - : (isJava ? root.load : noop); - - /** The unit testing framework. */ - var QUnit = root.QUnit || (root.QUnit = ( - QUnit = load('../node_modules/qunitjs/qunit/qunit.js') || root.QUnit, - QUnit = QUnit.QUnit || QUnit - )); - - /** Load QUnit Extras and ES6 Set/WeakMap shims. */ - (function() { - var paths = [ - './asset/set.js', - './asset/weakmap.js', - '../node_modules/qunit-extras/qunit-extras.js' - ]; - - var index = -1, - length = paths.length; - - while (++index < length) { - var object = load(paths[index]); - if (object) { - object.runInContext(root); - } - } - }()); - /*--------------------------------------------------------------------------*/ - // Log params provided to `test.js`. - if (params) { - console.log('test.js invoked with arguments: ' + JSON.stringify(slice.call(params))); - } // Exit early if going to run tests in a PhantomJS web page. if (phantom && isModularize) { var page = require('webpage').create(); - page.open(filePath, function(status) { - if (status != 'success') { - console.log('PhantomJS failed to load page: ' + filePath); - phantom.exit(1); - } - }); page.onCallback = function(details) { var coverage = details.coverage; @@ -216,11 +168,57 @@ }); }; + page.open(filePath, function(status) { + if (status != 'success') { + console.log('PhantomJS failed to load page: ' + filePath); + phantom.exit(1); + } + }); + + console.log('test.js invoked with arguments: ' + JSON.stringify(slice.call(params))); return; } /*--------------------------------------------------------------------------*/ + /** Used to test Web Workers. */ + var Worker = !(ui.isForeign || ui.isSauceLabs || isModularize) && (document && document.origin != 'null') && root.Worker; + + /** Used to test host objects in IE. */ + try { + var xml = new ActiveXObject('Microsoft.XMLDOM'); + } catch(e) {} + + /** Use a single "load" function. */ + var load = (typeof require == 'function' && !amd) + ? require + : (isJava ? root.load : noop); + + /** The unit testing framework. */ + var QUnit = root.QUnit || (root.QUnit = ( + QUnit = load('../node_modules/qunitjs/qunit/qunit.js') || root.QUnit, + QUnit = QUnit.QUnit || QUnit + )); + + /** Load QUnit Extras and ES6 Set/WeakMap shims. */ + (function() { + var paths = [ + './asset/set.js', + './asset/weakmap.js', + '../node_modules/qunit-extras/qunit-extras.js' + ]; + + var index = -1, + length = paths.length; + + while (++index < length) { + var object = load(paths[index]); + if (object) { + object.runInContext(root); + } + } + }()); + /** The `lodash` function to test. */ var _ = root._ || (root._ = ( _ = load(filePath) || root._, @@ -637,6 +635,10 @@ /*--------------------------------------------------------------------------*/ + if (params) { + console.log('test.js invoked with arguments: ' + JSON.stringify(slice.call(params))); + } + QUnit.module(basename); (function() { From 190da0dcd4b84b16e86bca6fd6054b36097a3eb6 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 10 May 2015 20:09:14 -0700 Subject: [PATCH 29/72] Capitalize comments in test/index.html. [ci skip] --- test/index.html | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/index.html b/test/index.html index 71972267bd..55e8688c5e 100644 --- a/test/index.html +++ b/test/index.html @@ -12,7 +12,7 @@ - diff --git a/vendor/backbone/LICENSE b/vendor/backbone/LICENSE index 3ffd97de09..184d1b9964 100644 --- a/vendor/backbone/LICENSE +++ b/vendor/backbone/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2014 Jeremy Ashkenas, DocumentCloud +Copyright (c) 2010-2015 Jeremy Ashkenas, DocumentCloud Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/vendor/backbone/backbone.js b/vendor/backbone/backbone.js index 24a550a0ad..8ebdac9304 100644 --- a/vendor/backbone/backbone.js +++ b/vendor/backbone/backbone.js @@ -1,11 +1,16 @@ -// Backbone.js 1.1.2 +// Backbone.js 1.2.0 -// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org -(function(root, factory) { +(function(factory) { + + // Establish the root object, `window` (`self`) in the browser, or `global` on the server. + // We use `self` instead of `window` for `WebWorker` support. + var root = (typeof self == 'object' && self.self == self && self) || + (typeof global == 'object' && global.global == global && global); // Set up Backbone appropriately for the environment. Start with AMD. if (typeof define === 'function' && define.amd) { @@ -17,15 +22,16 @@ // Next for Node.js or CommonJS. jQuery may not be needed as a module. } else if (typeof exports !== 'undefined') { - var _ = require('underscore'); - factory(root, exports, _); + var _ = require('underscore'), $; + try { $ = require('jquery'); } catch(e) {} + factory(root, exports, _, $); // Finally, as a browser global. } else { root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); } -}(this, function(root, Backbone, _, $) { +}(function(root, Backbone, _, $) { // Initial Setup // ------------- @@ -36,12 +42,10 @@ // Create local references to array methods we'll want to use later. var array = []; - var push = array.push; var slice = array.slice; - var splice = array.splice; // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.1.2'; + Backbone.VERSION = '1.2.0'; // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // the `$` variable. @@ -60,7 +64,7 @@ Backbone.emulateHTTP = false; // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as + // `application/json` requests ... this will encode the body as // `application/x-www-form-urlencoded` instead and will send the model in a // form param named `model`. Backbone.emulateJSON = false; @@ -78,123 +82,235 @@ // object.on('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // - var Events = Backbone.Events = { - - // Bind an event to a `callback` function. Passing `"all"` will bind - // the callback to all events fired. - on: function(name, callback, context) { - if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; - this._events || (this._events = {}); - var events = this._events[name] || (this._events[name] = []); - events.push({callback: callback, context: context, ctx: context || this}); - return this; - }, + var Events = Backbone.Events = {}; - // Bind an event to only be triggered a single time. After the first time - // the callback is invoked, it will be removed. - once: function(name, callback, context) { - if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - return this.on(name, once, context); - }, - - // Remove one or many callbacks. If `context` is null, removes all - // callbacks with that function. If `callback` is null, removes all - // callbacks for the event. If `name` is null, removes all bound - // callbacks for all events. - off: function(name, callback, context) { - var retain, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = void 0; - return this; + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Iterates over the standard `event, callback` (as well as the fancy multiple + // space-separated events `"change blur", callback` and jQuery-style event + // maps `{event: callback}`), reducing them by manipulating `memo`. + // Passes a normalized single event name and callback, as well as any + // optional `opts`. + var eventsApi = function(iteratee, memo, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + for (names = _.keys(name); i < names.length ; i++) { + memo = iteratee(memo, names[i], name[names[i]], opts); } - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (events = this._events[name]) { - this._events[name] = retain = []; - if (callback || context) { - for (j = 0, k = events.length; j < k; j++) { - ev = events[j]; - if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || - (context && context !== ev.context)) { - retain.push(ev); - } - } - } - if (!retain.length) delete this._events[name]; - } + } else if (name && eventSplitter.test(name)) { + // Handle space separated event names. + for (names = name.split(eventSplitter); i < names.length; i++) { + memo = iteratee(memo, names[i], callback, opts); } + } else { + memo = iteratee(memo, name, callback, opts); + } + return memo; + }; - return this; - }, + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + Events.on = function(name, callback, context) { + return internalOn(this, name, callback, context); + }; - // Trigger one or many events, firing all bound callbacks. Callbacks are - // passed the same arguments as `trigger` is, apart from the event name - // (unless you're listening on `"all"`, which will cause your callback to - // receive the true name of the event as the first argument). - trigger: function(name) { - if (!this._events) return this; - var args = slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, + // An internal use `on` function, used to guard the `listening` argument from + // the public API. + var internalOn = function(obj, name, callback, context, listening) { + obj._events = eventsApi(onApi, obj._events || {}, name, callback, { + context: context, + ctx: obj, + listening: listening + }); - // Tell this object to stop listening to either specific events ... or - // to every object it's currently listening to. - stopListening: function(obj, name, callback) { - var listeningTo = this._listeningTo; - if (!listeningTo) return this; - var remove = !name && !callback; - if (!callback && typeof name === 'object') callback = this; - if (obj) (listeningTo = {})[obj._listenId] = obj; - for (var id in listeningTo) { - obj = listeningTo[id]; - obj.off(name, callback, this); - if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; - } - return this; + if (listening) { + var listeners = obj._listeners || (obj._listeners = {}); + listeners[listening.id] = listening; } + return obj; }; - // Regular expression used to split event strings. - var eventSplitter = /\s+/; + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to. + Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + var thisId = this._listenId || (this._listenId = _.uniqueId('l')); + listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; + } - // Implement fancy features of the Events API such as multiple event - // names `"change blur"` and jQuery-style event maps `{change: action}` - // in terms of the existing API. - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; + // Bind callbacks on obj, and keep track of them on listening. + internalOn(obj, name, callback, this, listening); + return this; + }; + + // The reducing API that adds a callback to the `events` object. + var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); + } + return events; + }; + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + return this; + }; + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : _.keys(listeningTo); + + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; - // Handle event maps. - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + } + if (_.isEmpty(listeningTo)) this._listeningTo = void 0; + + return this; + }; + + // The reducing API that removes a callback from the `events` object. + var offApi = function(events, name, callback, options) { + // No events to consider. + if (!events) return; + + var i = 0, length, listening; + var context = options.context, listeners = options.listeners; + + // Delete all events listeners and "drop" events. + if (!name && !callback && !context) { + var ids = _.keys(listeners); + for (; i < ids.length; i++) { + listening = listeners[ids[i]]; + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; } - return false; + return; } - // Handle space separated event names. - if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); + var names = name ? [name] : _.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Replace events if there are any remaining. Otherwise, clean up. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + listening = handler.listening; + if (listening && --listening.count === 0) { + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; + } + } + } + + // Update tail event if the list has any events. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; } - return false; } + if (_.size(events)) return events; + }; - return true; + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. When multiple events are + // passed in using the space-separated syntax, the event will fire once for every + // event you passed in, not once for a combination of all events + Events.once = function(name, callback, context) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); + return this.on(events, void 0, context); + }; + + // Inversion-of-control versions of `once`. + Events.listenToOnce = function(obj, name, callback) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); + return this.listenTo(obj, events); + }; + + // Reduces the event callbacks into a map of `{event: onceWrapper}`. + // `offer` unbinds the `onceWrapper` after it as been called. + var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = _.once(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; + }; + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; + }; + + // Handles triggering the appropriate event callbacks. + var triggerApi = function(objEvents, name, cb, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; }; // A difficult-to-believe, but optimized internal dispatch function for @@ -211,21 +327,34 @@ } }; - var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; - - // Inversion-of-control versions of `on` and `once`. Tell *this* object to - // listen to an event in another object ... keeping track of what it's - // listening to. - _.each(listenMethods, function(implementation, method) { - Events[method] = function(obj, name, callback) { - var listeningTo = this._listeningTo || (this._listeningTo = {}); - var id = obj._listenId || (obj._listenId = _.uniqueId('l')); - listeningTo[id] = obj; - if (!callback && typeof name === 'object') callback = this; - obj[implementation](name, callback, this); - return this; - }; - }); + // Proxy Underscore methods to a Backbone class' prototype using a + // particular attribute as the data argument + var addMethod = function(length, method, attribute) { + switch (length) { + case 1: return function() { + return _[method](this[attribute]); + }; + case 2: return function(value) { + return _[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return _[method](this[attribute], iteratee, context); + }; + case 4: return function(iteratee, defaultVal, context) { + return _[method](this[attribute], iteratee, defaultVal, context); + }; + default: return function() { + var args = slice.call(arguments); + args.unshift(this[attribute]); + return _[method].apply(_, args); + }; + } + }; + var addUnderscoreMethods = function(Class, methods, attribute) { + _.each(methods, function(length, method) { + if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); + }); + }; // Aliases for backwards compatibility. Events.bind = Events.on; @@ -248,7 +377,7 @@ var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); - this.cid = _.uniqueId('c'); + this.cid = _.uniqueId(this.cidPrefix); this.attributes = {}; if (options.collection) this.collection = options.collection; if (options.parse) attrs = this.parse(attrs, options) || {}; @@ -271,6 +400,10 @@ // CouchDB users may want to set this to `"_id"`. idAttribute: 'id', + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, @@ -302,6 +435,11 @@ return this.get(attr) != null; }, + // Special-cased proxy to underscore's `_.matches` method. + matches: function(attrs) { + return !!_.iteratee(attrs, this)(this.attributes); + }, + // Set a hash of model attributes on the object, firing `"change"`. This is // the core primitive operation of a model, updating the data and notifying // anyone who needs to know about the change in state. The heart of the beast. @@ -353,7 +491,7 @@ // Trigger all relevant attribute changes. if (!silent) { if (changes.length) this._pending = options; - for (var i = 0, l = changes.length; i < l; i++) { + for (var i = 0; i < changes.length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } @@ -423,9 +561,8 @@ return _.clone(this._previousAttributes); }, - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overridden, - // triggering a `"change"` event. + // Fetch the model from the server, merging the response with the model's + // local attributes. Any changed attributes will trigger a "change" event. fetch: function(options) { options = options ? _.clone(options) : {}; if (options.parse === void 0) options.parse = true; @@ -433,7 +570,7 @@ var success = options.success; options.success = function(resp) { if (!model.set(model.parse(resp, options), options)) return false; - if (success) success(model, resp, options); + if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; wrapError(this, options); @@ -444,7 +581,7 @@ // If the server returns an attributes hash that differs, the model's // state will be `set` again. save: function(key, val, options) { - var attrs, method, xhr, attributes = this.attributes; + var attrs, method, xhr, attributes = this.attributes, wait; // Handle both `"key", value` and `{key: value}` -style arguments. if (key == null || typeof key === 'object') { @@ -455,18 +592,19 @@ } options = _.extend({validate: true}, options); + wait = options.wait; // If we're not waiting and attributes exist, save acts as // `set(attr).save(null, opts)` with validation. Otherwise, check if // the model will be valid when the attributes, if any, are set. - if (attrs && !options.wait) { + if (attrs && !wait) { if (!this.set(attrs, options)) return false; } else { if (!this._validate(attrs, options)) return false; } // Set temporary attributes if `{wait: true}`. - if (attrs && options.wait) { + if (attrs && wait) { this.attributes = _.extend({}, attributes, attrs); } @@ -478,22 +616,22 @@ options.success = function(resp) { // Ensure attributes are restored during synchronous saves. model.attributes = attributes; - var serverAttrs = model.parse(resp, options); - if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs); if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { return false; } - if (success) success(model, resp, options); + if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; wrapError(this, options); method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); - if (method === 'patch') options.attrs = attrs; + if (method === 'patch' && !options.attrs) options.attrs = attrs; xhr = this.sync(method, this, options); // Restore attributes. - if (attrs && options.wait) this.attributes = attributes; + if (attrs && wait) this.attributes = attributes; return xhr; }, @@ -505,25 +643,27 @@ options = options ? _.clone(options) : {}; var model = this; var success = options.success; + var wait = options.wait; var destroy = function() { + model.stopListening(); model.trigger('destroy', model, model.collection, options); }; options.success = function(resp) { - if (options.wait || model.isNew()) destroy(); - if (success) success(model, resp, options); + if (wait) destroy(); + if (success) success.call(options.context, model, resp, options); if (!model.isNew()) model.trigger('sync', model, resp, options); }; + var xhr = false; if (this.isNew()) { - options.success(); - return false; + _.defer(options.success); + } else { + wrapError(this, options); + xhr = this.sync('delete', this, options); } - wrapError(this, options); - - var xhr = this.sync('delete', this, options); - if (!options.wait) destroy(); + if (!wait) destroy(); return xhr; }, @@ -536,7 +676,8 @@ _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; - return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); + var id = this.id || this.attributes[this.idAttribute]; + return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(id); }, // **parse** converts a response into the hash of attributes to be `set` on @@ -574,22 +715,17 @@ }); // Underscore methods that we want to implement on the Model. - var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, + omit: 0, chain: 1, isEmpty: 1 }; // Mix in each Underscore method as a proxy to `Model#attributes`. - _.each(modelMethods, function(method) { - Model.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.attributes); - return _[method].apply(_, args); - }; - }); + addUnderscoreMethods(Model, modelMethods, 'attributes'); // Backbone.Collection // ------------------- // If models tend to represent a single row of data, a Backbone Collection is - // more analagous to a table full of data ... or a small slice or page of that + // more analogous to a table full of data ... or a small slice or page of that // table, or a collection of rows that belong together for a particular reason // -- all of the messages in this particular folder, all of the documents // belonging to this particular author, and so on. Collections maintain @@ -640,24 +776,11 @@ // Remove a model, or a list of models from the set. remove: function(models, options) { - var singular = !_.isArray(models); + var singular = !_.isArray(models), removed; models = singular ? [models] : _.clone(models); options || (options = {}); - var i, l, index, model; - for (i = 0, l = models.length; i < l; i++) { - model = models[i] = this.get(models[i]); - if (!model) continue; - delete this._byId[model.id]; - delete this._byId[model.cid]; - index = this.indexOf(model); - this.models.splice(index, 1); - this.length--; - if (!options.silent) { - options.index = index; - model.trigger('remove', model, this, options); - } - this._removeReference(model, options); - } + removed = this._removeModels(models, options); + if (!options.silent && removed) this.trigger('update', this, options); return singular ? models[0] : models; }, @@ -669,32 +792,29 @@ options = _.defaults({}, options, setOptions); if (options.parse) models = this.parse(models, options); var singular = !_.isArray(models); - models = singular ? (models ? [models] : []) : _.clone(models); - var i, l, id, model, attrs, existing, sort; + models = singular ? (models ? [models] : []) : models.slice(); + var id, model, attrs, existing, sort; var at = options.at; - var targetModel = this.model; + if (at != null) at = +at; + if (at < 0) at += this.length + 1; var sortable = this.comparator && (at == null) && options.sort !== false; var sortAttr = _.isString(this.comparator) ? this.comparator : null; var toAdd = [], toRemove = [], modelMap = {}; var add = options.add, merge = options.merge, remove = options.remove; var order = !sortable && add && remove ? [] : false; + var orderChanged = false; // Turn bare objects into model references, and prevent invalid models // from being added. - for (i = 0, l = models.length; i < l; i++) { - attrs = models[i] || {}; - if (attrs instanceof Model) { - id = model = attrs; - } else { - id = attrs[targetModel.prototype.idAttribute || 'id']; - } + for (var i = 0; i < models.length; i++) { + attrs = models[i]; // If a duplicate is found, prevent it from being added and // optionally merge it into the existing model. - if (existing = this.get(id)) { + if (existing = this.get(attrs)) { if (remove) modelMap[existing.cid] = true; - if (merge) { - attrs = attrs === model ? model.attributes : attrs; + if (merge && attrs !== existing) { + attrs = this._isModel(attrs) ? attrs.attributes : attrs; if (options.parse) attrs = existing.parse(attrs, options); existing.set(attrs, options); if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; @@ -711,30 +831,38 @@ // Do not add multiple models with the same `id`. model = existing || model; - if (order && (model.isNew() || !modelMap[model.id])) order.push(model); - modelMap[model.id] = true; + if (!model) continue; + id = this.modelId(model.attributes); + if (order && (model.isNew() || !modelMap[id])) { + order.push(model); + + // Check to see if this is actually a new model at this index. + orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid; + } + + modelMap[id] = true; } // Remove nonexistent models if appropriate. if (remove) { - for (i = 0, l = this.length; i < l; ++i) { + for (var i = 0; i < this.length; i++) { if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); } - if (toRemove.length) this.remove(toRemove, options); + if (toRemove.length) this._removeModels(toRemove, options); } // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length || (order && order.length)) { + if (toAdd.length || orderChanged) { if (sortable) sort = true; this.length += toAdd.length; if (at != null) { - for (i = 0, l = toAdd.length; i < l; i++) { + for (var i = 0; i < toAdd.length; i++) { this.models.splice(at + i, 0, toAdd[i]); } } else { if (order) this.models.length = 0; var orderedModels = order || toAdd; - for (i = 0, l = orderedModels.length; i < l; i++) { + for (var i = 0; i < orderedModels.length; i++) { this.models.push(orderedModels[i]); } } @@ -745,10 +873,13 @@ // Unless silenced, it's time to fire all appropriate add/sort events. if (!options.silent) { - for (i = 0, l = toAdd.length; i < l; i++) { - (model = toAdd[i]).trigger('add', model, this, options); + var addOpts = at != null ? _.clone(options) : options; + for (var i = 0; i < toAdd.length; i++) { + if (at != null) addOpts.index = at + i; + (model = toAdd[i]).trigger('add', model, this, addOpts); } - if (sort || (order && order.length)) this.trigger('sort', this, options); + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length) this.trigger('update', this, options); } // Return the added (or merged) model (or models). @@ -760,8 +891,8 @@ // any granular `add` or `remove` events. Fires `reset` when finished. // Useful for bulk operations and optimizations. reset: function(models, options) { - options || (options = {}); - for (var i = 0, l = this.models.length; i < l; i++) { + options = options ? _.clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { this._removeReference(this.models[i], options); } options.previousModels = this.models; @@ -803,23 +934,22 @@ // Get a model from the set by id. get: function(obj) { if (obj == null) return void 0; - return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; + var id = this.modelId(this._isModel(obj) ? obj.attributes : obj); + return this._byId[obj] || this._byId[id] || this._byId[obj.cid]; }, // Get the model at the given index. at: function(index) { + if (index < 0) index += this.length; return this.models[index]; }, // Return models with matching attributes. Useful for simple cases of // `filter`. where: function(attrs, first) { - if (_.isEmpty(attrs)) return first ? void 0 : []; + var matches = _.matches(attrs); return this[first ? 'find' : 'filter'](function(model) { - for (var key in attrs) { - if (attrs[key] !== model.get(key)) return false; - } - return true; + return matches(model.attributes); }); }, @@ -863,7 +993,7 @@ options.success = function(resp) { var method = options.reset ? 'reset' : 'set'; collection[method](resp, options); - if (success) success(collection, resp, options); + if (success) success.call(options.context, collection, resp, options); collection.trigger('sync', collection, resp, options); }; wrapError(this, options); @@ -875,13 +1005,14 @@ // wait for the server to agree. create: function(model, options) { options = options ? _.clone(options) : {}; + var wait = options.wait; if (!(model = this._prepareModel(model, options))) return false; - if (!options.wait) this.add(model, options); + if (!wait) this.add(model, options); var collection = this; var success = options.success; - options.success = function(model, resp) { - if (options.wait) collection.add(model, options); - if (success) success(model, resp, options); + options.success = function(model, resp, callbackOpts) { + if (wait) collection.add(model, callbackOpts); + if (success) success.call(callbackOpts.context, model, resp, callbackOpts); }; model.save(null, options); return model; @@ -895,7 +1026,15 @@ // Create a new collection with an identical list of models as this one. clone: function() { - return new this.constructor(this.models); + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function (attrs) { + return attrs[this.model.prototype.idAttribute || 'id']; }, // Private method to reset all internal state. Called when the collection @@ -909,7 +1048,10 @@ // Prepare a hash of attributes (or other model) to be added to this // collection. _prepareModel: function(attrs, options) { - if (attrs instanceof Model) return attrs; + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } options = options ? _.clone(options) : {}; options.collection = this; var model = new this.model(attrs, options); @@ -918,11 +1060,44 @@ return false; }, + // Internal method called by both remove and set. Does not trigger any + // additional events. Returns true if anything was actually removed. + _removeModels: function(models, options) { + var i, l, index, model, removed = false; + for (var i = 0, j = 0; i < models.length; i++) { + var model = models[i] = this.get(models[i]); + if (!model) continue; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + delete this._byId[model.cid]; + var index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + models[j++] = model; + this._removeReference(model, options); + removed = true; + } + // We only need to slice if models array should be smaller, which is + // caused by some models not actually getting removed. + if (models.length !== j) models = models.slice(0, j); + return removed; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function (model) { + return model instanceof Model; + }, + // Internal method to create a model's ties to a collection. _addReference: function(model, options) { this._byId[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; - if (!model.collection) model.collection = this; + var id = this.modelId(model.attributes); + if (id != null) this._byId[id] = model; model.on('all', this._onModelEvent, this); }, @@ -939,9 +1114,13 @@ _onModelEvent: function(event, model, collection, options) { if ((event === 'add' || event === 'remove') && collection !== this) return; if (event === 'destroy') this.remove(model, options); - if (model && event === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - if (model.id != null) this._byId[model.id] = model; + if (event === 'change') { + var prevId = this.modelId(model.previousAttributes()); + var id = this.modelId(model.attributes); + if (prevId !== id) { + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } } this.trigger.apply(this, arguments); } @@ -951,27 +1130,23 @@ // Underscore methods that we want to implement on the Collection. // 90% of the core usefulness of Backbone Collections is actually implemented // right here: - var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', - 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', - 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', - 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', - 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', - 'lastIndexOf', 'isEmpty', 'chain', 'sample']; + var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4, + foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 2, + contains: 2, invoke: 2, max: 3, min: 3, toArray: 1, size: 1, first: 3, + head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, + without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, + isEmpty: 1, chain: 1, sample: 3, partition: 3 }; // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Collection.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.models); - return _[method].apply(_, args); - }; - }); + addUnderscoreMethods(Collection, collectionMethods, 'models'); // Underscore methods that take a property name as an argument. var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; // Use attributes instead of properties. _.each(attributeMethods, function(method) { + if (!_[method]) return; Collection.prototype[method] = function(value, context) { var iterator = _.isFunction(value) ? value : function(model) { return model.get(value); @@ -999,7 +1174,6 @@ _.extend(this, _.pick(options, viewOptions)); this._ensureElement(); this.initialize.apply(this, arguments); - this.delegateEvents(); }; // Cached regex to split keys for `delegate`. @@ -1034,21 +1208,37 @@ // Remove this view by taking the element out of the DOM, and removing any // applicable Backbone.Events listeners. remove: function() { - this.$el.remove(); + this._removeElement(); this.stopListening(); return this; }, - // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); return this; }, + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + }, + // Set callbacks, where `this.events` is a hash of // // *{"event selector": "callback"}* @@ -1062,8 +1252,6 @@ // pairs. Callbacks will be bound to the view, with `this` set properly. // Uses event delegation for efficiency. // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); @@ -1071,28 +1259,39 @@ var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; if (!method) continue; - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.on(eventName, method); - } else { - this.$el.on(eventName, selector, method); - } + this.delegate(match[1], match[2], _.bind(method, this)); } return this; }, - // Clears all callbacks previously bound to the view with `delegateEvents`. + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + }, + + // Clears all callbacks previously bound to the view by `delegateEvents`. // You usually don't need to use this, but may wish to if you have multiple // Backbone views attached to the same DOM element. undelegateEvents: function() { - this.$el.off('.delegateEvents' + this.cid); + if (this.$el) this.$el.off('.delegateEvents' + this.cid); return this; }, + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + }, + + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + // Ensure that the View has a DOM element to render into. // If `this.el` is a string, pass it through `$()`, take the first // matching element, and re-assign it to `el`. Otherwise, create @@ -1102,11 +1301,17 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); - this.setElement($el, false); + this.setElement(this._createElement(_.result(this, 'tagName'))); + this._setAttributes(attrs); } else { - this.setElement(_.result(this, 'el'), false); + this.setElement(_.result(this, 'el')); } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); } }); @@ -1175,14 +1380,13 @@ params.processData = false; } - // If we're sending a `PATCH` request, and we're in an old Internet Explorer - // that still has ActiveX enabled by default, override jQuery to use that - // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. - if (params.type === 'PATCH' && noXhrPatch) { - params.xhr = function() { - return new ActiveXObject("Microsoft.XMLHTTP"); - }; - } + // Pass along `textStatus` and `errorThrown` from jQuery. + var error = options.error; + options.error = function(xhr, textStatus, errorThrown) { + options.textStatus = textStatus; + options.errorThrown = errorThrown; + if (error) error.call(options.context, xhr, textStatus, errorThrown); + }; // Make the request, allowing the user to override any Ajax options. var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); @@ -1190,10 +1394,6 @@ return xhr; }; - var noXhrPatch = - typeof window !== 'undefined' && !!window.ActiveXObject && - !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. var methodMap = { 'create': 'POST', @@ -1251,17 +1451,18 @@ var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); - router.execute(callback, args); - router.trigger.apply(router, ['route:' + name].concat(args)); - router.trigger('route', name, args); - Backbone.history.trigger('route', router, name, args); + if (router.execute(callback, args, name) !== false) { + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + } }); return this; }, // Execute a route handler with the provided parameters. This is an // excellent place to do pre-route setup or post-route cleanup. - execute: function(callback, args) { + execute: function(callback, args, name) { if (callback) callback.apply(this, args); }, @@ -1334,12 +1535,6 @@ // Cached regex for stripping leading and trailing slashes. var rootStripper = /^\/+|\/+$/g; - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Cached regex for removing a trailing slash. - var trailingSlash = /\/$/; - // Cached regex for stripping urls of hash. var pathStripper = /#.*$/; @@ -1355,7 +1550,29 @@ // Are we at the app root? atRoot: function() { - return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; + var path = this.location.pathname.replace(/[^\/]$/, '$&/'); + return path === this.root && !this.getSearch(); + }, + + // Does the pathname match the root? + matchRoot: function() { + var path = this.decodeFragment(this.location.pathname); + var root = path.slice(0, this.root.length - 1) + '/'; + return root === this.root; + }, + + // Unicode characters in `location.pathname` are percent encoded so they're + // decoded for comparison. `%25` should not be decoded since it may be part + // of an encoded parameter. + decodeFragment: function(fragment) { + return decodeURI(fragment.replace(/%25/g, '%2525')); + }, + + // In IE6, the hash fragment and search params are incorrect if the + // fragment contains `?`. + getSearch: function() { + var match = this.location.href.replace(/#.*/, '').match(/\?.+/); + return match ? match[0] : ''; }, // Gets the true hash value. Cannot use location.hash directly due to bug @@ -1365,14 +1582,19 @@ return match ? match[1] : ''; }, - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment: function(fragment, forcePushState) { + // Get the pathname and search params, without the root. + getPath: function() { + var path = this.decodeFragment( + this.location.pathname + this.getSearch() + ).slice(this.root.length - 1); + return path.charAt(0) === '/' ? path.slice(1) : path; + }, + + // Get the cross-browser normalized URL fragment from the path or hash. + getFragment: function(fragment) { if (fragment == null) { - if (this._hasPushState || !this._wantsHashChange || forcePushState) { - fragment = decodeURI(this.location.pathname + this.location.search); - var root = this.root.replace(trailingSlash, ''); - if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); + if (this._usePushState || !this._wantsHashChange) { + fragment = this.getPath(); } else { fragment = this.getHash(); } @@ -1383,7 +1605,7 @@ // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. start: function(options) { - if (History.started) throw new Error("Backbone.history has already been started"); + if (History.started) throw new Error('Backbone.history has already been started'); History.started = true; // Figure out the initial configuration. Do we need an iframe? @@ -1391,36 +1613,16 @@ this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window; + this._useHashChange = this._wantsHashChange && this._hasHashChange; this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + this._hasPushState = !!(this.history && this.history.pushState); + this._usePushState = this._wantsPushState && this._hasPushState; + this.fragment = this.getFragment(); // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - if (oldIE && this._wantsHashChange) { - var frame = Backbone.$('