Merged in 1.4.0

This commit is contained in:
jashkenas
2019-02-19 10:34:22 -08:00
18 changed files with 1978 additions and 989 deletions

View File

@@ -5,11 +5,11 @@
"amd": true
},
"globals": {
"attachEvent": true,
"detachEvent": true
"attachEvent": false,
"detachEvent": false
},
"rules": {
"array-bracket-spacing": [2],
"array-bracket-spacing": 2,
"block-scoped-var": 2,
"brace-style": [1, "1tbs", {"allowSingleLine": true}],
"camelcase": 2,
@@ -21,6 +21,7 @@
"eqeqeq": [2, "smart"],
"indent": [2, 2, {"SwitchCase": 1, "VariableDeclarator": 2}],
"key-spacing": 1,
"keyword-spacing": [2, { "after": true }],
"linebreak-style": 2,
"max-depth": [1, 4],
"max-params": [1, 5],
@@ -37,7 +38,6 @@
"no-duplicate-case": 2,
"no-else-return": 1,
"no-empty-character-class": 2,
"no-empty-label": 2,
"no-eval": 2,
"no-ex-assign": 2,
"no-extend-native": 2,
@@ -51,6 +51,7 @@
"no-inner-declarations": 2,
"no-irregular-whitespace": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-lonely-if": 2,
"no-multi-str": 2,
@@ -79,10 +80,8 @@
"quotes": [2, "single", "avoid-escape"],
"radix": 2,
"semi": 2,
"space-after-keywords": [2, "always"],
"space-before-function-paren": [2, {"anonymous": "never", "named": "never"}],
"space-infix-ops": 2,
"space-return-throw-case": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"use-isnan": 2,
"valid-typeof": 2,

View File

@@ -1,6 +1,6 @@
language: node_js
node_js:
- "0.10"
- "6"
before_install:
- npm install -g npm
- npm install -g karma-cli

View File

@@ -1,4 +1,4 @@
Copyright (c) 2010-2016 Jeremy Ashkenas, DocumentCloud
Copyright (c) 2010-2019 Jeremy Ashkenas, DocumentCloud
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation

2
backbone-min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
// Backbone.js 1.3.3
// Backbone.js 1.4.0
// (c) 2010-2016 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) {
@@ -28,7 +28,7 @@
// 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, _, $) {
@@ -44,7 +44,7 @@
var slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.3.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}`).
@@ -160,23 +115,21 @@
// 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, {
this._events = eventsApi(onApi, this._events || {}, name, callback, {
context: context,
ctx: obj,
listening: listening
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
@@ -186,17 +139,23 @@
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;
};
@@ -212,6 +171,16 @@
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
@@ -222,6 +191,7 @@
context: context,
listeners: this._listeners
});
return this;
};
@@ -232,7 +202,6 @@
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,7 +210,9 @@
if (!listening) break;
listening.obj.off(name, callback, this);
if (listening.interop) listening.off(name, callback);
}
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
};
@@ -250,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];
@@ -272,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];
@@ -283,21 +251,19 @@
) {
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];
}
}
return events;
};
@@ -307,7 +273,7 @@
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
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);
};
@@ -315,7 +281,7 @@
// Inversion-of-control versions of `once`.
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);
};
@@ -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,6 +398,7 @@
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;
@@ -422,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(){},
@@ -562,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
@@ -643,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);
@@ -731,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
// -------------------
@@ -754,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();
@@ -783,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(){},
@@ -985,7 +994,7 @@
get: function(obj) {
if (obj == null) return void 0;
return this._byId[obj] ||
this._byId[this.modelId(obj.attributes || obj)] ||
this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] ||
obj.cid && this._byId[obj.cid];
},
@@ -1021,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)) {
@@ -1093,6 +1102,21 @@
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() {
@@ -1189,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: 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};
// 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;
}
// Mix in each Underscore method as a proxy to `Collection#models`.
addUnderscoreMethods(Collection, collectionMethods, 'models');
// 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;
// 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
// -------------
@@ -1219,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);
@@ -1242,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(){},
@@ -1309,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;
},
@@ -1367,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
// -------------
@@ -1447,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 `$`.
@@ -1467,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);
@@ -1482,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(){},
@@ -1539,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]*))?$');
},
@@ -1571,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') {
@@ -1812,11 +1985,14 @@
}
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) {

File diff suppressed because it is too large Load Diff

View File

@@ -303,7 +303,7 @@
<div id="sidebar" class="interface">
<a class="toc_title" href="#">
Backbone.js <span class="version">(1.3.3)</span>
Backbone.js <span class="version">(1.4.0)</span>
</a>
<ul class="toc_section">
<li>&raquo; <a href="http://github.com/jashkenas/backbone">GitHub Repository</a></li>
@@ -348,6 +348,7 @@
</a>
<ul class="toc_section">
<li data-name="extend"> <a href="#Model-extend">extend</a></li>
<li data-name="preinitialize"> <a href="#Model-preinitialize">preinitialize</a></li>
<li data-name="constructor / initialize"> <a href="#Model-constructor">constructor / initialize</a></li>
<li data-name="get"> <a href="#Model-get">get</a></li>
<li data-name="set"> <a href="#Model-set">set</a></li>
@@ -390,6 +391,7 @@
<li data-name="extend"> <a href="#Collection-extend">extend</a></li>
<li data-name="model"> <a href="#Collection-model">model</a></li>
<li data-name="modelId"> <a href="#Collection-modelId">modelId</a></li>
<li data-name="preinitialize" data-name="preinitialize"> <a href="#Collection-preinitialize">preinitialize</a></li>
<li data-name="constructor / initialize" data-name="constructor / initialize"> <a href="#Collection-constructor">constructor / initialize</a></li>
<li data-name="models"> <a href="#Collection-models">models</a></li>
<li data-name="toJSON"> <a href="#Collection-toJSON">toJSON</a></li>
@@ -417,6 +419,7 @@
<li data-name="clone"> <a href="#Collection-clone">clone</a></li>
<li data-name="fetch"> <a href="#Collection-fetch">fetch</a></li>
<li data-name="create"> <a href="#Collection-create">create</a></li>
<li data-name="sync"> <a href="#Collection-mixin">mixin</a></li>
</ul>
</div>
@@ -427,6 +430,7 @@
<ul class="toc_section">
<li data-name="extend"> <a href="#Router-extend">extend</a></li>
<li data-name="routes"> <a href="#Router-routes">routes</a></li>
<li data-name="preinitialize"> <a href="#Router-preinitialize">preinitialize</a></li>
<li data-name="constructor / initialize"> <a href="#Router-constructor">constructor / initialize</a></li>
<li data-name="route"> <a href="#Router-route">route</a></li>
<li data-name="navigate"> <a href="#Router-navigate">navigate</a></li>
@@ -461,6 +465,7 @@
</a>
<ul class="toc_section">
<li data-name="extend"> <a href="#View-extend">extend</a></li>
<li data-name="preinitialize"> <a href="#View-preinitialize">preinitialize</a></li>
<li data-name="constructor / initialize"> <a href="#View-constructor">constructor / initialize</a></li>
<li data-name="el"> <a href="#View-el">el</a></li>
<li data-name="$el"> <a href="#View-$el">$el</a></li>
@@ -588,10 +593,7 @@
<p>
You can report bugs and discuss features on the
<a href="http://github.com/jashkenas/backbone/issues">GitHub issues page</a>,
on Freenode IRC in the <tt>#documentcloud</tt> channel, post questions to the
<a href="https://groups.google.com/forum/#!forum/backbonejs">Google Group</a>,
add pages to the <a href="https://github.com/jashkenas/backbone/wiki">wiki</a>
or send tweets to <a href="http://twitter.com/documentcloud">@documentcloud</a>.
or add pages to the <a href="https://github.com/jashkenas/backbone/wiki">wiki</a>.
</p>
<p>
@@ -614,7 +616,7 @@
<tr>
<td><a class="punch" href="backbone-min.js">Production Version (1.3.3)</a></td>
<td class="text" style="line-height: 16px;">
<i>7.6kb, Packed and gzipped</i><br />
<i>7.9kb, Packed and gzipped</i><br />
<small>(<a href="backbone-min.map">Source Map</a>)</small>
</td>
</tr>
@@ -622,9 +624,6 @@
<td><a class="punch" href="https://raw.github.com/jashkenas/backbone/master/backbone.js">Edge Version (master)</a></td>
<td>
<i>Unreleased, use at your own risk</i>
<a class="travis-badge" href="https://travis-ci.org/jashkenas/backbone">
<img src="https://travis-ci.org/jashkenas/backbone.png" />
</a>
</td>
</tr>
</table>
@@ -633,9 +632,7 @@
Backbone's only hard dependency is
<a href="http://underscorejs.org/">Underscore.js</a> <small>( >= 1.8.3)</small>.
For RESTful persistence and DOM manipulation with <a href="#View">Backbone.View</a>,
include <b><a href="https://jquery.com/">jQuery</a></b> ( >= 1.11.0), and
<b><a href="https://github.com/douglascrockford/JSON-js">json2.js</a></b> for older
Internet Explorer support.
include <b><a href="https://jquery.com/">jQuery</a></b> ( >= 1.11.0).
<i>(Mimics of the Underscore and jQuery APIs, such as
<a href="https://lodash.com/">Lodash</a> and
<a href="http://zeptojs.com/">Zepto</a>, will
@@ -704,7 +701,7 @@
<b>Model</b>
<ul>
<li>Orchestrates data and business logic.</li>
<li>Loads and saves from the server.</li>
<li>Loads and saves data from the server.</li>
<li>Emits events when data changes.</li>
</ul>
</div>
@@ -1031,7 +1028,7 @@ view.stopListening(model);
<ul class="small">
<li><b>"add"</b> (model, collection, options) &mdash; when a model is added to a collection.</li>
<li><b>"remove"</b> (model, collection, options) &mdash; when a model is removed from a collection.</li>
<li><b>"update"</b> (collection, options) &mdash; single event triggered after any number of models have been added or removed from a collection.</li>
<li><b>"update"</b> (collection, options) &mdash; single event triggered after any number of models have been added, removed or changed in a collection.</li>
<li><b>"reset"</b> (collection, options) &mdash; when the collection's entire contents have been <a href="#Collection-reset">reset</a>.</li>
<li><b>"sort"</b> (collection, options) &mdash; when the collection has been re-sorted.</li>
<li><b>"change"</b> (model, options) &mdash; when a model's attributes have changed.</li>
@@ -1039,7 +1036,7 @@ view.stopListening(model);
<li><b>"destroy"</b> (model, collection, options) &mdash; when a model is <a href="#Model-destroy">destroyed</a>.</li>
<li><b>"request"</b> (model_or_collection, xhr, options) &mdash; when a model or collection has started a request to the server.</li>
<li><b>"sync"</b> (model_or_collection, response, options) &mdash; when a model or collection has been successfully synced with the server.</li>
<li><b>"error"</b> (model_or_collection, response, options) &mdash; when a model's or collection's request to the server has failed.</li>
<li><b>"error"</b> (model_or_collection, xhr, options) &mdash; when a model's or collection's request to the server has failed.</li>
<li><b>"invalid"</b> (model, error, options) &mdash; when a model's <a href="#Model-validate">validation</a> fails on the client.</li>
<li><b>"route:[name]"</b> (params) &mdash; Fired by the router when a specific route is matched.</li>
<li><b>"route"</b> (route, params) &mdash; Fired by the router when <i>any</i> route has been matched.</li>
@@ -1146,6 +1143,24 @@ var Note = Backbone.Model.extend({
...
}
});
</pre>
<p id="Model-preinitialize">
<b class="header">preinitialize</b><code>new Model([attributes], [options])</code>
<br />
For use with models as ES classes. If you define a <b>preinitialize</b>
method, it will be invoked when the Model is first created, before any
instantiation logic is run for the Model.
</p>
<pre>
class Country extends Backbone.Model {
preinitialize({countryCode}) {
this.name = COUNTRY_NAMES[countryCode];
}
initialize() { ... }
}
</pre>
<p id="Model-constructor">
@@ -1272,6 +1287,8 @@ if (note.has("title")) {
A special property of models, the <b>id</b> is an arbitrary string
(integer id or UUID). If you set the <b>id</b> in the
attributes hash, it will be copied onto the model as a direct property.
<code>model.id</code> should not be manipulated directly,
it should be modified only via <code>model.set('id', …)</code>.
Models can be retrieved by id from collections, and the id is used to generate
model URLs by default.
</p>
@@ -1531,21 +1548,22 @@ chapters.keys().join(', ');
<br />
This method is left undefined and you're encouraged to override it with
any custom validation logic you have that can be performed in JavaScript.
If the attributes are valid, don't return anything from <b>validate</b>;
if they are invalid return an error of your choosing. It can be as
simple as a string error message to be displayed, or a complete error
object that describes the error programmatically.
</p>
<p>
By default <tt>save</tt> checks <b>validate</b> before
setting any attributes but you may also tell <tt>set</tt> to validate
the new attributes by passing <tt>{validate: true}</tt> as an option.
<br />
The <b>validate</b> method receives the model attributes as well as any
options passed to <tt>set</tt> or <tt>save</tt>.
If the attributes are valid, don't return anything from <b>validate</b>;
if they are invalid return an error of your choosing. It
can be as simple as a string error message to be displayed, or a complete
error object that describes the error programmatically. If <b>validate</b>
returns an error, <tt>save</tt> will not continue, and the
model attributes will not be modified on the server.
Failed validations trigger an <tt>"invalid"</tt> event, and set the
<tt>validationError</tt> property on the model with the value returned by
this method.
options passed to <tt>set</tt> or <tt>save</tt>, if <b>validate</b>
returns an error, <tt>save</tt> does not continue, the model attributes
are not modified on the server, an <tt>"invalid"</tt> event is triggered,
and the <tt>validationError</tt> property is set on the model with the
value returned by this method.
</p>
<pre class="runnable">
@@ -1583,11 +1601,18 @@ one.save({
</p>
<p id="Model-isValid">
<b class="header">isValid</b><code>model.isValid()</code>
<b class="header">isValid</b><code>model.isValid(options)</code>
<br />
Run <a href="#Model-validate">validate</a> to check the model state.
</p>
<p>
The <tt>validate</tt> method receives the model attributes as well as any
options passed to <b>isValid</b>, if <tt>validate</tt> returns an error
an <tt>"invalid"</tt> event is triggered, and the error is set on the
model in the <tt>validationError</tt> property.
</p>
<pre class="runnable">
var Chapter = Backbone.Model.extend({
validate: function(attrs, options) {
@@ -1771,11 +1796,11 @@ bill.set({name : "Bill Jones"});
<p id="Collection-model">
<b class="header">model</b><code>collection.model([attrs], [options])</code>
<br />
Override this property to specify the model class that the collection
contains. If defined, you can pass raw attributes objects (and arrays) to
Override this property to specify the model class that the collection contains.
If defined, you can pass raw attributes objects (and arrays) and options to
<a href="#Collection-add">add</a>, <a href="#Collection-create">create</a>,
and <a href="#Collection-reset">reset</a>, and the attributes will be
converted into a model of the proper type.
converted into a model of the proper type using the provided options, if any.
</p>
<pre>
@@ -1836,6 +1861,24 @@ var library = new Library([
var dvdId = library.get('dvd1').id;
var vhsId = library.get('vhs1').id;
alert('dvd: ' + dvdId + ', vhs: ' + vhsId);
</pre>
<p id="Collection-preinitialize">
<b class="header">preinitialize</b><code>new Backbone.Collection([models], [options])</code>
<br />
For use with collections as ES classes. If you define a <b>preinitialize</b>
method, it will be invoked when the Collection is first created and before
any instantiation logic is run for the Collection.
</p>
<pre>
class Library extends Backbone.Collection {
preinitialize() {
this.on("add", function() {
console.log("Add model event got fired!");
};
}
}
</pre>
<p id="Collection-constructor">
@@ -1966,7 +2009,8 @@ var randomThree = books.sample(3);
<br />
Add a model (or an array of models) to the collection, firing an <tt>"add"</tt>
event for each model, and an <tt>"update"</tt> event afterwards. If a <a href="#Collection-model">model</a> property is defined, you may also pass
raw attributes objects, and have them be vivified as instances of the model.
raw attributes objects and options, and have them be vivified as instances of the model using
the provided options.
Returns the added (or preexisting, if duplicate) models.
Pass <tt>{at: index}</tt> to splice the model into the collection at the
specified <tt>index</tt>. If you're adding models to the collection that are
@@ -2128,14 +2172,13 @@ var book = library.get(110);
<b class="header">comparator</b><code>collection.comparator</code>
<br />
By default there is no <b>comparator</b> for a collection.
If you define a comparator, it will be used to maintain
the collection in sorted order. This means that as models are added,
they are inserted at the correct index in <tt>collection.models</tt>.
If you define a comparator, it will be used to sort the collection any
time a model is added.
A comparator can be defined as a
<a href="http://underscorejs.org/#sortBy">sortBy</a>
(pass a function that takes a single argument),
as a
<a href="https://developer.mozilla.org/JavaScript/Reference/Global_Objects/Array/sort">sort</a>
<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">sort</a>
(pass a comparator function that expects two arguments),
or as a string indicating the attribute to sort by.
</p>
@@ -2178,9 +2221,9 @@ alert(chapters.pluck('title'));
<p id="Collection-sort">
<b class="header">sort</b><code>collection.sort([options])</code>
<br />
Force a collection to re-sort itself. You don't need to call this under
normal circumstances, as a collection with a <a href="#Collection-comparator">comparator</a>
will sort itself whenever a model is added. To disable sorting when adding
Force a collection to re-sort itself. Note that a collection with a
<a href="#Collection-comparator">comparator</a> will sort itself
automatically whenever a model is added. To disable sorting when adding
a model, pass <tt>{sort: false}</tt> to <tt>add</tt>. Calling <b>sort</b>
triggers a <tt>"sort"</tt> event on the collection.
</p>
@@ -2229,6 +2272,7 @@ alert(musketeers.length);
<br />
Just like <a href="#Collection-where">where</a>, but directly returns only
the first model in the collection that matches the passed <b>attributes</b>.
If no model matches returns <tt>undefined</tt>.
</p>
<p id="Collection-url">
@@ -2341,8 +2385,8 @@ accounts.fetch();
failed, the model will be unsaved, with validation errors.
In order for this to work, you should set the
<a href="#Collection-model">model</a> property of the collection.
The <b>create</b> method can accept either an attributes hash or an
existing, unsaved model object.
The <b>create</b> method can accept either an attributes hash and options to be
passed down during model instantiation or an existing, unsaved model object.
</p>
<p>
@@ -2364,6 +2408,32 @@ var othello = nypl.create({
title: "Othello",
author: "William Shakespeare"
});
</pre>
<p id="Collection-mixin">
<b class="header">mixin</b><code>Backbone.Collection.mixin(properties)</code>
<br />
<code>mixin</code> provides a way to enhance the base <b>Backbone.Collection</b>
and any collections which extend it. This can be used to add generic methods
(e.g. additional <a href="#Collection-Underscore-Methods"><b>Underscore Methods</b></a>).
</p>
<pre>
Backbone.Collection.mixin({
sum: function(models, iteratee) {
return _.reduce(models, function(s, m) {
return s + iteratee(m);
}, 0);
}
});
var cart = new Backbone.Collection([
{price: 16, name: 'monopoly'},
{price: 5, name: 'deck of cards'},
{price: 20, name: 'chess'}
]);
var cost = cart.sum('price');
</pre>
<h2 id="Router">Backbone.Router</h2>
@@ -2481,6 +2551,30 @@ routes: {
router.on("route:help", function(page) {
...
});
</pre>
<p id="Router-preinitialize">
<b class="header">preinitialize</b><code>new Backbone.Router([options])</code>
<br />
For use with routers as ES classes. If you define a <b>preinitialize</b>
method, it will be invoked when the Router is first created and before
any instantiation logic is run for the Router.
</p>
<pre>
class Router extends Backbone.Router {
preinitialize() {
// Override execute method
this.execute = function(callback, args, name) {
if (!loggedIn) {
goToLogin();
return false;
}
args.push(parseQueryString(args.pop()));
if (callback) callback.apply(this, args);
}
}
}
</pre>
<p id="Router-constructor">
@@ -2807,6 +2901,28 @@ var DocumentRow = Backbone.View.extend({
you want to wait to define them until runtime.
</p>
<p id="View-preinitialize">
<b class="header">preinitialize</b><code>new View([options])</code>
<br />
For use with views as ES classes. If you define a <b>preinitialize</b>
method, it will be invoked when the view is first created, before any
instantiation logic is run.
</p>
<pre>
class Document extends Backbone.View {
preinitialize({autoRender}) {
this.autoRender = autoRender;
}
initialize() {
if (this.autoRender) {
this.listenTo(this.model, "change", this.render);
}
}
}
</pre>
<p id="View-constructor">
<b class="header">constructor / initialize</b><code>new View([options])</code>
<br />
@@ -3336,7 +3452,7 @@ inbox.messages.fetch({reset: true});
</pre>
<p>You have to <a href="http://mathiasbynens.be/notes/etago">escape</a>
<tt>&lt;/</tt> within the JSON string, to prevent javascript injection
<tt>&lt;/</tt> within the JSON string, to prevent JavaScript injection
attacks.
<p id="FAQ-extending">
@@ -3471,9 +3587,9 @@ ActiveRecord::Base.include_root_in_json = false
<h2 id="examples">Examples</h2>
<p>
The list of examples that follows, while long, is not exhaustive. If you've
worked on an app that uses Backbone, please add it to the
<a href="https://github.com/jashkenas/backbone/wiki/Projects-and-Companies-using-Backbone">wiki page of Backbone apps</a>.
The list of examples that follows, while long, is not exhaustive — nor in
any way current. If you've worked on an app that uses Backbone, please
add it to the <a href="https://github.com/jashkenas/backbone/wiki/Projects-and-Companies-using-Backbone">wiki page of Backbone apps</a>.
</p>
<p id="examples-todos">
@@ -3595,7 +3711,7 @@ ActiveRecord::Base.include_root_in_json = false
<a href="http://earth.nullschool.net">Earth.nullschool.net</a> displays real-time weather
conditions on an interactive animated globe, and Backbone provides the
foundation upon which all of the site's components are built. Despite the
presence of several other javascript libraries, Backbone's non-opinionated
presence of several other JavaScript libraries, Backbone's non-opinionated
design made it effortless to mix-in the <a href="#Events">Events</a> functionality used for
distributing state changes throughout the page. When the decision was made
to switch to Backbone, large blocks of custom logic simply disappeared.
@@ -3918,7 +4034,7 @@ ActiveRecord::Base.include_root_in_json = false
large single-page application that
benefits from Backbone's structure and modularity. ZocDoc's Backbone
classes are tested with
<a href="http://pivotal.github.io/jasmine/">Jasmine</a>, and delivered
<a href="https://jasmine.github.io/">Jasmine</a>, and delivered
to the end user with
<a href="http://getcassette.net/">Cassette</a>.
</p>
@@ -4056,7 +4172,7 @@ ActiveRecord::Base.include_root_in_json = false
<a href="http://m.soundcloud.com">SoundCloud Mobile</a>. The project uses
the public SoundCloud <a href="http://soundcloud.com/developers">API</a>
as a data source (channeled through a nginx proxy),
<a href="http://api.jquery.com/category/plugins/templates/">jQuery templates</a>
<a href="https://github.com/BorisMoore/jquery-tmpl">jQuery templates</a>
for the rendering, <a href="http://docs.jquery.com/Qunit">Qunit
</a> and <a href="http://www.phantomjs.org/">PhantomJS</a> for
the testing suite. The JS code, templates and CSS are built for the
@@ -4326,6 +4442,32 @@ ActiveRecord::Base.include_root_in_json = false
<h2 id="changelog">Change Log</h2>
<b class="header">1.4.0</b> &mdash; <small><i>Feb. 19, 2019</i></small>
&mdash; <a href="https://github.com/jashkenas/backbone/compare/1.3.3...1.4.0">Diff</a>
&mdash; <a href="https://cdn.rawgit.com/jashkenas/backbone/1.4.0/index.html">Docs</a>
<br />
<ul style="margin-top: 5px;">
<li>
Collections now support the <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Iteration_protocols">Javascript Iterator Protocol!</a>
</li>
<li>
<tt>listenTo</tt> uses the listened object's public <tt>on</tt> method.
This helps maintain interoperability between Backbone and other event
libraries (including Node.js).
</li>
<li>
Added support for setting instance properties before the constructor in
<tt>ES2015 classes</tt> with a <tt>preinitialize</tt> method.
</li>
<li>
<tt>Collection.get</tt> now checks if obj is a <tt>Model</tt> to allow
retrieving models with an `attributes` key.
</li>
<li>
Fixed several issues with Router's URL hashing and parsing.
</li>
</ul>
<b class="header">1.3.3</b> &mdash; <small><i>Apr. 5, 2016</i></small>
&mdash; <a href="https://github.com/jashkenas/backbone/compare/1.2.3...1.3.3">Diff</a>
&mdash; <a href="https://cdn.rawgit.com/jashkenas/backbone/1.3.3/index.html">Docs</a>
@@ -4339,6 +4481,9 @@ ActiveRecord::Base.include_root_in_json = false
Added <tt>options.changes</tt> to <tt>Collection</tt> "update" event which
includes added, merged, and removed models.
</li>
<li>
Added support for <tt>Collection#mixin</tt> and <tt>Model#mixin</tt>.
</li>
<li>
Ensured <tt>Collection#reduce</tt> and <tt>Collection#reduceRight</tt>
work without an initial <tt>accumulator</tt> value.
@@ -4419,7 +4564,7 @@ ActiveRecord::Base.include_root_in_json = false
Bug fix in <tt>Collection#remove</tt>. The removed models are now actually returned.
</li>
<li>
<tt>Model#fetch</tt> no longer parses the response when passing <tt>patch: false</tt>.
<tt>Model#fetch</tt> no longer parses the response when passing <tt>parse: false</tt>.
</li>
<li>
Bug fix for iframe-based History when used with JSDOM.
@@ -4518,7 +4663,7 @@ ActiveRecord::Base.include_root_in_json = false
have actually been removed from the collection.
</li>
<li>
Fixed loading Backbone.js in strict ES6 module loaders.
Fixed loading Backbone.js in strict ES2015 module loaders.
</li>
</ul>

View File

@@ -17,10 +17,6 @@ var sauceBrowsers = _.reduce([
['internet explorer', '11', 'Windows 10'],
['internet explorer', '10', 'Windows 8'],
['internet explorer', '9', 'Windows 7'],
['internet explorer', '8'],
// Currently karma-sauce has issues with sockets and these browsers
// ['internet explorer', '7'],
// ['internet explorer', '6'],
['opera', '12'],
['opera', '11'],
@@ -30,7 +26,7 @@ var sauceBrowsers = _.reduce([
// 4.3 currently erros with some router tests
// ['android', '4.3'],
['android', '4.0'],
['safari', '8.0', 'OS X 10.10'],

View File

@@ -18,11 +18,12 @@
"devDependencies": {
"coffee-script": "1.7.1",
"docco": "0.7.0",
"eslint": "1.10.x",
"eslint": "^2.11.0",
"karma": "^0.13.13",
"karma-phantomjs-launcher": "^0.1.4",
"karma-qunit": "^0.1.5",
"qunitjs": "^1.18.0",
"karma-phantomjs-launcher": "^1.0.0",
"karma-qunit": "^1.0.0",
"phantomjs-prebuilt": "^2.1.7",
"qunitjs": "^2.0.0",
"uglify-js": "^2.4.17"
},
"scripts": {
@@ -32,7 +33,7 @@
"lint": "eslint backbone.js test/*.js"
},
"main": "backbone.js",
"version": "1.3.3",
"version": "1.4.0",
"license": "MIT",
"repository": {
"type": "git",

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
var a, b, c, d, e, col, otherCol;
@@ -593,7 +593,7 @@
assert.equal(error, 'fail');
assert.equal(options.validationError, 'fail');
});
assert.equal(collection.create({'foo': 'bar'}, {validate: true}), false);
assert.equal(collection.create({foo: 'bar'}, {validate: true}), false);
});
QUnit.test('create will pass extra options to success callback', function(assert) {
@@ -661,6 +661,31 @@
assert.equal(coll.one, 1);
});
QUnit.test('preinitialize', function(assert) {
assert.expect(1);
var Collection = Backbone.Collection.extend({
preinitialize: function() {
this.one = 1;
}
});
var coll = new Collection;
assert.equal(coll.one, 1);
});
QUnit.test('preinitialize occurs before the collection is set up', function(assert) {
assert.expect(2);
var Collection = Backbone.Collection.extend({
preinitialize: function() {
assert.notEqual(this.model, FooModel);
}
});
var FooModel = Backbone.Model.extend({id: 'foo'});
var coll = new Collection({}, {
model: FooModel
});
assert.equal(coll.model, FooModel);
});
QUnit.test('toJSON', function(assert) {
assert.expect(1);
assert.equal(JSON.stringify(col), '[{"id":3,"label":"a"},{"id":2,"label":"b"},{"id":1,"label":"c"},{"id":0,"label":"d"}]');
@@ -686,6 +711,27 @@
assert.equal(coll.findWhere({a: 4}), void 0);
});
QUnit.test('mixin', function(assert) {
Backbone.Collection.mixin({
sum: function(models, iteratee) {
return _.reduce(models, function(s, m) {
return s + iteratee(m);
}, 0);
}
});
var coll = new Backbone.Collection([
{a: 1},
{a: 1, b: 2},
{a: 2, b: 2},
{a: 3}
]);
assert.equal(coll.sum(function(m) {
return m.get('a');
}), 7);
});
QUnit.test('Underscore methods', function(assert) {
assert.expect(21);
assert.equal(col.map(function(model){ return model.get('label'); }).join(' '), 'a b c d');
@@ -1724,15 +1770,69 @@
return new M(attrs);
}
});
var c2 = new C2({'_id': 1});
var c2 = new C2({_id: 1});
assert.equal(c2.get(1), void 0);
assert.equal(c2.modelId(c2.at(0).attributes), void 0);
var m = new M({'_id': 2});
var m = new M({_id: 2});
c2.add(m);
assert.equal(c2.get(2), void 0);
assert.equal(c2.modelId(m.attributes), void 0);
});
QUnit.test('Collection implements Iterable, values is default iterator function', function(assert) {
/* global Symbol */
var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
// This test only applies to environments which define Symbol.iterator.
if (!$$iterator) {
assert.expect(0);
return;
}
assert.expect(2);
var collection = new Backbone.Collection([]);
assert.strictEqual(collection[$$iterator], collection.values);
var iterator = collection[$$iterator]();
assert.deepEqual(iterator.next(), {value: void 0, done: true});
});
QUnit.test('Collection.values iterates models in sorted order', function(assert) {
assert.expect(4);
var one = new Backbone.Model({id: 1});
var two = new Backbone.Model({id: 2});
var three = new Backbone.Model({id: 3});
var collection = new Backbone.Collection([one, two, three]);
var iterator = collection.values();
assert.strictEqual(iterator.next().value, one);
assert.strictEqual(iterator.next().value, two);
assert.strictEqual(iterator.next().value, three);
assert.strictEqual(iterator.next().value, void 0);
});
QUnit.test('Collection.keys iterates ids in sorted order', function(assert) {
assert.expect(4);
var one = new Backbone.Model({id: 1});
var two = new Backbone.Model({id: 2});
var three = new Backbone.Model({id: 3});
var collection = new Backbone.Collection([one, two, three]);
var iterator = collection.keys();
assert.strictEqual(iterator.next().value, 1);
assert.strictEqual(iterator.next().value, 2);
assert.strictEqual(iterator.next().value, 3);
assert.strictEqual(iterator.next().value, void 0);
});
QUnit.test('Collection.entries iterates ids and models in sorted order', function(assert) {
assert.expect(4);
var one = new Backbone.Model({id: 1});
var two = new Backbone.Model({id: 2});
var three = new Backbone.Model({id: 3});
var collection = new Backbone.Collection([one, two, three]);
var iterator = collection.entries();
assert.deepEqual(iterator.next().value, [1, one]);
assert.deepEqual(iterator.next().value, [2, two]);
assert.deepEqual(iterator.next().value, [3, three]);
assert.strictEqual(iterator.next().value, void 0);
});
QUnit.test('#3039 #3951: adding at index fires with correct at', function(assert) {
assert.expect(4);
var collection = new Backbone.Collection([{val: 0}, {val: 4}]);
@@ -1995,4 +2095,9 @@
assert.equal(fired, false);
});
})();
QUnit.test('get models with `attributes` key', function(assert) {
var model = {id: 1, attributes: {}};
var collection = new Backbone.Collection([model]);
assert.ok(collection.get(model));
});
})(QUnit);

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
QUnit.module('Backbone.Events');
@@ -703,4 +703,41 @@
two.trigger('y', 2);
});
})();
QUnit.test('#3611 - listenTo is compatible with non-Backbone event libraries', function(assert) {
var obj = _.extend({}, Backbone.Events);
var other = {
events: {},
on: function(name, callback) {
this.events[name] = callback;
},
trigger: function(name) {
this.events[name]();
}
};
obj.listenTo(other, 'test', function() { assert.ok(true); });
other.trigger('test');
});
QUnit.test('#3611 - stopListening is compatible with non-Backbone event libraries', function(assert) {
var obj = _.extend({}, Backbone.Events);
var other = {
events: {},
on: function(name, callback) {
this.events[name] = callback;
},
off: function() {
this.events = {};
},
trigger: function(name) {
var fn = this.events[name];
if (fn) fn();
}
};
obj.listenTo(other, 'test', function() { assert.ok(false); });
obj.stopListening(other);
other.trigger('test');
assert.equal(_.size(obj._listeningTo), 0);
});
})(QUnit);

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
var ProxyModel = Backbone.Model.extend();
var Klass = Backbone.Collection.extend({
@@ -63,6 +63,36 @@
assert.equal(model.get('value'), 2);
});
QUnit.test('preinitialize', function(assert) {
assert.expect(2);
var Model = Backbone.Model.extend({
preinitialize: function() {
this.one = 1;
}
});
var model = new Model({}, {collection: collection});
assert.equal(model.one, 1);
assert.equal(model.collection, collection);
});
QUnit.test('preinitialize occurs before the model is set up', function(assert) {
assert.expect(6);
var Model = Backbone.Model.extend({
preinitialize: function() {
assert.equal(this.collection, undefined);
assert.equal(this.cid, undefined);
assert.equal(this.id, undefined);
}
});
var model = new Model({id: 'foo'}, {collection: collection});
assert.equal(model.collection, collection);
assert.equal(model.id, 'foo');
assert.notEqual(model.cid, undefined);
});
QUnit.test('parse can return null', function(assert) {
assert.expect(1);
var Model = Backbone.Model.extend({
@@ -1365,6 +1395,28 @@
assert.ok(!model.set('valid', false, {validate: true}));
});
QUnit.test('mixin', function(assert) {
Backbone.Model.mixin({
isEqual: function(model1, model2) {
return _.isEqual(model1, model2.attributes);
}
});
var model1 = new Backbone.Model({
a: {b: 2}, c: 3
});
var model2 = new Backbone.Model({
a: {b: 2}, c: 3
});
var model3 = new Backbone.Model({
a: {b: 4}, c: 3
});
assert.equal(model1.isEqual(model2), true);
assert.equal(model1.isEqual(model3), false);
});
QUnit.test('#1179 - isValid returns true in the absence of validate.', function(assert) {
assert.expect(1);
var model = new Backbone.Model();
@@ -1415,4 +1467,4 @@
assert.equal(model.id, 3);
});
})();
})(QUnit);

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
QUnit.module('Backbone.noConflict');
@@ -10,4 +10,4 @@
assert.equal(window.Backbone, noconflictBackbone, 'Backbone is still pointing to the original Backbone');
});
})();
})(QUnit);

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
var router = null;
var location = null;
@@ -28,7 +28,8 @@
'fragment',
'pathname',
'protocol'
));
));
// In IE, anchor.pathname does not contain a leading slash though
// window.location.pathname does.
if (!/^\//.test(this.pathname)) this.pathname = '/' + this.pathname;
@@ -42,7 +43,7 @@
QUnit.module('Backbone.Router', {
setup: function() {
beforeEach: function() {
location = new Location('http://example.com');
Backbone.history = _.extend(new Backbone.History, {location: location});
router = new Router({testing: 101});
@@ -53,7 +54,7 @@
Backbone.history.on('route', onRoute);
},
teardown: function() {
afterEach: function() {
Backbone.history.stop();
Backbone.history.off('route', onRoute);
}
@@ -95,6 +96,10 @@
'*anything': 'anything'
},
preinitialize: function(options) {
this.testpreinit = 'foo';
},
initialize: function(options) {
this.testing = options.testing;
this.route('implicit', 'implicit');
@@ -121,19 +126,19 @@
this.charType = 'escaped';
},
contacts: function(){
contacts: function() {
this.contact = 'index';
},
newContact: function(){
newContact: function() {
this.contact = 'new';
},
loadContact: function(){
loadContact: function() {
this.contact = 'load';
},
optionalItem: function(arg){
optionalItem: function(arg) {
this.arg = arg !== void 0 ? arg : null;
},
@@ -181,6 +186,11 @@
assert.equal(router.testing, 101);
});
QUnit.test('preinitialize', function(assert) {
assert.expect(1);
assert.equal(router.testpreinit, 'foo');
});
QUnit.test('routes (simple)', function(assert) {
assert.expect(4);
location.replace('http://example.com#search/news');
@@ -234,10 +244,11 @@
assert.ok(Backbone.history.navigate('search/manhattan/p20', true));
});
QUnit.test('route precedence via navigate', function(assert){
QUnit.test('route precedence via navigate', function(assert) {
assert.expect(6);
// check both 0.9.x and backwards-compatibility options
_.each([{trigger: true}, true], function( options ){
// Check both 0.9.x and backwards-compatibility options
_.each([{trigger: true}, true], function(options) {
Backbone.history.navigate('contacts', options);
assert.equal(router.contact, 'index');
Backbone.history.navigate('contacts/new', options);
@@ -249,7 +260,7 @@
QUnit.test('loadUrl is not called for identical routes.', function(assert) {
assert.expect(0);
Backbone.history.loadUrl = function(){ assert.ok(false); };
Backbone.history.loadUrl = function() { assert.ok(false); };
location.replace('http://example.com#route');
Backbone.history.navigate('route');
Backbone.history.navigate('/route');
@@ -345,9 +356,9 @@
assert.strictEqual(router.path, 'c/d/e');
});
QUnit.test("fires event when router doesn't have callback on it", function(assert) {
QUnit.test('fires event when router doesn\'t have callback on it', function(assert) {
assert.expect(1);
router.on('route:noCallback', function(){ assert.ok(true); });
router.on('route:noCallback', function() { assert.ok(true); });
location.replace('http://example.com#noCallback');
Backbone.history.checkUrl();
});
@@ -536,8 +547,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){},
replaceState: function(){}
pushState: function() {},
replaceState: function() {}
}
});
Backbone.history.start({root: 'root'});
@@ -551,8 +562,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){},
replaceState: function(state, title, url){
pushState: function() {},
replaceState: function(state, title, url) {
assert.strictEqual(url, '/root/x/y');
}
}
@@ -570,8 +581,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){},
replaceState: function(){}
pushState: function() {},
replaceState: function() {}
}
});
Backbone.history.start({root: ''});
@@ -625,8 +636,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){},
replaceState: function(state, title, url){
pushState: function() {},
replaceState: function(state, title, url) {
assert.strictEqual(url, '/root/x/y?a=b');
}
}
@@ -641,8 +652,8 @@
assert.expect(1);
var MyRouter = Backbone.Router.extend({
routes: {'': 'empty'},
empty: function(){},
route: function(route){
empty: function() {},
route: function(route) {
assert.strictEqual(route, '');
}
});
@@ -655,7 +666,8 @@
assert.strictEqual(history.getFragment('fragment '), 'fragment');
});
QUnit.test('#1820 - Leading slash and trailing space.', 1, function(assert) {
QUnit.test('#1820 - Leading slash and trailing space.', function(assert) {
assert.expect(1);
var history = new Backbone.History;
assert.strictEqual(history.getFragment('/fragment '), 'fragment');
});
@@ -670,7 +682,7 @@
assert.strictEqual(router.z, '123');
});
QUnit.test("#2062 - Trigger 'route' event on router instance.", function(assert) {
QUnit.test('#2062 - Trigger "route" event on router instance.', function(assert) {
assert.expect(2);
router.on('route', function(name, args) {
assert.strictEqual(name, 'routeEvent');
@@ -709,8 +721,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){},
replaceState: function(){ assert.ok(false); }
pushState: function() {},
replaceState: function() { assert.ok(false); }
}
});
Backbone.history.start({
@@ -726,8 +738,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){},
replaceState: function(){}
pushState: function() {},
replaceState: function() {}
}
});
@@ -753,7 +765,7 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(state, title, url){
pushState: function(state, title, url) {
assert.strictEqual(url, '/root');
}
}
@@ -785,7 +797,7 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(state, title, url){
pushState: function(state, title, url) {
assert.strictEqual(url, '/root?x=1');
}
}
@@ -823,7 +835,7 @@
assert.expect(1);
var MyRouter = Backbone.Router.extend({
routes: {
path: function(params){
path: function(params) {
assert.strictEqual(params, 'x=y%3Fz');
}
}
@@ -921,7 +933,7 @@
Backbone.history = _.extend(new Backbone.History, {location: location});
var MyRouter = Backbone.Router.extend({
routes: {'foo/:id/bar': 'foo'},
foo: function(){},
foo: function() {},
execute: function(callback, args, name) {
assert.strictEqual(callback, this.foo);
assert.deepEqual(args, ['123', 'x=y']);
@@ -953,8 +965,8 @@
Backbone.history = _.extend(new Backbone.History, {
location: location,
history: {
pushState: function(){ assert.ok(false); },
replaceState: function(){ assert.ok(false); }
pushState: function() { assert.ok(false); },
replaceState: function() { assert.ok(false); }
}
});
Backbone.history.start({pushState: true});
@@ -991,14 +1003,14 @@
Backbone.history.start({root: '/root', pushState: true});
});
QUnit.test("Paths that don't match the root should not match no root", function(assert) {
QUnit.test('Paths that don\'t match the root should not match no root', function(assert) {
assert.expect(0);
location.replace('http://example.com/foo');
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
var MyRouter = Backbone.Router.extend({
routes: {
foo: function(){
foo: function() {
assert.ok(false, 'should not match unless root matches');
}
}
@@ -1007,14 +1019,14 @@
Backbone.history.start({root: 'root', pushState: true});
});
QUnit.test("Paths that don't match the root should not match roots of the same length", function(assert) {
QUnit.test('Paths that don\'t match the root should not match roots of the same length', function(assert) {
assert.expect(0);
location.replace('http://example.com/xxxx/foo');
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
var MyRouter = Backbone.Router.extend({
routes: {
foo: function(){
foo: function() {
assert.ok(false, 'should not match unless root matches');
}
}
@@ -1029,7 +1041,7 @@
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
var MyRouter = Backbone.Router.extend({
routes: {foo: function(){ assert.ok(true); }}
routes: {foo: function() { assert.ok(true); }}
});
var myRouter = new MyRouter;
Backbone.history.start({root: 'x+y.z', pushState: true});
@@ -1041,7 +1053,7 @@
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
var MyRouter = Backbone.Router.extend({
routes: {foo: function(){ assert.ok(true); }}
routes: {foo: function() { assert.ok(true); }}
});
var myRouter = new MyRouter;
Backbone.history.start({root: '®ooτ', pushState: true});
@@ -1053,10 +1065,17 @@
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
var MyRouter = Backbone.Router.extend({
routes: {'': function(){ assert.ok(true); }}
routes: {'': function() { assert.ok(true); }}
});
var myRouter = new MyRouter;
Backbone.history.start({root: '®ooτ', pushState: true});
});
})();
QUnit.test('#4025 - navigate updates URL hash as is', function(assert) {
assert.expect(1);
var route = 'search/has%20space';
Backbone.history.navigate(route);
assert.strictEqual(location.hash, '#' + route);
});
})(QUnit);

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
var sync = Backbone.sync;
var ajax = Backbone.ajax;
@@ -14,7 +14,7 @@
var env = QUnit.config.current.testEnvironment;
// We never want to actually call these during tests.
history.pushState = history.replaceState = function(){};
history.pushState = history.replaceState = function() {};
// Capture ajax settings for comparison.
Backbone.ajax = function(settings) {
@@ -42,4 +42,4 @@
history.replaceState = replaceState;
});
})();
})(QUnit);

View File

@@ -1,4 +1,4 @@
(function() {
(function(QUnit) {
var Library = Backbone.Collection.extend({
url: function() { return '/library'; }
@@ -158,7 +158,7 @@
QUnit.test('Backbone.ajax', function(assert) {
assert.expect(1);
Backbone.ajax = function(settings){
Backbone.ajax = function(settings) {
assert.strictEqual(settings.url, '/test');
};
var model = new Backbone.Model();
@@ -236,4 +236,4 @@
this.ajaxSettings.error({}, 'textStatus', 'errorThrown');
});
})();
})(QUnit);

View File

@@ -1,13 +1,13 @@
(function() {
(function(QUnit) {
var view;
QUnit.module('Backbone.View', {
beforeEach: function(assert) {
beforeEach: function() {
$('#qunit-fixture').append(
'<div id="testElement"><h1>Test</h1></div>'
);
);
view = new Backbone.View({
id: 'test-view',
@@ -61,6 +61,28 @@
assert.strictEqual(new View().one, 1);
});
QUnit.test('preinitialize', function(assert) {
assert.expect(1);
var View = Backbone.View.extend({
preinitialize: function() {
this.one = 1;
}
});
assert.strictEqual(new View().one, 1);
});
QUnit.test('preinitialize occurs before the view is set up', function(assert) {
assert.expect(2);
var View = Backbone.View.extend({
preinitialize: function() {
assert.equal(this.el, undefined);
}
});
var _view = new View({});
assert.notEqual(_view.el, undefined);
});
QUnit.test('render', function(assert) {
assert.expect(1);
var myView = new Backbone.View;
@@ -72,8 +94,8 @@
var counter1 = 0, counter2 = 0;
var myView = new Backbone.View({el: '#testElement'});
myView.increment = function(){ counter1++; };
myView.$el.on('click', function(){ counter2++; });
myView.increment = function() { counter1++; };
myView.$el.on('click', function() { counter2++; });
var events = {'click h1': 'increment'};
@@ -129,11 +151,10 @@
assert.equal(myView.counter, 3);
});
QUnit.test('delegateEvents ignore undefined methods', function(assert) {
assert.expect(0);
var myView = new Backbone.View({el: '<p></p>'});
myView.delegateEvents({'click': 'undefinedMethod'});
myView.delegateEvents({click: 'undefinedMethod'});
myView.$el.trigger('click');
});
@@ -142,8 +163,8 @@
var counter1 = 0, counter2 = 0;
var myView = new Backbone.View({el: '#testElement'});
myView.increment = function(){ counter1++; };
myView.$el.on('click', function(){ counter2++; });
myView.increment = function() { counter1++; };
myView.$el.on('click', function() { counter2++; });
var events = {'click h1': 'increment'};
@@ -203,7 +224,7 @@
assert.expect(2);
var myView = new Backbone.View({el: '#testElement'});
myView.delegate('click', function() { assert.ok(true); });
var handler = function(){ assert.ok(false); };
var handler = function() { assert.ok(false); };
myView.delegate('click', 'h1', handler);
myView.undelegate('click', 'h1', handler);
myView.$('h1').trigger('click');
@@ -405,8 +426,8 @@
assert.expect(0);
var View = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, 'all x', function(){ assert.ok(false); });
this.listenTo(this.collection, 'all x', function(){ assert.ok(false); });
this.listenTo(this.model, 'all x', function() { assert.ok(false); });
this.listenTo(this.collection, 'all x', function() { assert.ok(false); });
}
});
@@ -492,4 +513,4 @@
assert.notEqual($oldEl, myView.$el);
});
})();
})(QUnit);