Update vendor/backbone to 1.2.1.

This commit is contained in:
jdalton
2015-06-04 19:27:19 -07:00
parent 7a9011c7e1
commit 9e1f68d9eb
6 changed files with 229 additions and 128 deletions

View File

@@ -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-

View File

@@ -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);
});
})();

View File

@@ -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);

View File

@@ -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();

View File

@@ -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');
};

View File

@@ -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);
});
})();