diff --git a/vendor/backbone/backbone.js b/vendor/backbone/backbone.js index 8ebdac930..58800425c 100644 --- a/vendor/backbone/backbone.js +++ b/vendor/backbone/backbone.js @@ -1,4 +1,4 @@ -// Backbone.js 1.2.0 +// Backbone.js 1.2.1 // (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 = [].slice; // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.2.0'; + Backbone.VERSION = '1.2.1'; // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // the `$` variable. @@ -69,6 +68,35 @@ // form param named `model`. Backbone.emulateJSON = false; + // 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); + }); + }; + // Backbone.Events // --------------- @@ -96,6 +124,7 @@ 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); } @@ -205,7 +234,7 @@ // 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. @@ -274,7 +303,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 +356,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 +444,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,29 +461,32 @@ 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; + + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; // Check for changes of `id`. if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; // 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; } @@ -539,13 +542,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 +568,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 +585,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 +594,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 +606,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 +675,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 +697,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, @@ -761,7 +760,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. @@ -776,12 +775,12 @@ // 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, @@ -790,7 +789,7 @@ // the core operation for updating the data contained by the collection. set: function(models, options) { 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; @@ -910,8 +909,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 +920,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. @@ -986,8 +983,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 +1002,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 +1057,27 @@ 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. + // Returns removed models, or false if nothing is 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]); + 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 +1096,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); }, @@ -1133,7 +1129,7 @@ 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, + contains: 2, 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 }; @@ -1170,7 +1166,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); @@ -1253,11 +1248,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 +1266,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 +1281,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 @@ -1647,15 +1645,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 +1692,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 +1715,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; @@ -1774,12 +1773,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- diff --git a/vendor/backbone/test/collection.js b/vendor/backbone/test/collection.js index 51a9b7bef..fa8481861 100644 --- a/vendor/backbone/test/collection.js +++ b/vendor/backbone/test/collection.js @@ -298,17 +298,13 @@ deepEqual(col.pluck('id'), [1, 2, 3]); }); - test("remove", 7, function() { + test("remove", 10, function() { var removed = null; - var otherRemoved = null; var result = null; col.on('remove', function(model, col, options) { removed = model.get('label'); equal(options.index, 3); }); - otherCol.on('remove', function(model, col, options) { - otherRemoved = true; - }); result = col.remove(d); equal(removed, 'd'); strictEqual(result, d); @@ -317,7 +313,13 @@ strictEqual(result, undefined); equal(col.length, 3); equal(col.first(), a); - equal(otherRemoved, null); + col.off(); + result = col.remove([c, d]); + equal(result.length, 1, 'only returns removed models'); + equal(result[0], c, 'only returns removed models'); + result = col.remove([c, b]); + equal(result.length, 1, 'only returns removed models'); + equal(result[0], b, 'only returns removed models'); }); test("add and remove return values", 13, function() { @@ -559,6 +561,20 @@ }); + test("create with wait:true should not call collection.parse", 0, function() { + var Collection = Backbone.Collection.extend({ + url: '/test', + parse: function () { + ok(false); + } + }); + + var collection = new Collection; + + collection.create({}, {wait: true}); + this.ajaxSettings.success(); + }); + test("a failing create returns model with errors", function() { var ValidatingModel = Backbone.Model.extend({ validate: function(attrs) { @@ -1605,4 +1621,19 @@ collection.set([{id: 1}, {id: 2}]); }); + test("#3610 - invoke collects arguments", 3, function() { + var Model = Backbone.Model.extend({ + method: function(a, b, c) { + equal(a, 1); + equal(b, 2); + equal(c, 3); + } + }); + var Collection = Backbone.Collection.extend({ + model: Model + }); + var collection = new Collection([{id: 1}]); + collection.invoke('method', 1, 2, 3); + }); + })(); diff --git a/vendor/backbone/test/events.js b/vendor/backbone/test/events.js index 017cf47f6..a2730bc01 100644 --- a/vendor/backbone/test/events.js +++ b/vendor/backbone/test/events.js @@ -66,6 +66,24 @@ equal(obj.counter, 5); }); + test("binding and trigger with event maps context", 2, function() { + var obj = { counter: 0 }; + var context = {}; + _.extend(obj, Backbone.Events); + + obj.on({ + a: function() { + strictEqual(this, context, 'defaults `context` to `callback` param'); + } + }, context).trigger('a'); + + obj.off().on({ + a: function() { + strictEqual(this, context, 'will not override explicit `context` param'); + } + }, this, context).trigger('a'); + }); + test("listenTo and stopListening", 1, function() { var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); diff --git a/vendor/backbone/test/model.js b/vendor/backbone/test/model.js index 94647bbd0..faaf61dda 100644 --- a/vendor/backbone/test/model.js +++ b/vendor/backbone/test/model.js @@ -550,6 +550,21 @@ model.destroy(options); }); + test("#3470 - save and fetch with parse false", 2, function() { + var i = 0; + var model = new Backbone.Model(); + model.parse = function() { + ok(false); + }; + model.sync = function(method, model, options) { + options.success({i: ++i}); + }; + model.fetch({parse: false}); + equal(model.get('i'), i); + model.save(null, {parse: false}); + equal(model.get('i'), i); + }); + test("save with PATCH", function() { doc.clear().set({id: 1, a: 1, b: 2, c: 3, d: 4}); doc.save(); diff --git a/vendor/backbone/test/router.js b/vendor/backbone/test/router.js index db2db909c..acd17cecb 100644 --- a/vendor/backbone/test/router.js +++ b/vendor/backbone/test/router.js @@ -918,7 +918,7 @@ test('#3358 - pushState to hashChange transition with search params', 1, function() { Backbone.history.stop(); - location.replace('/root?foo=bar'); + location.replace('http://example.com/root?foo=bar'); location.replace = function(url) { strictEqual(url, '/root#?foo=bar'); }; diff --git a/vendor/backbone/test/view.js b/vendor/backbone/test/view.js index 392a50f79..a3e269749 100644 --- a/vendor/backbone/test/view.js +++ b/vendor/backbone/test/view.js @@ -48,6 +48,11 @@ strictEqual(new View().one, 1); }); + test("render", 1, function() { + var view = new Backbone.View; + equal(view.render(), view, '#render returns the view instance'); + }); + test("delegateEvents", 6, function() { var counter1 = 0, counter2 = 0; @@ -72,7 +77,7 @@ equal(counter2, 3); }); - test("delegate", 2, function() { + test("delegate", 3, function() { var view = new Backbone.View({el: '#testElement'}); view.delegate('click', 'h1', function() { ok(true); @@ -81,6 +86,8 @@ ok(true); }); view.$('h1').trigger('click'); + + equal(view.delegate(), view, '#delegate returns the view instance'); }); test("delegateEvents allows functions for callbacks", 3, function() { @@ -112,7 +119,7 @@ view.$el.trigger('click'); }); - test("undelegateEvents", 6, function() { + test("undelegateEvents", 7, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); @@ -135,9 +142,11 @@ view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 3); + + equal(view.undelegateEvents(), view, '#undelegateEvents returns the view instance'); }); - test("undelegate", 0, function() { + test("undelegate", 1, function() { view = new Backbone.View({el: '#testElement'}); view.delegate('click', function() { ok(false); }); view.delegate('click', 'h1', function() { ok(false); }); @@ -146,6 +155,8 @@ view.$('h1').trigger('click'); view.$el.trigger('click'); + + equal(view.undelegate(), view, '#undelegate returns the view instance'); }); test("undelegate with passed handler", 1, function() { @@ -387,14 +398,14 @@ equal(counter, 2); }); - test("remove", 1, function() { + test("remove", 2, function() { var view = new Backbone.View; document.body.appendChild(view.el); view.delegate('click', function() { ok(false); }); view.listenTo(view, 'all x', function() { ok(false); }); - view.remove(); + equal(view.remove(), view, '#remove returns the view instance'); view.$el.trigger('click'); view.trigger('x'); @@ -402,4 +413,25 @@ notEqual(view.el.parentNode, document.body); }); + test("setElement", 3, function() { + var view = new Backbone.View({ + events: { + click: function() { ok(false); } + } + }); + view.events = { + click: function() { ok(true); } + }; + var oldEl = view.el; + var $oldEl = view.$el; + + view.setElement(document.createElement('div')); + + $oldEl.click(); + view.$el.click(); + + notEqual(oldEl, view.el); + notEqual($oldEl, view.$el); + }); + })();