diff --git a/vendor/backbone/backbone.js b/vendor/backbone/backbone.js index 24a550a0a..28e9e30eb 100644 --- a/vendor/backbone/backbone.js +++ b/vendor/backbone/backbone.js @@ -36,9 +36,7 @@ // 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'; @@ -108,27 +106,46 @@ // 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; + + // Remove all callbacks for all events. if (!name && !callback && !context) { this._events = void 0; return this; } - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { + + var names = name ? [name] : _.keys(this._events); + for (var i = 0, length = names.length; i < length; 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); - } - } + + // Bail out if there are no events stored. + var events = this._events[name]; + if (!events) continue; + + // Remove all callbacks for this event. + if (!callback && !context) { + delete this._events[name]; + continue; + } + + // Find any remaining events. + var remaining = []; + for (var j = 0, k = events.length; j < k; j++) { + var event = events[j]; + if ( + callback && callback !== event.callback && + callback !== event.callback._callback || + context && context !== event.context + ) { + remaining.push(event); } - if (!retain.length) delete this._events[name]; + } + + // Replace events if there are any remaining. Otherwise, clean up. + if (remaining.length) { + this._events[name] = remaining; + } else { + delete this._events[name]; } } @@ -188,7 +205,7 @@ // Handle space separated event names. if (eventSplitter.test(name)) { var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { + for (var i = 0, length = names.length; i < length; i++) { obj[action].apply(obj, [names[i]].concat(rest)); } return false; @@ -353,7 +370,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, length = changes.length; i < length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } @@ -578,6 +595,7 @@ // Mix in each Underscore method as a proxy to `Model#attributes`. _.each(modelMethods, function(method) { + if (!_[method]) return; Model.prototype[method] = function() { var args = slice.call(arguments); args.unshift(this.attributes); @@ -589,7 +607,7 @@ // ------------------- // 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 @@ -643,13 +661,12 @@ var singular = !_.isArray(models); 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]); + for (var i = 0, length = models.length; i < length; i++) { + var model = models[i] = this.get(models[i]); if (!model) continue; delete this._byId[model.id]; delete this._byId[model.cid]; - index = this.indexOf(model); + var index = this.indexOf(model); this.models.splice(index, 1); this.length--; if (!options.silent) { @@ -669,10 +686,9 @@ 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; var sortable = this.comparator && (at == null) && options.sort !== false; var sortAttr = _.isString(this.comparator) ? this.comparator : null; var toAdd = [], toRemove = [], modelMap = {}; @@ -681,12 +697,12 @@ // Turn bare objects into model references, and prevent invalid models // from being added. - for (i = 0, l = models.length; i < l; i++) { + for (var i = 0, length = models.length; i < length; i++) { attrs = models[i] || {}; - if (attrs instanceof Model) { + if (this._isModel(attrs)) { id = model = attrs; } else { - id = attrs[targetModel.prototype.idAttribute || 'id']; + id = attrs[this.model.prototype.idAttribute || 'id']; } // If a duplicate is found, prevent it from being added and @@ -711,13 +727,14 @@ // Do not add multiple models with the same `id`. model = existing || model; + if (!model) continue; if (order && (model.isNew() || !modelMap[model.id])) order.push(model); modelMap[model.id] = true; } // Remove nonexistent models if appropriate. if (remove) { - for (i = 0, l = this.length; i < l; ++i) { + for (var i = 0, length = this.length; i < length; i++) { if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); } if (toRemove.length) this.remove(toRemove, options); @@ -728,13 +745,13 @@ if (sortable) sort = true; this.length += toAdd.length; if (at != null) { - for (i = 0, l = toAdd.length; i < l; i++) { + for (var i = 0, length = toAdd.length; i < 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, length = orderedModels.length; i < length; i++) { this.models.push(orderedModels[i]); } } @@ -745,7 +762,7 @@ // Unless silenced, it's time to fire all appropriate add/sort events. if (!options.silent) { - for (i = 0, l = toAdd.length; i < l; i++) { + for (var i = 0, length = toAdd.length; i < length; i++) { (model = toAdd[i]).trigger('add', model, this, options); } if (sort || (order && order.length)) this.trigger('sort', this, options); @@ -761,7 +778,7 @@ // Useful for bulk operations and optimizations. reset: function(models, options) { options || (options = {}); - for (var i = 0, l = this.models.length; i < l; i++) { + for (var i = 0, length = this.models.length; i < length; i++) { this._removeReference(this.models[i], options); } options.previousModels = this.models; @@ -895,7 +912,10 @@ // 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 + }); }, // Private method to reset all internal state. Called when the collection @@ -909,7 +929,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 +941,16 @@ return false; }, + // 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; model.on('all', this._onModelEvent, this); }, @@ -956,10 +984,11 @@ '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']; + 'lastIndexOf', 'isEmpty', 'chain', 'sample', 'partition']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { + if (!_[method]) return; Collection.prototype[method] = function() { var args = slice.call(arguments); args.unshift(this.models); @@ -972,6 +1001,7 @@ // 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 +1029,6 @@ _.extend(this, _.pick(options, viewOptions)); this._ensureElement(); this.initialize.apply(this, arguments); - this.delegateEvents(); }; // Cached regex to split keys for `delegate`. @@ -1034,21 +1063,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` and a hash of `attributes`. `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 +1107,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 +1114,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 +1156,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); } }); @@ -1184,6 +1244,14 @@ }; } + // 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.apply(this, arguments); + }; + // Make the request, allowing the user to override any Ajax options. var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); model.trigger('request', model, xhr, options); @@ -1251,17 +1319,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 +1403,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 +1418,8 @@ // 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.location.search; }, // Gets the true hash value. Cannot use location.hash directly due to bug @@ -1365,14 +1429,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 = decodeURI(this.location.pathname + this.location.search); + var root = this.root.slice(0, -1); + if (!path.indexOf(root)) path = path.slice(root.length); + return path.slice(1); + }, + + // 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._hasPushState || !this._wantsHashChange) { + fragment = this.getPath(); } else { fragment = this.getHash(); } @@ -1391,36 +1460,43 @@ this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window; 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.fragment = this.getFragment(); + + // Add a cross-platform `addEventListener` shim for older browsers. + var addEventListener = window.addEventListener || function (eventName, listener) { + return attachEvent('on' + eventName, listener); + }; // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - if (oldIE && this._wantsHashChange) { - var frame = Backbone.$('