--- a/web/res/js/backbone.js Wed Dec 11 10:17:08 2019 +0100
+++ b/web/res/js/backbone.js Wed Dec 11 11:02:15 2019 +0100
@@ -1,6 +1,6 @@
-// Backbone.js 1.2.3
+// Backbone.js 1.4.0
-// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
@@ -9,8 +9,8 @@
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
- var root = (typeof self == 'object' && self.self == self && self) ||
- (typeof global == 'object' && global.global == global && global);
+ var root = typeof self == 'object' && self.self === self && self ||
+ typeof global == 'object' && global.global === global && global;
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
@@ -23,15 +23,15 @@
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
- try { $ = require('jquery'); } catch(e) {}
+ try { $ = require('jquery'); } catch (e) {}
factory(root, exports, _, $);
// Finally, as a browser global.
} else {
- root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
+ root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$);
}
-}(function(root, Backbone, _, $) {
+})(function(root, Backbone, _, $) {
// Initial Setup
// -------------
@@ -44,7 +44,7 @@
var slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
- Backbone.VERSION = '1.2.3';
+ Backbone.VERSION = '1.4.0';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
@@ -68,54 +68,6 @@
// 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
// ---------------
@@ -134,6 +86,9 @@
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
+ // A private global variable to share between listeners and listenees.
+ var _listening;
+
// 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}`).
@@ -146,7 +101,7 @@
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
- // Handle space separated event names by delegating them individually.
+ // Handle space-separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
@@ -160,43 +115,47 @@
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
Events.on = function(name, callback, context) {
- return internalOn(this, name, callback, context);
- };
-
- // Guard the `listening` argument from the public API.
- var internalOn = function(obj, name, callback, context, listening) {
- obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
- context: context,
- ctx: obj,
- listening: listening
+ this._events = eventsApi(onApi, this._events || {}, name, callback, {
+ context: context,
+ ctx: this,
+ listening: _listening
});
- if (listening) {
- var listeners = obj._listeners || (obj._listeners = {});
- listeners[listening.id] = listening;
+ if (_listening) {
+ var listeners = this._listeners || (this._listeners = {});
+ listeners[_listening.id] = _listening;
+ // Allow the listening to use a counter, instead of tracking
+ // callbacks for library interop
+ _listening.interop = false;
}
- return obj;
+ return this;
};
// 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
// for easier unbinding later.
- Events.listenTo = function(obj, name, callback) {
+ Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
- var listening = listeningTo[id];
+ var listening = _listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
- var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
- listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
+ this._listenId || (this._listenId = _.uniqueId('l'));
+ listening = _listening = listeningTo[id] = new Listening(this, obj);
}
- // Bind callbacks on obj, and keep track of them on listening.
- internalOn(obj, name, callback, this, listening);
+ // Bind callbacks on obj.
+ var error = tryCatchOn(obj, name, callback, this);
+ _listening = void 0;
+
+ if (error) throw error;
+ // If the target obj is not Backbone.Events, track events manually.
+ if (listening.interop) listening.on(name, callback);
+
return this;
};
@@ -207,32 +166,42 @@
var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
- handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
+ handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};
+ // An try-catch guarded #on function, to prevent poisoning the global
+ // `_listening` variable.
+ var tryCatchOn = function(obj, name, callback, context) {
+ try {
+ obj.on(name, callback, context);
+ } catch (e) {
+ return e;
+ }
+ };
+
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
- Events.off = function(name, callback, context) {
+ Events.off = function(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
- context: context,
- listeners: this._listeners
+ context: context,
+ listeners: this._listeners
});
+
return this;
};
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
- Events.stopListening = function(obj, name, callback) {
+ Events.stopListening = function(obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
var ids = obj ? [obj._listenId] : _.keys(listeningTo);
-
for (var i = 0; i < ids.length; i++) {
var listening = listeningTo[ids[i]];
@@ -241,6 +210,7 @@
if (!listening) break;
listening.obj.off(name, callback, this);
+ if (listening.interop) listening.off(name, callback);
}
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
@@ -251,21 +221,18 @@
var offApi = function(events, name, callback, options) {
if (!events) return;
- var i = 0, listening;
var context = options.context, listeners = options.listeners;
+ var i = 0, names;
- // Delete all events listeners and "drop" events.
- if (!name && !callback && !context) {
- var ids = _.keys(listeners);
- for (; i < ids.length; i++) {
- listening = listeners[ids[i]];
- delete listeners[listening.id];
- delete listening.listeningTo[listening.objId];
+ // Delete all event listeners and "drop" events.
+ if (!name && !context && !callback) {
+ for (names = _.keys(listeners); i < names.length; i++) {
+ listeners[names[i]].cleanup();
}
return;
}
- var names = name ? [name] : _.keys(events);
+ names = name ? [name] : _.keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
@@ -273,7 +240,7 @@
// Bail out if there are no events stored.
if (!handlers) break;
- // Replace events if there are any remaining. Otherwise, clean up.
+ // Find any remaining events.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
@@ -284,38 +251,37 @@
) {
remaining.push(handler);
} else {
- listening = handler.listening;
- if (listening && --listening.count === 0) {
- delete listeners[listening.id];
- delete listening.listeningTo[listening.objId];
- }
+ var listening = handler.listening;
+ if (listening) listening.off(name, callback);
}
}
- // Update tail event if the list has any events. Otherwise, clean up.
+ // Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
- if (_.size(events)) return events;
+
+ return events;
};
// Bind an event to only be triggered a single time. After the first time
// 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) {
+ Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
- var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
- return this.on(events, void 0, context);
+ var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
+ if (typeof name === 'string' && context == null) callback = void 0;
+ return this.on(events, callback, context);
};
// Inversion-of-control versions of `once`.
- Events.listenToOnce = function(obj, name, callback) {
+ Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
- var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
+ var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
return this.listenTo(obj, events);
};
@@ -336,7 +302,7 @@
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
- Events.trigger = function(name) {
+ Events.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
@@ -348,7 +314,7 @@
};
// Handles triggering the appropriate event callbacks.
- var triggerApi = function(objEvents, name, cb, args) {
+ var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
@@ -373,6 +339,44 @@
}
};
+ // A listening class that tracks and cleans up memory bindings
+ // when all callbacks have been offed.
+ var Listening = function(listener, obj) {
+ this.id = listener._listenId;
+ this.listener = listener;
+ this.obj = obj;
+ this.interop = true;
+ this.count = 0;
+ this._events = void 0;
+ };
+
+ Listening.prototype.on = Events.on;
+
+ // Offs a callback (or several).
+ // Uses an optimized counter if the listenee uses Backbone.Events.
+ // Otherwise, falls back to manual tracking to support events
+ // library interop.
+ Listening.prototype.off = function(name, callback) {
+ var cleanup;
+ if (this.interop) {
+ this._events = eventsApi(offApi, this._events, name, callback, {
+ context: void 0,
+ listeners: void 0
+ });
+ cleanup = !this._events;
+ } else {
+ this.count--;
+ cleanup = this.count === 0;
+ }
+ if (cleanup) this.cleanup();
+ };
+
+ // Cleans up memory bindings between the listener and the listenee.
+ Listening.prototype.cleanup = function() {
+ delete this.listener._listeningTo[this.obj._listenId];
+ if (!this.interop) delete this.obj._listeners[this.id];
+ };
+
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
@@ -394,11 +398,13 @@
var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {};
options || (options = {});
+ this.preinitialize.apply(this, arguments);
this.cid = _.uniqueId(this.cidPrefix);
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
- attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
+ var defaults = _.result(this, 'defaults');
+ attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
@@ -421,6 +427,10 @@
// You may want to override this if you're experiencing name clashes with model ids.
cidPrefix: 'c',
+ // preinitialize is an empty function by default. You can override it with a function
+ // or object. preinitialize will run before any instantiation logic is run in the Model.
+ preinitialize: function(){},
+
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
@@ -506,7 +516,7 @@
}
// Update the `id`.
- this.id = this.get(this.idAttribute);
+ if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
// Trigger all relevant attribute changes.
if (!silent) {
@@ -561,12 +571,14 @@
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
+ var hasChanged;
for (var attr in diff) {
var val = diff[attr];
if (_.isEqual(old[attr], val)) continue;
changed[attr] = val;
+ hasChanged = true;
}
- return _.size(changed) ? changed : false;
+ return hasChanged ? changed : false;
},
// Get the previous value of an attribute, recorded at the time the last
@@ -619,8 +631,8 @@
// the model will be valid when the attributes, if any, are set.
if (attrs && !wait) {
if (!this.set(attrs, options)) return false;
- } else {
- if (!this._validate(attrs, options)) return false;
+ } else if (!this._validate(attrs, options)) {
+ return false;
}
// After a successful server-side save, the client is (optionally)
@@ -642,7 +654,7 @@
// 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');
+ var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
if (method === 'patch' && !options.attrs) options.attrs = attrs;
var xhr = this.sync(method, this, options);
@@ -714,7 +726,7 @@
// Check if the model is currently in a valid state.
isValid: function(options) {
- return this._validate({}, _.defaults({validate: true}, options));
+ return this._validate({}, _.extend({}, options, {validate: true}));
},
// Run validation against the next complete set of model attributes,
@@ -730,14 +742,6 @@
});
- // 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 };
-
- // Mix in each Underscore method as a proxy to `Model#attributes`.
- addUnderscoreMethods(Model, modelMethods, 'attributes');
-
// Backbone.Collection
// -------------------
@@ -753,6 +757,7 @@
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
+ this.preinitialize.apply(this, arguments);
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset();
@@ -769,7 +774,8 @@
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];
+ var i;
+ for (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];
};
@@ -781,6 +787,11 @@
// This should be overridden in most cases.
model: Model,
+
+ // preinitialize is an empty function by default. You can override it with a function
+ // or object. preinitialize will run before any instantiation logic is run in the Collection.
+ preinitialize: function(){},
+
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
@@ -807,9 +818,12 @@
remove: function(models, options) {
options = _.extend({}, options);
var singular = !_.isArray(models);
- models = singular ? [models] : _.clone(models);
+ models = singular ? [models] : models.slice();
var removed = this._removeModels(models, options);
- if (!options.silent && removed) this.trigger('update', this, options);
+ if (!options.silent && removed.length) {
+ options.changes = {added: [], merged: [], removed: removed};
+ this.trigger('update', this, options);
+ }
return singular ? removed[0] : removed;
},
@@ -820,18 +834,22 @@
set: function(models, options) {
if (models == null) return;
- options = _.defaults({}, options, setOptions);
- if (options.parse && !this._isModel(models)) models = this.parse(models, options);
+ options = _.extend({}, setOptions, options);
+ if (options.parse && !this._isModel(models)) {
+ models = this.parse(models, options) || [];
+ }
var singular = !_.isArray(models);
models = singular ? [models] : models.slice();
var at = options.at;
if (at != null) at = +at;
+ if (at > this.length) at = this.length;
if (at < 0) at += this.length + 1;
var set = [];
var toAdd = [];
+ var toMerge = [];
var toRemove = [];
var modelMap = {};
@@ -840,13 +858,13 @@
var remove = options.remove;
var sort = false;
- var sortable = this.comparator && (at == null) && options.sort !== false;
+ var sortable = this.comparator && at == null && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
// Turn bare objects into model references, and prevent invalid models
// from being added.
- var model;
- for (var i = 0; i < models.length; i++) {
+ var model, i;
+ for (i = 0; i < models.length; i++) {
model = models[i];
// If a duplicate is found, prevent it from being added and
@@ -857,6 +875,7 @@
var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
+ toMerge.push(existing);
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
@@ -890,8 +909,8 @@
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];
+ orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
+ return m !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
@@ -905,7 +924,7 @@
// Silently sort the collection if appropriate.
if (sort) this.sort({silent: true});
- // Unless silenced, it's time to fire all appropriate add/sort events.
+ // Unless silenced, it's time to fire all appropriate add/sort/update events.
if (!options.silent) {
for (i = 0; i < toAdd.length; i++) {
if (at != null) options.index = at + i;
@@ -913,7 +932,14 @@
model.trigger('add', model, this, options);
}
if (sort || orderChanged) this.trigger('sort', this, options);
- if (toAdd.length || toRemove.length) this.trigger('update', this, options);
+ if (toAdd.length || toRemove.length || toMerge.length) {
+ options.changes = {
+ added: toAdd,
+ removed: toRemove,
+ merged: toMerge
+ };
+ this.trigger('update', this, options);
+ }
}
// Return the added (or merged) model (or models).
@@ -963,11 +989,18 @@
return slice.apply(this.models, arguments);
},
- // Get a model from the set by id.
+ // Get a model from the set by id, cid, model object with id or cid
+ // properties, or an attributes object that is transformed through modelId.
get: function(obj) {
if (obj == null) return void 0;
- var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
- return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
+ return this._byId[obj] ||
+ this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] ||
+ obj.cid && this._byId[obj.cid];
+ },
+
+ // Returns `true` if the model is in the collection.
+ has: function(obj) {
+ return this.get(obj) != null;
},
// Get the model at the given index.
@@ -997,7 +1030,7 @@
options || (options = {});
var length = comparator.length;
- if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
+ if (_.isFunction(comparator)) comparator = comparator.bind(this);
// Run sort based on type of `comparator`.
if (length === 1 || _.isString(comparator)) {
@@ -1011,7 +1044,7 @@
// Pluck an attribute from each model in the collection.
pluck: function(attr) {
- return _.invoke(this.models, 'get', attr);
+ return this.map(attr + '');
},
// Fetch the default set of models for this collection, resetting the
@@ -1042,9 +1075,9 @@
if (!wait) this.add(model, options);
var collection = this;
var success = options.success;
- options.success = function(model, resp, callbackOpts) {
- if (wait) collection.add(model, callbackOpts);
- if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
+ options.success = function(m, resp, callbackOpts) {
+ if (wait) collection.add(m, callbackOpts);
+ if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
};
model.save(null, options);
return model;
@@ -1065,10 +1098,25 @@
},
// Define how to uniquely identify models in the collection.
- modelId: function (attrs) {
+ modelId: function(attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
},
+ // Get an iterator of all models in this collection.
+ values: function() {
+ return new CollectionIterator(this, ITERATOR_VALUES);
+ },
+
+ // Get an iterator of all model IDs in this collection.
+ keys: function() {
+ return new CollectionIterator(this, ITERATOR_KEYS);
+ },
+
+ // Get an iterator of all [ID, model] tuples in this collection.
+ entries: function() {
+ return new CollectionIterator(this, ITERATOR_KEYSVALUES);
+ },
+
// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function() {
@@ -1103,6 +1151,12 @@
this.models.splice(index, 1);
this.length--;
+ // Remove references before triggering 'remove' event to prevent an
+ // infinite loop. #3693
+ delete this._byId[model.cid];
+ var id = this.modelId(model.attributes);
+ if (id != null) delete this._byId[id];
+
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
@@ -1111,12 +1165,12 @@
removed.push(model);
this._removeReference(model, options);
}
- return removed.length ? removed : false;
+ return removed;
},
// Method for checking whether an object should be considered a model for
// the purposes of adding to the collection.
- _isModel: function (model) {
+ _isModel: function(model) {
return model instanceof Model;
},
@@ -1142,14 +1196,16 @@
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
- if ((event === 'add' || event === 'remove') && collection !== this) return;
- if (event === 'destroy') this.remove(model, options);
- if (event === 'change') {
- var prevId = this.modelId(model.previousAttributes());
- var id = this.modelId(model.attributes);
- if (prevId !== id) {
- if (prevId != null) delete this._byId[prevId];
- if (id != null) this._byId[id] = model;
+ if (model) {
+ if ((event === 'add' || event === 'remove') && collection !== this) return;
+ if (event === 'destroy') this.remove(model, options);
+ if (event === 'change') {
+ var prevId = this.modelId(model.previousAttributes());
+ var id = this.modelId(model.attributes);
+ if (prevId !== id) {
+ if (prevId != null) delete this._byId[prevId];
+ if (id != null) this._byId[id] = model;
+ }
}
}
this.trigger.apply(this, arguments);
@@ -1157,20 +1213,71 @@
});
- // Underscore methods that we want to implement on the Collection.
- // 90% of the core usefulness of Backbone Collections is actually implemented
- // 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: 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, groupBy: 3, countBy: 3,
- sortBy: 3, indexBy: 3};
+ // Defining an @@iterator method implements JavaScript's Iterable protocol.
+ // In modern ES2015 browsers, this value is found at Symbol.iterator.
+ /* global Symbol */
+ var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
+ if ($$iterator) {
+ Collection.prototype[$$iterator] = Collection.prototype.values;
+ }
+
+ // CollectionIterator
+ // ------------------
+
+ // A CollectionIterator implements JavaScript's Iterator protocol, allowing the
+ // use of `for of` loops in modern browsers and interoperation between
+ // Backbone.Collection and other JavaScript functions and third-party libraries
+ // which can operate on Iterables.
+ var CollectionIterator = function(collection, kind) {
+ this._collection = collection;
+ this._kind = kind;
+ this._index = 0;
+ };
+
+ // This "enum" defines the three possible kinds of values which can be emitted
+ // by a CollectionIterator that correspond to the values(), keys() and entries()
+ // methods on Collection, respectively.
+ var ITERATOR_VALUES = 1;
+ var ITERATOR_KEYS = 2;
+ var ITERATOR_KEYSVALUES = 3;
- // Mix in each Underscore method as a proxy to `Collection#models`.
- addUnderscoreMethods(Collection, collectionMethods, 'models');
+ // All Iterators should themselves be Iterable.
+ if ($$iterator) {
+ CollectionIterator.prototype[$$iterator] = function() {
+ return this;
+ };
+ }
+
+ CollectionIterator.prototype.next = function() {
+ if (this._collection) {
+
+ // Only continue iterating if the iterated collection is long enough.
+ if (this._index < this._collection.length) {
+ var model = this._collection.at(this._index);
+ this._index++;
+
+ // Construct a value depending on what kind of values should be iterated.
+ var value;
+ if (this._kind === ITERATOR_VALUES) {
+ value = model;
+ } else {
+ var id = this._collection.modelId(model.attributes);
+ if (this._kind === ITERATOR_KEYS) {
+ value = id;
+ } else { // ITERATOR_KEYSVALUES
+ value = [id, model];
+ }
+ }
+ return {value: value, done: false};
+ }
+
+ // Once exhausted, remove the reference to the collection so future
+ // calls to the next method always return done.
+ this._collection = void 0;
+ }
+
+ return {value: void 0, done: true};
+ };
// Backbone.View
// -------------
@@ -1187,6 +1294,7 @@
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
+ this.preinitialize.apply(this, arguments);
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
@@ -1210,6 +1318,10 @@
return this.$el.find(selector);
},
+ // preinitialize is an empty function by default. You can override it with a function
+ // or object. preinitialize will run before any instantiation logic is run in the View
+ preinitialize: function(){},
+
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
@@ -1277,7 +1389,7 @@
if (!_.isFunction(method)) method = this[method];
if (!method) continue;
var match = key.match(delegateEventSplitter);
- this.delegate(match[1], match[2], _.bind(method, this));
+ this.delegate(match[1], match[2], method.bind(this));
}
return this;
},
@@ -1335,6 +1447,94 @@
});
+ // 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(base, length, method, attribute) {
+ switch (length) {
+ case 1: return function() {
+ return base[method](this[attribute]);
+ };
+ case 2: return function(value) {
+ return base[method](this[attribute], value);
+ };
+ case 3: return function(iteratee, context) {
+ return base[method](this[attribute], cb(iteratee, this), context);
+ };
+ case 4: return function(iteratee, defaultVal, context) {
+ return base[method](this[attribute], cb(iteratee, this), defaultVal, context);
+ };
+ default: return function() {
+ var args = slice.call(arguments);
+ args.unshift(this[attribute]);
+ return base[method].apply(base, args);
+ };
+ }
+ };
+
+ var addUnderscoreMethods = function(Class, base, methods, attribute) {
+ _.each(methods, function(length, method) {
+ if (base[method]) Class.prototype[method] = addMethod(base, 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);
+ };
+ };
+
+ // Underscore methods that we want to implement on the Collection.
+ // 90% of the core usefulness of Backbone Collections is actually implemented
+ // right here:
+ var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
+ foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 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, groupBy: 3, countBy: 3,
+ sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};
+
+
+ // 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};
+
+ // Mix in each Underscore method as a proxy to `Collection#models`.
+
+ _.each([
+ [Collection, collectionMethods, 'models'],
+ [Model, modelMethods, 'attributes']
+ ], function(config) {
+ var Base = config[0],
+ methods = config[1],
+ attribute = config[2];
+
+ Base.mixin = function(obj) {
+ var mappings = _.reduce(_.functions(obj), function(memo, name) {
+ memo[name] = 0;
+ return memo;
+ }, {});
+ addUnderscoreMethods(Base, obj, mappings, attribute);
+ };
+
+ addUnderscoreMethods(Base, _, methods, attribute);
+ });
+
// Backbone.sync
// -------------
@@ -1415,11 +1615,11 @@
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
- 'create': 'POST',
- 'update': 'PUT',
- 'patch': 'PATCH',
- 'delete': 'DELETE',
- 'read': 'GET'
+ create: 'POST',
+ update: 'PUT',
+ patch: 'PATCH',
+ delete: 'DELETE',
+ read: 'GET'
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
@@ -1435,6 +1635,7 @@
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
+ this.preinitialize.apply(this, arguments);
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
@@ -1450,6 +1651,10 @@
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
+ // preinitialize is an empty function by default. You can override it with a function
+ // or object. preinitialize will run before any instantiation logic is run in the Router.
+ preinitialize: function(){},
+
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
@@ -1507,11 +1712,11 @@
// against the current location hash.
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
- .replace(optionalParam, '(?:$1)?')
- .replace(namedParam, function(match, optional) {
- return optional ? match : '([^/?]+)';
- })
- .replace(splatParam, '([^?]*?)');
+ .replace(optionalParam, '(?:$1)?')
+ .replace(namedParam, function(match, optional) {
+ return optional ? match : '([^/?]+)';
+ })
+ .replace(splatParam, '([^?]*?)');
return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
},
@@ -1539,7 +1744,7 @@
// falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
- this.checkUrl = _.bind(this.checkUrl, this);
+ this.checkUrl = this.checkUrl.bind(this);
// Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') {
@@ -1576,8 +1781,8 @@
// Does the pathname match the root?
matchRoot: function() {
var path = this.decodeFragment(this.location.pathname);
- var root = path.slice(0, this.root.length - 1) + '/';
- return root === this.root;
+ var rootPath = path.slice(0, this.root.length - 1) + '/';
+ return rootPath === this.root;
},
// Unicode characters in `location.pathname` are percent encoded so they're
@@ -1649,8 +1854,8 @@
// If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !this.atRoot()) {
- var root = this.root.slice(0, -1) || '/';
- this.location.replace(root + '#' + this.getPath());
+ var rootPath = this.root.slice(0, -1) || '/';
+ this.location.replace(rootPath + '#' + this.getPath());
// Return immediately as browser will do redirect to new url
return true;
@@ -1679,7 +1884,7 @@
}
// Add a cross-platform `addEventListener` shim for older browsers.
- var addEventListener = window.addEventListener || function (eventName, listener) {
+ var addEventListener = window.addEventListener || function(eventName, listener) {
return attachEvent('on' + eventName, listener);
};
@@ -1700,7 +1905,7 @@
// but possibly useful for unit testing Routers.
stop: function() {
// Add a cross-platform `removeEventListener` shim for older browsers.
- var removeEventListener = window.removeEventListener || function (eventName, listener) {
+ var removeEventListener = window.removeEventListener || function(eventName, listener) {
return detachEvent('on' + eventName, listener);
};
@@ -1774,17 +1979,20 @@
fragment = this.getFragment(fragment || '');
// Don't include a trailing slash on the root.
- var root = this.root;
+ var rootPath = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
- root = root.slice(0, -1) || '/';
+ rootPath = rootPath.slice(0, -1) || '/';
}
- var url = root + fragment;
+ var url = rootPath + fragment;
- // Strip the hash and decode for matching.
- fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
+ // Strip the fragment of the query and hash for matching.
+ fragment = fragment.replace(pathStripper, '');
- if (this.fragment === fragment) return;
- this.fragment = fragment;
+ // Decode for matching.
+ var decodedFragment = this.decodeFragment(fragment);
+
+ if (this.fragment === decodedFragment) return;
+ this.fragment = decodedFragment;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._usePushState) {
@@ -1794,7 +2002,7 @@
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
- if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
+ 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
@@ -1856,14 +2064,9 @@
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
- // `parent` constructor function.
- var Surrogate = function(){ this.constructor = child; };
- Surrogate.prototype = parent.prototype;
- child.prototype = new Surrogate;
-
- // Add prototype properties (instance properties) to the subclass,
- // if supplied.
- if (protoProps) _.extend(child.prototype, protoProps);
+ // `parent`'s constructor function and add the prototype properties.
+ child.prototype = _.create(parent.prototype, protoProps);
+ child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed
// later.
@@ -1890,5 +2093,4 @@
};
return Backbone;
-
-}));
+});