From e205fe332f6c17633b5bd9f15275462ec94cf90d Mon Sep 17 00:00:00 2001 From: Puru Dahal Date: Fri, 25 Sep 2015 15:09:09 -0700 Subject: [PATCH] Update backbone version to 1.2.3 --- vendor/assets/javascripts/backbone.js | 458 ++++++++++++++------------ 1 file changed, 242 insertions(+), 216 deletions(-) diff --git a/vendor/assets/javascripts/backbone.js b/vendor/assets/javascripts/backbone.js index 8ebdac9..c924965 100644 --- a/vendor/assets/javascripts/backbone.js +++ b/vendor/assets/javascripts/backbone.js @@ -1,4 +1,4 @@ -// Backbone.js 1.2.0 +// Backbone.js 1.2.3 // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Backbone may be freely distributed under the MIT license. @@ -40,12 +40,11 @@ // restored later on, if `noConflict` is used. var previousBackbone = root.Backbone; - // Create local references to array methods we'll want to use later. - var array = []; - var slice = array.slice; + // Create a local reference to a common array method we'll want to use later. + var slice = Array.prototype.slice; // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.2.0'; + Backbone.VERSION = '1.2.3'; // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // the `$` variable. @@ -69,12 +68,60 @@ // form param named `model`. Backbone.emulateJSON = false; + // Proxy Backbone class methods to Underscore functions, wrapping the model's + // `attributes` object or collection's `models` array behind the scenes. + // + // collection.filter(function(model) { return model.get('age') > 10 }); + // collection.each(this.addView); + // + // `Function#apply` can be slow so we use the method's arg count, if we know it. + 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], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return _[method](this[attribute], cb(iteratee, this), 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); + }); + }; + + // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. + var cb = function(iteratee, instance) { + if (_.isFunction(iteratee)) return iteratee; + if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; + }; + var modelMatcher = function(attrs) { + var matcher = _.matches(attrs); + return function(model) { + return matcher(model.attributes); + }; + }; + // Backbone.Events // --------------- // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may bind with `on` or remove with `off` callback - // functions to an event; `trigger`-ing an event fires all callbacks in + // a custom event channel. You may bind a callback to an event with `on` or + // remove with `off`; `trigger`-ing an event fires all callbacks in // succession. // // var object = {}; @@ -89,25 +136,25 @@ // 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) { + // maps `{event: callback}`). + var eventsApi = function(iteratee, events, name, callback, opts) { var i = 0, names; if (name && typeof name === 'object') { // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; for (names = _.keys(name); i < names.length ; i++) { - memo = iteratee(memo, names[i], name[names[i]], opts); + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); } } else if (name && eventSplitter.test(name)) { - // Handle space separated event names. + // Handle space separated event names by delegating them individually. for (names = name.split(eventSplitter); i < names.length; i++) { - memo = iteratee(memo, names[i], callback, opts); + events = iteratee(events, names[i], callback, opts); } } else { - memo = iteratee(memo, name, callback, opts); + // Finally, standard events. + events = iteratee(events, name, callback, opts); } - return memo; + return events; }; // Bind an event to a `callback` function. Passing `"all"` will bind @@ -116,8 +163,7 @@ return internalOn(this, name, callback, context); }; - // An internal use `on` function, used to guard the `listening` argument from - // the public API. + // 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, @@ -134,7 +180,8 @@ }; // 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. + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. Events.listenTo = function(obj, name, callback) { if (!obj) return this; var id = obj._listenId || (obj._listenId = _.uniqueId('l')); @@ -202,10 +249,9 @@ // 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 i = 0, listening; var context = options.context, listeners = options.listeners; // Delete all events listeners and "drop" events. @@ -257,9 +303,9 @@ }; // 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 + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, 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)); @@ -274,7 +320,7 @@ }; // Reduces the event callbacks into a map of `{event: onceWrapper}`. - // `offer` unbinds the `onceWrapper` after it as been called. + // `offer` unbinds the `onceWrapper` after it has been called. var onceMap = function(map, name, callback, offer) { if (callback) { var once = map[name] = _.once(function() { @@ -327,35 +373,6 @@ } }; - // 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; Events.unbind = Events.off; @@ -444,10 +461,10 @@ // 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. set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; if (key == null) return this; // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; if (typeof key === 'object') { attrs = key; options = val; @@ -461,33 +478,36 @@ if (!this._validate(attrs, options)) return false; // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; + var unset = options.unset; + var silent = options.silent; + var changes = []; + var changing = this._changing; + this._changing = true; if (!changing) { this._previousAttributes = _.clone(this.attributes); this.changed = {}; } - current = this.attributes, prev = this._previousAttributes; - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; // For each `set` attribute, update or delete the current value. - for (attr in attrs) { + for (var attr in attrs) { val = attrs[attr]; if (!_.isEqual(current[attr], val)) changes.push(attr); if (!_.isEqual(prev[attr], val)) { - this.changed[attr] = val; + changed[attr] = val; } else { - delete this.changed[attr]; + delete changed[attr]; } unset ? delete current[attr] : current[attr] = val; } + // Update the `id`. + this.id = this.get(this.idAttribute); + // Trigger all relevant attribute changes. if (!silent) { if (changes.length) this._pending = options; @@ -539,13 +559,14 @@ // determining if there *would be* a change. changedAttributes: function(diff) { if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; - var val, changed = false; var old = this._changing ? this._previousAttributes : this.attributes; + var changed = {}; for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; + var val = diff[attr]; + if (_.isEqual(old[attr], val)) continue; + changed[attr] = val; } - return changed; + return _.size(changed) ? changed : false; }, // Get the previous value of an attribute, recorded at the time the last @@ -564,12 +585,12 @@ // 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; + options = _.extend({parse: true}, options); var model = this; var success = options.success; options.success = function(resp) { - if (!model.set(model.parse(resp, options), options)) return false; + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (!model.set(serverAttrs, options)) return false; if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; @@ -581,9 +602,8 @@ // 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, wait; - // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; if (key == null || typeof key === 'object') { attrs = key; options = val; @@ -591,8 +611,8 @@ (attrs = {})[key] = val; } - options = _.extend({validate: true}, options); - wait = options.wait; + options = _.extend({validate: true, parse: true}, options); + var wait = options.wait; // If we're not waiting and attributes exist, save acts as // `set(attr).save(null, opts)` with validation. Otherwise, check if @@ -603,35 +623,31 @@ if (!this._validate(attrs, options)) return false; } - // Set temporary attributes if `{wait: true}`. - if (attrs && wait) { - this.attributes = _.extend({}, attributes, attrs); - } - // After a successful server-side save, the client is (optionally) // updated with the server-side state. - if (options.parse === void 0) options.parse = true; var model = this; var success = options.success; + var attributes = this.attributes; options.success = function(resp) { // Ensure attributes are restored during synchronous saves. model.attributes = attributes; 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 (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (serverAttrs && !model.set(serverAttrs, options)) return false; 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'); + // Set temporary attributes if `{wait: true}` to properly find new ids. + if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); + + var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); if (method === 'patch' && !options.attrs) options.attrs = attrs; - xhr = this.sync(method, this, options); + var xhr = this.sync(method, this, options); // Restore attributes. - if (attrs && wait) this.attributes = attributes; + this.attributes = attributes; return xhr; }, @@ -676,8 +692,8 @@ _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; - var id = this.id || this.attributes[this.idAttribute]; - return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(id); + var id = this.get(this.idAttribute); + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); }, // **parse** converts a response into the hash of attributes to be `set` on @@ -698,7 +714,7 @@ // Check if the model is currently in a valid state. isValid: function(options) { - return this._validate({}, _.extend(options || {}, { validate: true })); + return this._validate({}, _.defaults({validate: true}, options)); }, // Run validation against the next complete set of model attributes, @@ -714,7 +730,8 @@ }); - // Underscore methods that we want to implement on the Model. + // Underscore methods that we want to implement on the Model, mapped to the + // number of arguments they take. var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, omit: 0, chain: 1, isEmpty: 1 }; @@ -747,6 +764,16 @@ var setOptions = {add: true, remove: true, merge: true}; var addOptions = {add: true, remove: false}; + // Splices `insert` into `array` at index `at`. + var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + for (var i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; + }; + // Define the Collection's inheritable methods. _.extend(Collection.prototype, Events, { @@ -761,7 +788,7 @@ // The JSON representation of a Collection is an array of the // models' attributes. toJSON: function(options) { - return this.map(function(model){ return model.toJSON(options); }); + return this.map(function(model) { return model.toJSON(options); }); }, // Proxy `Backbone.sync` by default. @@ -769,19 +796,21 @@ return Backbone.sync.apply(this, arguments); }, - // Add a model, or list of models to the set. + // Add a model, or list of models to the set. `models` may be Backbone + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. add: function(models, options) { return this.set(models, _.extend({merge: false}, options, addOptions)); }, // Remove a model, or a list of models from the set. remove: function(models, options) { - var singular = !_.isArray(models), removed; + options = _.extend({}, options); + var singular = !_.isArray(models); models = singular ? [models] : _.clone(models); - options || (options = {}); - removed = this._removeModels(models, options); + var removed = this._removeModels(models, options); if (!options.silent && removed) this.trigger('update', this, options); - return singular ? models[0] : models; + return singular ? removed[0] : removed; }, // Update a collection by `set`-ing a new list of models, adding new ones, @@ -789,83 +818,88 @@ // already exist in the collection, as necessary. Similar to **Model#set**, // the core operation for updating the data contained by the collection. set: function(models, options) { + if (models == null) return; + options = _.defaults({}, options, setOptions); - if (options.parse) models = this.parse(models, options); + if (options.parse && !this._isModel(models)) models = this.parse(models, options); + var singular = !_.isArray(models); - models = singular ? (models ? [models] : []) : models.slice(); - var id, model, attrs, existing, sort; + models = singular ? [models] : models.slice(); + var at = options.at; if (at != null) at = +at; if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; 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. + var model; for (var i = 0; i < models.length; i++) { - attrs = models[i]; + model = models[i]; // If a duplicate is found, prevent it from being added and // optionally merge it into the existing model. - if (existing = this.get(attrs)) { - if (remove) modelMap[existing.cid] = true; - if (merge && attrs !== existing) { - attrs = this._isModel(attrs) ? attrs.attributes : attrs; + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; if (options.parse) attrs = existing.parse(attrs, options); existing.set(attrs, options); - if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); } models[i] = existing; // If this is a new, valid model, push it to the `toAdd` list. } else if (add) { - model = models[i] = this._prepareModel(attrs, options); - if (!model) continue; - toAdd.push(model); - this._addReference(model, options); - } - - // Do not add multiple models with the same `id`. - model = existing || model; - 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; + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } } - - modelMap[id] = true; } - // Remove nonexistent models if appropriate. + // Remove stale models. if (remove) { - for (var i = 0; i < this.length; i++) { - if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); } if (toRemove.length) this._removeModels(toRemove, options); } // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length || orderChanged) { + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length != set.length || _.some(this.models, function(model, index) { + return model !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.length) { if (sortable) sort = true; - this.length += toAdd.length; - if (at != null) { - 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 (var i = 0; i < orderedModels.length; i++) { - this.models.push(orderedModels[i]); - } - } + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; } // Silently sort the collection if appropriate. @@ -873,10 +907,10 @@ // Unless silenced, it's time to fire all appropriate add/sort events. if (!options.silent) { - 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); + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); } if (sort || orderChanged) this.trigger('sort', this, options); if (toAdd.length || toRemove.length) this.trigger('update', this, options); @@ -910,8 +944,7 @@ // Remove a model from the end of the collection. pop: function(options) { var model = this.at(this.length - 1); - this.remove(model, options); - return model; + return this.remove(model, options); }, // Add a model to the beginning of the collection. @@ -922,8 +955,7 @@ // Remove a model from the beginning of the collection. shift: function(options) { var model = this.at(0); - this.remove(model, options); - return model; + return this.remove(model, options); }, // Slice out a sub-array of models from the collection. @@ -947,10 +979,7 @@ // Return models with matching attributes. Useful for simple cases of // `filter`. where: function(attrs, first) { - var matches = _.matches(attrs); - return this[first ? 'find' : 'filter'](function(model) { - return matches(model.attributes); - }); + return this[first ? 'find' : 'filter'](attrs); }, // Return the first model with matching attributes. Useful for simple cases @@ -963,16 +992,19 @@ // normal circumstances, as the set will maintain sort order as each item // is added. sort: function(options) { - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); options || (options = {}); + var length = comparator.length; + if (_.isFunction(comparator)) comparator = _.bind(comparator, this); + // Run sort based on type of `comparator`. - if (_.isString(this.comparator) || this.comparator.length === 1) { - this.models = this.sortBy(this.comparator, this); + if (length === 1 || _.isString(comparator)) { + this.models = this.sortBy(comparator); } else { - this.models.sort(_.bind(this.comparator, this)); + this.models.sort(comparator); } - if (!options.silent) this.trigger('sort', this, options); return this; }, @@ -986,8 +1018,7 @@ // collection when they arrive. If `reset: true` is passed, the response // data will be passed through the `reset` method instead of `set`. fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; + options = _.extend({parse: true}, options); var success = options.success; var collection = this; options.success = function(resp) { @@ -1006,7 +1037,8 @@ create: function(model, options) { options = options ? _.clone(options) : {}; var wait = options.wait; - if (!(model = this._prepareModel(model, options))) return false; + model = this._prepareModel(model, options); + if (!model) return false; if (!wait) this.add(model, options); var collection = this; var success = options.success; @@ -1060,31 +1092,26 @@ return false; }, - // Internal method called by both remove and set. Does not trigger any - // additional events. Returns true if anything was actually removed. + // Internal method called by both remove and set. _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]); + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = 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; + + removed.push(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; + return removed.length ? removed : false; }, // Method for checking whether an object should be considered a model for @@ -1103,6 +1130,9 @@ // Internal method to sever a model's ties to a collection. _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); }, @@ -1132,29 +1162,16 @@ // right here: 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, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, + contains: 3, invoke: 0, 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 }; + isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, + sortBy: 3, indexBy: 3}; // Mix in each Underscore method as a proxy to `Collection#models`. 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); - }; - return _[method](this.models, iterator, context); - }; - }); - // Backbone.View // ------------- @@ -1170,7 +1187,6 @@ // if an existing element is not provided... var View = Backbone.View = function(options) { this.cid = _.uniqueId('view'); - options || (options = {}); _.extend(this, _.pick(options, viewOptions)); this._ensureElement(); this.initialize.apply(this, arguments); @@ -1179,7 +1195,7 @@ // Cached regex to split keys for `delegate`. var delegateEventSplitter = /^(\S+)\s*(.*)$/; - // List of view options to be merged as properties. + // List of view options to be set as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; // Set up all inheritable **Backbone.View** properties and methods. @@ -1253,11 +1269,12 @@ // Uses event delegation for efficiency. // Omitting the selector binds the event to `this.el`. delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return this; + events || (events = _.result(this, 'events')); + if (!events) return this; this.undelegateEvents(); for (var key in events) { var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; + if (!_.isFunction(method)) method = this[method]; if (!method) continue; var match = key.match(delegateEventSplitter); this.delegate(match[1], match[2], _.bind(method, this)); @@ -1270,6 +1287,7 @@ // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. delegate: function(eventName, selector, listener) { this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. @@ -1284,6 +1302,7 @@ // `selector` and `listener` are both optional. undelegate: function(eventName, selector, listener) { this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; }, // Produces a DOM element to be assigned to your view. Exposed for @@ -1520,7 +1539,7 @@ // falls back to polling. var History = Backbone.History = function() { this.handlers = []; - _.bindAll(this, 'checkUrl'); + this.checkUrl = _.bind(this.checkUrl, this); // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { @@ -1613,7 +1632,7 @@ this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; - this._hasHashChange = 'onhashchange' in window; + this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); this._useHashChange = this._wantsHashChange && this._hasHashChange; this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.history && this.history.pushState); @@ -1647,15 +1666,16 @@ // support the `hashchange` event, HTML5 history, or the user wants // `hashChange` but not `pushState`. if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { - var iframe = document.createElement('iframe'); - iframe.src = 'javascript:0'; - iframe.style.display = 'none'; - iframe.tabIndex = -1; + this.iframe = document.createElement('iframe'); + this.iframe.src = 'javascript:0'; + this.iframe.style.display = 'none'; + this.iframe.tabIndex = -1; var body = document.body; // Using `appendChild` will throw on IE < 9 if the document is not ready. - this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow; - this.iframe.document.open().close(); - this.iframe.location.hash = '#' + this.fragment; + var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; + iWindow.document.open(); + iWindow.document.close(); + iWindow.location.hash = '#' + this.fragment; } // Add a cross-platform `addEventListener` shim for older browsers. @@ -1693,7 +1713,7 @@ // Clean up the iframe if necessary. if (this.iframe) { - document.body.removeChild(this.iframe.frameElement); + document.body.removeChild(this.iframe); this.iframe = null; } @@ -1716,7 +1736,7 @@ // If the user pressed the back button, the iframe's hash will have // changed and we should use that for comparison. if (current === this.fragment && this.iframe) { - current = this.getHash(this.iframe); + current = this.getHash(this.iframe.contentWindow); } if (current === this.fragment) return false; @@ -1731,7 +1751,7 @@ // If the root doesn't match, no routes can match either. if (!this.matchRoot()) return false; fragment = this.fragment = this.getFragment(fragment); - return _.any(this.handlers, function(handler) { + return _.some(this.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; @@ -1774,12 +1794,18 @@ // fragment to store history. } else if (this._wantsHashChange) { this._updateHash(this.location, fragment, options.replace); - if (this.iframe && (fragment !== this.getHash(this.iframe))) { + if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) { + var iWindow = this.iframe.contentWindow; + // Opening and closing the iframe tricks IE7 and earlier to push a // history entry on hash-tag change. When replace is true, we don't // want this. - if (!options.replace) this.iframe.document.open().close(); - this._updateHash(this.iframe.location, fragment, options.replace); + if (!options.replace) { + iWindow.document.open(); + iWindow.document.close(); + } + + this._updateHash(iWindow.location, fragment, options.replace); } // If you've told us that you explicitly don't want fallback hashchange-