backbone-rails 1.1.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa24e20fdd39692add0b4a959eedb138732d76ce
4
+ data.tar.gz: 03bf7d7b25f3ce21e0c92bf86e0f328662c97a40
5
+ SHA512:
6
+ metadata.gz: 1a5f563db118db9f9ffd7a488b1b676e7cc104e5559b62df35444029bda63055652066b1ee3147a739f39db1b539c1525924f47c0cc169da82acb1846b59326b
7
+ data.tar.gz: 69bf3dbd1e4808ca3a955061b6ed18f34cf7633899f3e62edb0aca6f319df2c58b13ffaf570984c4368931f0cef1f461834338cf7c216a218f8975adb3f0f679
@@ -1,11 +1,16 @@
1
- // Backbone.js 1.1.2
1
+ // Backbone.js 1.2.3
2
2
 
3
- // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
3
+ // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4
4
  // Backbone may be freely distributed under the MIT license.
5
5
  // For all details and documentation:
6
6
  // http://backbonejs.org
7
7
 
8
- (function(root, factory) {
8
+ (function(factory) {
9
+
10
+ // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
11
+ // We use `self` instead of `window` for `WebWorker` support.
12
+ var root = (typeof self == 'object' && self.self == self && self) ||
13
+ (typeof global == 'object' && global.global == global && global);
9
14
 
10
15
  // Set up Backbone appropriately for the environment. Start with AMD.
11
16
  if (typeof define === 'function' && define.amd) {
@@ -17,15 +22,16 @@
17
22
 
18
23
  // Next for Node.js or CommonJS. jQuery may not be needed as a module.
19
24
  } else if (typeof exports !== 'undefined') {
20
- var _ = require('underscore');
21
- factory(root, exports, _);
25
+ var _ = require('underscore'), $;
26
+ try { $ = require('jquery'); } catch(e) {}
27
+ factory(root, exports, _, $);
22
28
 
23
29
  // Finally, as a browser global.
24
30
  } else {
25
31
  root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
26
32
  }
27
33
 
28
- }(this, function(root, Backbone, _, $) {
34
+ }(function(root, Backbone, _, $) {
29
35
 
30
36
  // Initial Setup
31
37
  // -------------
@@ -34,14 +40,11 @@
34
40
  // restored later on, if `noConflict` is used.
35
41
  var previousBackbone = root.Backbone;
36
42
 
37
- // Create local references to array methods we'll want to use later.
38
- var array = [];
39
- var push = array.push;
40
- var slice = array.slice;
41
- var splice = array.splice;
43
+ // Create a local reference to a common array method we'll want to use later.
44
+ var slice = Array.prototype.slice;
42
45
 
43
46
  // Current version of the library. Keep in sync with `package.json`.
44
- Backbone.VERSION = '1.1.2';
47
+ Backbone.VERSION = '1.2.3';
45
48
 
46
49
  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
47
50
  // the `$` variable.
@@ -60,17 +63,65 @@
60
63
  Backbone.emulateHTTP = false;
61
64
 
62
65
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
63
- // `application/json` requests ... will encode the body as
66
+ // `application/json` requests ... this will encode the body as
64
67
  // `application/x-www-form-urlencoded` instead and will send the model in a
65
68
  // form param named `model`.
66
69
  Backbone.emulateJSON = false;
67
70
 
71
+ // Proxy Backbone class methods to Underscore functions, wrapping the model's
72
+ // `attributes` object or collection's `models` array behind the scenes.
73
+ //
74
+ // collection.filter(function(model) { return model.get('age') > 10 });
75
+ // collection.each(this.addView);
76
+ //
77
+ // `Function#apply` can be slow so we use the method's arg count, if we know it.
78
+ var addMethod = function(length, method, attribute) {
79
+ switch (length) {
80
+ case 1: return function() {
81
+ return _[method](this[attribute]);
82
+ };
83
+ case 2: return function(value) {
84
+ return _[method](this[attribute], value);
85
+ };
86
+ case 3: return function(iteratee, context) {
87
+ return _[method](this[attribute], cb(iteratee, this), context);
88
+ };
89
+ case 4: return function(iteratee, defaultVal, context) {
90
+ return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
91
+ };
92
+ default: return function() {
93
+ var args = slice.call(arguments);
94
+ args.unshift(this[attribute]);
95
+ return _[method].apply(_, args);
96
+ };
97
+ }
98
+ };
99
+ var addUnderscoreMethods = function(Class, methods, attribute) {
100
+ _.each(methods, function(length, method) {
101
+ if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
102
+ });
103
+ };
104
+
105
+ // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
106
+ var cb = function(iteratee, instance) {
107
+ if (_.isFunction(iteratee)) return iteratee;
108
+ if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
109
+ if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
110
+ return iteratee;
111
+ };
112
+ var modelMatcher = function(attrs) {
113
+ var matcher = _.matches(attrs);
114
+ return function(model) {
115
+ return matcher(model.attributes);
116
+ };
117
+ };
118
+
68
119
  // Backbone.Events
69
120
  // ---------------
70
121
 
71
122
  // A module that can be mixed in to *any object* in order to provide it with
72
- // custom events. You may bind with `on` or remove with `off` callback
73
- // functions to an event; `trigger`-ing an event fires all callbacks in
123
+ // a custom event channel. You may bind a callback to an event with `on` or
124
+ // remove with `off`; `trigger`-ing an event fires all callbacks in
74
125
  // succession.
75
126
  //
76
127
  // var object = {};
@@ -78,123 +129,234 @@
78
129
  // object.on('expand', function(){ alert('expanded'); });
79
130
  // object.trigger('expand');
80
131
  //
81
- var Events = Backbone.Events = {
82
-
83
- // Bind an event to a `callback` function. Passing `"all"` will bind
84
- // the callback to all events fired.
85
- on: function(name, callback, context) {
86
- if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
87
- this._events || (this._events = {});
88
- var events = this._events[name] || (this._events[name] = []);
89
- events.push({callback: callback, context: context, ctx: context || this});
90
- return this;
91
- },
132
+ var Events = Backbone.Events = {};
92
133
 
93
- // Bind an event to only be triggered a single time. After the first time
94
- // the callback is invoked, it will be removed.
95
- once: function(name, callback, context) {
96
- if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
97
- var self = this;
98
- var once = _.once(function() {
99
- self.off(name, once);
100
- callback.apply(this, arguments);
101
- });
102
- once._callback = callback;
103
- return this.on(name, once, context);
104
- },
105
-
106
- // Remove one or many callbacks. If `context` is null, removes all
107
- // callbacks with that function. If `callback` is null, removes all
108
- // callbacks for the event. If `name` is null, removes all bound
109
- // callbacks for all events.
110
- off: function(name, callback, context) {
111
- var retain, ev, events, names, i, l, j, k;
112
- if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
113
- if (!name && !callback && !context) {
114
- this._events = void 0;
115
- return this;
134
+ // Regular expression used to split event strings.
135
+ var eventSplitter = /\s+/;
136
+
137
+ // Iterates over the standard `event, callback` (as well as the fancy multiple
138
+ // space-separated events `"change blur", callback` and jQuery-style event
139
+ // maps `{event: callback}`).
140
+ var eventsApi = function(iteratee, events, name, callback, opts) {
141
+ var i = 0, names;
142
+ if (name && typeof name === 'object') {
143
+ // Handle event maps.
144
+ if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
145
+ for (names = _.keys(name); i < names.length ; i++) {
146
+ events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
116
147
  }
117
- names = name ? [name] : _.keys(this._events);
118
- for (i = 0, l = names.length; i < l; i++) {
119
- name = names[i];
120
- if (events = this._events[name]) {
121
- this._events[name] = retain = [];
122
- if (callback || context) {
123
- for (j = 0, k = events.length; j < k; j++) {
124
- ev = events[j];
125
- if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
126
- (context && context !== ev.context)) {
127
- retain.push(ev);
128
- }
129
- }
130
- }
131
- if (!retain.length) delete this._events[name];
132
- }
148
+ } else if (name && eventSplitter.test(name)) {
149
+ // Handle space separated event names by delegating them individually.
150
+ for (names = name.split(eventSplitter); i < names.length; i++) {
151
+ events = iteratee(events, names[i], callback, opts);
133
152
  }
153
+ } else {
154
+ // Finally, standard events.
155
+ events = iteratee(events, name, callback, opts);
156
+ }
157
+ return events;
158
+ };
134
159
 
135
- return this;
136
- },
160
+ // Bind an event to a `callback` function. Passing `"all"` will bind
161
+ // the callback to all events fired.
162
+ Events.on = function(name, callback, context) {
163
+ return internalOn(this, name, callback, context);
164
+ };
137
165
 
138
- // Trigger one or many events, firing all bound callbacks. Callbacks are
139
- // passed the same arguments as `trigger` is, apart from the event name
140
- // (unless you're listening on `"all"`, which will cause your callback to
141
- // receive the true name of the event as the first argument).
142
- trigger: function(name) {
143
- if (!this._events) return this;
144
- var args = slice.call(arguments, 1);
145
- if (!eventsApi(this, 'trigger', name, args)) return this;
146
- var events = this._events[name];
147
- var allEvents = this._events.all;
148
- if (events) triggerEvents(events, args);
149
- if (allEvents) triggerEvents(allEvents, arguments);
150
- return this;
151
- },
166
+ // Guard the `listening` argument from the public API.
167
+ var internalOn = function(obj, name, callback, context, listening) {
168
+ obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
169
+ context: context,
170
+ ctx: obj,
171
+ listening: listening
172
+ });
152
173
 
153
- // Tell this object to stop listening to either specific events ... or
154
- // to every object it's currently listening to.
155
- stopListening: function(obj, name, callback) {
156
- var listeningTo = this._listeningTo;
157
- if (!listeningTo) return this;
158
- var remove = !name && !callback;
159
- if (!callback && typeof name === 'object') callback = this;
160
- if (obj) (listeningTo = {})[obj._listenId] = obj;
161
- for (var id in listeningTo) {
162
- obj = listeningTo[id];
163
- obj.off(name, callback, this);
164
- if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
165
- }
166
- return this;
174
+ if (listening) {
175
+ var listeners = obj._listeners || (obj._listeners = {});
176
+ listeners[listening.id] = listening;
167
177
  }
168
178
 
179
+ return obj;
169
180
  };
170
181
 
171
- // Regular expression used to split event strings.
172
- var eventSplitter = /\s+/;
182
+ // Inversion-of-control versions of `on`. Tell *this* object to listen to
183
+ // an event in another object... keeping track of what it's listening to
184
+ // for easier unbinding later.
185
+ Events.listenTo = function(obj, name, callback) {
186
+ if (!obj) return this;
187
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
188
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
189
+ var listening = listeningTo[id];
190
+
191
+ // This object is not listening to any other events on `obj` yet.
192
+ // Setup the necessary references to track the listening callbacks.
193
+ if (!listening) {
194
+ var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
195
+ listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
196
+ }
197
+
198
+ // Bind callbacks on obj, and keep track of them on listening.
199
+ internalOn(obj, name, callback, this, listening);
200
+ return this;
201
+ };
202
+
203
+ // The reducing API that adds a callback to the `events` object.
204
+ var onApi = function(events, name, callback, options) {
205
+ if (callback) {
206
+ var handlers = events[name] || (events[name] = []);
207
+ var context = options.context, ctx = options.ctx, listening = options.listening;
208
+ if (listening) listening.count++;
209
+
210
+ handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
211
+ }
212
+ return events;
213
+ };
214
+
215
+ // Remove one or many callbacks. If `context` is null, removes all
216
+ // callbacks with that function. If `callback` is null, removes all
217
+ // callbacks for the event. If `name` is null, removes all bound
218
+ // callbacks for all events.
219
+ Events.off = function(name, callback, context) {
220
+ if (!this._events) return this;
221
+ this._events = eventsApi(offApi, this._events, name, callback, {
222
+ context: context,
223
+ listeners: this._listeners
224
+ });
225
+ return this;
226
+ };
227
+
228
+ // Tell this object to stop listening to either specific events ... or
229
+ // to every object it's currently listening to.
230
+ Events.stopListening = function(obj, name, callback) {
231
+ var listeningTo = this._listeningTo;
232
+ if (!listeningTo) return this;
233
+
234
+ var ids = obj ? [obj._listenId] : _.keys(listeningTo);
235
+
236
+ for (var i = 0; i < ids.length; i++) {
237
+ var listening = listeningTo[ids[i]];
238
+
239
+ // If listening doesn't exist, this object is not currently
240
+ // listening to obj. Break out early.
241
+ if (!listening) break;
242
+
243
+ listening.obj.off(name, callback, this);
244
+ }
245
+ if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
246
+
247
+ return this;
248
+ };
173
249
 
174
- // Implement fancy features of the Events API such as multiple event
175
- // names `"change blur"` and jQuery-style event maps `{change: action}`
176
- // in terms of the existing API.
177
- var eventsApi = function(obj, action, name, rest) {
178
- if (!name) return true;
250
+ // The reducing API that removes a callback from the `events` object.
251
+ var offApi = function(events, name, callback, options) {
252
+ if (!events) return;
179
253
 
180
- // Handle event maps.
181
- if (typeof name === 'object') {
182
- for (var key in name) {
183
- obj[action].apply(obj, [key, name[key]].concat(rest));
254
+ var i = 0, listening;
255
+ var context = options.context, listeners = options.listeners;
256
+
257
+ // Delete all events listeners and "drop" events.
258
+ if (!name && !callback && !context) {
259
+ var ids = _.keys(listeners);
260
+ for (; i < ids.length; i++) {
261
+ listening = listeners[ids[i]];
262
+ delete listeners[listening.id];
263
+ delete listening.listeningTo[listening.objId];
184
264
  }
185
- return false;
265
+ return;
186
266
  }
187
267
 
188
- // Handle space separated event names.
189
- if (eventSplitter.test(name)) {
190
- var names = name.split(eventSplitter);
191
- for (var i = 0, l = names.length; i < l; i++) {
192
- obj[action].apply(obj, [names[i]].concat(rest));
268
+ var names = name ? [name] : _.keys(events);
269
+ for (; i < names.length; i++) {
270
+ name = names[i];
271
+ var handlers = events[name];
272
+
273
+ // Bail out if there are no events stored.
274
+ if (!handlers) break;
275
+
276
+ // Replace events if there are any remaining. Otherwise, clean up.
277
+ var remaining = [];
278
+ for (var j = 0; j < handlers.length; j++) {
279
+ var handler = handlers[j];
280
+ if (
281
+ callback && callback !== handler.callback &&
282
+ callback !== handler.callback._callback ||
283
+ context && context !== handler.context
284
+ ) {
285
+ remaining.push(handler);
286
+ } else {
287
+ listening = handler.listening;
288
+ if (listening && --listening.count === 0) {
289
+ delete listeners[listening.id];
290
+ delete listening.listeningTo[listening.objId];
291
+ }
292
+ }
293
+ }
294
+
295
+ // Update tail event if the list has any events. Otherwise, clean up.
296
+ if (remaining.length) {
297
+ events[name] = remaining;
298
+ } else {
299
+ delete events[name];
193
300
  }
194
- return false;
195
301
  }
302
+ if (_.size(events)) return events;
303
+ };
304
+
305
+ // Bind an event to only be triggered a single time. After the first time
306
+ // the callback is invoked, its listener will be removed. If multiple events
307
+ // are passed in using the space-separated syntax, the handler will fire
308
+ // once for each event, not once for a combination of all events.
309
+ Events.once = function(name, callback, context) {
310
+ // Map the event into a `{event: once}` object.
311
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
312
+ return this.on(events, void 0, context);
313
+ };
196
314
 
197
- return true;
315
+ // Inversion-of-control versions of `once`.
316
+ Events.listenToOnce = function(obj, name, callback) {
317
+ // Map the event into a `{event: once}` object.
318
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
319
+ return this.listenTo(obj, events);
320
+ };
321
+
322
+ // Reduces the event callbacks into a map of `{event: onceWrapper}`.
323
+ // `offer` unbinds the `onceWrapper` after it has been called.
324
+ var onceMap = function(map, name, callback, offer) {
325
+ if (callback) {
326
+ var once = map[name] = _.once(function() {
327
+ offer(name, once);
328
+ callback.apply(this, arguments);
329
+ });
330
+ once._callback = callback;
331
+ }
332
+ return map;
333
+ };
334
+
335
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
336
+ // passed the same arguments as `trigger` is, apart from the event name
337
+ // (unless you're listening on `"all"`, which will cause your callback to
338
+ // receive the true name of the event as the first argument).
339
+ Events.trigger = function(name) {
340
+ if (!this._events) return this;
341
+
342
+ var length = Math.max(0, arguments.length - 1);
343
+ var args = Array(length);
344
+ for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
345
+
346
+ eventsApi(triggerApi, this._events, name, void 0, args);
347
+ return this;
348
+ };
349
+
350
+ // Handles triggering the appropriate event callbacks.
351
+ var triggerApi = function(objEvents, name, cb, args) {
352
+ if (objEvents) {
353
+ var events = objEvents[name];
354
+ var allEvents = objEvents.all;
355
+ if (events && allEvents) allEvents = allEvents.slice();
356
+ if (events) triggerEvents(events, args);
357
+ if (allEvents) triggerEvents(allEvents, [name].concat(args));
358
+ }
359
+ return objEvents;
198
360
  };
199
361
 
200
362
  // A difficult-to-believe, but optimized internal dispatch function for
@@ -211,22 +373,6 @@
211
373
  }
212
374
  };
213
375
 
214
- var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
215
-
216
- // Inversion-of-control versions of `on` and `once`. Tell *this* object to
217
- // listen to an event in another object ... keeping track of what it's
218
- // listening to.
219
- _.each(listenMethods, function(implementation, method) {
220
- Events[method] = function(obj, name, callback) {
221
- var listeningTo = this._listeningTo || (this._listeningTo = {});
222
- var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
223
- listeningTo[id] = obj;
224
- if (!callback && typeof name === 'object') callback = this;
225
- obj[implementation](name, callback, this);
226
- return this;
227
- };
228
- });
229
-
230
376
  // Aliases for backwards compatibility.
231
377
  Events.bind = Events.on;
232
378
  Events.unbind = Events.off;
@@ -248,7 +394,7 @@
248
394
  var Model = Backbone.Model = function(attributes, options) {
249
395
  var attrs = attributes || {};
250
396
  options || (options = {});
251
- this.cid = _.uniqueId('c');
397
+ this.cid = _.uniqueId(this.cidPrefix);
252
398
  this.attributes = {};
253
399
  if (options.collection) this.collection = options.collection;
254
400
  if (options.parse) attrs = this.parse(attrs, options) || {};
@@ -271,6 +417,10 @@
271
417
  // CouchDB users may want to set this to `"_id"`.
272
418
  idAttribute: 'id',
273
419
 
420
+ // The prefix is used to create the client id which is used to identify models locally.
421
+ // You may want to override this if you're experiencing name clashes with model ids.
422
+ cidPrefix: 'c',
423
+
274
424
  // Initialize is an empty function by default. Override it with your own
275
425
  // initialization logic.
276
426
  initialize: function(){},
@@ -302,14 +452,19 @@
302
452
  return this.get(attr) != null;
303
453
  },
304
454
 
455
+ // Special-cased proxy to underscore's `_.matches` method.
456
+ matches: function(attrs) {
457
+ return !!_.iteratee(attrs, this)(this.attributes);
458
+ },
459
+
305
460
  // Set a hash of model attributes on the object, firing `"change"`. This is
306
461
  // the core primitive operation of a model, updating the data and notifying
307
462
  // anyone who needs to know about the change in state. The heart of the beast.
308
463
  set: function(key, val, options) {
309
- var attr, attrs, unset, changes, silent, changing, prev, current;
310
464
  if (key == null) return this;
311
465
 
312
466
  // Handle both `"key", value` and `{key: value}` -style arguments.
467
+ var attrs;
313
468
  if (typeof key === 'object') {
314
469
  attrs = key;
315
470
  options = val;
@@ -323,37 +478,40 @@
323
478
  if (!this._validate(attrs, options)) return false;
324
479
 
325
480
  // Extract attributes and options.
326
- unset = options.unset;
327
- silent = options.silent;
328
- changes = [];
329
- changing = this._changing;
330
- this._changing = true;
481
+ var unset = options.unset;
482
+ var silent = options.silent;
483
+ var changes = [];
484
+ var changing = this._changing;
485
+ this._changing = true;
331
486
 
332
487
  if (!changing) {
333
488
  this._previousAttributes = _.clone(this.attributes);
334
489
  this.changed = {};
335
490
  }
336
- current = this.attributes, prev = this._previousAttributes;
337
491
 
338
- // Check for changes of `id`.
339
- if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
492
+ var current = this.attributes;
493
+ var changed = this.changed;
494
+ var prev = this._previousAttributes;
340
495
 
341
496
  // For each `set` attribute, update or delete the current value.
342
- for (attr in attrs) {
497
+ for (var attr in attrs) {
343
498
  val = attrs[attr];
344
499
  if (!_.isEqual(current[attr], val)) changes.push(attr);
345
500
  if (!_.isEqual(prev[attr], val)) {
346
- this.changed[attr] = val;
501
+ changed[attr] = val;
347
502
  } else {
348
- delete this.changed[attr];
503
+ delete changed[attr];
349
504
  }
350
505
  unset ? delete current[attr] : current[attr] = val;
351
506
  }
352
507
 
508
+ // Update the `id`.
509
+ this.id = this.get(this.idAttribute);
510
+
353
511
  // Trigger all relevant attribute changes.
354
512
  if (!silent) {
355
513
  if (changes.length) this._pending = options;
356
- for (var i = 0, l = changes.length; i < l; i++) {
514
+ for (var i = 0; i < changes.length; i++) {
357
515
  this.trigger('change:' + changes[i], this, current[changes[i]], options);
358
516
  }
359
517
  }
@@ -401,13 +559,14 @@
401
559
  // determining if there *would be* a change.
402
560
  changedAttributes: function(diff) {
403
561
  if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
404
- var val, changed = false;
405
562
  var old = this._changing ? this._previousAttributes : this.attributes;
563
+ var changed = {};
406
564
  for (var attr in diff) {
407
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
408
- (changed || (changed = {}))[attr] = val;
565
+ var val = diff[attr];
566
+ if (_.isEqual(old[attr], val)) continue;
567
+ changed[attr] = val;
409
568
  }
410
- return changed;
569
+ return _.size(changed) ? changed : false;
411
570
  },
412
571
 
413
572
  // Get the previous value of an attribute, recorded at the time the last
@@ -423,17 +582,16 @@
423
582
  return _.clone(this._previousAttributes);
424
583
  },
425
584
 
426
- // Fetch the model from the server. If the server's representation of the
427
- // model differs from its current attributes, they will be overridden,
428
- // triggering a `"change"` event.
585
+ // Fetch the model from the server, merging the response with the model's
586
+ // local attributes. Any changed attributes will trigger a "change" event.
429
587
  fetch: function(options) {
430
- options = options ? _.clone(options) : {};
431
- if (options.parse === void 0) options.parse = true;
588
+ options = _.extend({parse: true}, options);
432
589
  var model = this;
433
590
  var success = options.success;
434
591
  options.success = function(resp) {
435
- if (!model.set(model.parse(resp, options), options)) return false;
436
- if (success) success(model, resp, options);
592
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
593
+ if (!model.set(serverAttrs, options)) return false;
594
+ if (success) success.call(options.context, model, resp, options);
437
595
  model.trigger('sync', model, resp, options);
438
596
  };
439
597
  wrapError(this, options);
@@ -444,9 +602,8 @@
444
602
  // If the server returns an attributes hash that differs, the model's
445
603
  // state will be `set` again.
446
604
  save: function(key, val, options) {
447
- var attrs, method, xhr, attributes = this.attributes;
448
-
449
605
  // Handle both `"key", value` and `{key: value}` -style arguments.
606
+ var attrs;
450
607
  if (key == null || typeof key === 'object') {
451
608
  attrs = key;
452
609
  options = val;
@@ -454,46 +611,43 @@
454
611
  (attrs = {})[key] = val;
455
612
  }
456
613
 
457
- options = _.extend({validate: true}, options);
614
+ options = _.extend({validate: true, parse: true}, options);
615
+ var wait = options.wait;
458
616
 
459
617
  // If we're not waiting and attributes exist, save acts as
460
618
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
461
619
  // the model will be valid when the attributes, if any, are set.
462
- if (attrs && !options.wait) {
620
+ if (attrs && !wait) {
463
621
  if (!this.set(attrs, options)) return false;
464
622
  } else {
465
623
  if (!this._validate(attrs, options)) return false;
466
624
  }
467
625
 
468
- // Set temporary attributes if `{wait: true}`.
469
- if (attrs && options.wait) {
470
- this.attributes = _.extend({}, attributes, attrs);
471
- }
472
-
473
626
  // After a successful server-side save, the client is (optionally)
474
627
  // updated with the server-side state.
475
- if (options.parse === void 0) options.parse = true;
476
628
  var model = this;
477
629
  var success = options.success;
630
+ var attributes = this.attributes;
478
631
  options.success = function(resp) {
479
632
  // Ensure attributes are restored during synchronous saves.
480
633
  model.attributes = attributes;
481
- var serverAttrs = model.parse(resp, options);
482
- if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
483
- if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
484
- return false;
485
- }
486
- if (success) success(model, resp, options);
634
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
635
+ if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
636
+ if (serverAttrs && !model.set(serverAttrs, options)) return false;
637
+ if (success) success.call(options.context, model, resp, options);
487
638
  model.trigger('sync', model, resp, options);
488
639
  };
489
640
  wrapError(this, options);
490
641
 
491
- method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
492
- if (method === 'patch') options.attrs = attrs;
493
- xhr = this.sync(method, this, options);
642
+ // Set temporary attributes if `{wait: true}` to properly find new ids.
643
+ if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
644
+
645
+ var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
646
+ if (method === 'patch' && !options.attrs) options.attrs = attrs;
647
+ var xhr = this.sync(method, this, options);
494
648
 
495
649
  // Restore attributes.
496
- if (attrs && options.wait) this.attributes = attributes;
650
+ this.attributes = attributes;
497
651
 
498
652
  return xhr;
499
653
  },
@@ -505,25 +659,27 @@
505
659
  options = options ? _.clone(options) : {};
506
660
  var model = this;
507
661
  var success = options.success;
662
+ var wait = options.wait;
508
663
 
509
664
  var destroy = function() {
665
+ model.stopListening();
510
666
  model.trigger('destroy', model, model.collection, options);
511
667
  };
512
668
 
513
669
  options.success = function(resp) {
514
- if (options.wait || model.isNew()) destroy();
515
- if (success) success(model, resp, options);
670
+ if (wait) destroy();
671
+ if (success) success.call(options.context, model, resp, options);
516
672
  if (!model.isNew()) model.trigger('sync', model, resp, options);
517
673
  };
518
674
 
675
+ var xhr = false;
519
676
  if (this.isNew()) {
520
- options.success();
521
- return false;
677
+ _.defer(options.success);
678
+ } else {
679
+ wrapError(this, options);
680
+ xhr = this.sync('delete', this, options);
522
681
  }
523
- wrapError(this, options);
524
-
525
- var xhr = this.sync('delete', this, options);
526
- if (!options.wait) destroy();
682
+ if (!wait) destroy();
527
683
  return xhr;
528
684
  },
529
685
 
@@ -536,7 +692,8 @@
536
692
  _.result(this.collection, 'url') ||
537
693
  urlError();
538
694
  if (this.isNew()) return base;
539
- return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
695
+ var id = this.get(this.idAttribute);
696
+ return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
540
697
  },
541
698
 
542
699
  // **parse** converts a response into the hash of attributes to be `set` on
@@ -557,7 +714,7 @@
557
714
 
558
715
  // Check if the model is currently in a valid state.
559
716
  isValid: function(options) {
560
- return this._validate({}, _.extend(options || {}, { validate: true }));
717
+ return this._validate({}, _.defaults({validate: true}, options));
561
718
  },
562
719
 
563
720
  // Run validation against the next complete set of model attributes,
@@ -573,23 +730,19 @@
573
730
 
574
731
  });
575
732
 
576
- // Underscore methods that we want to implement on the Model.
577
- var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
733
+ // Underscore methods that we want to implement on the Model, mapped to the
734
+ // number of arguments they take.
735
+ var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
736
+ omit: 0, chain: 1, isEmpty: 1 };
578
737
 
579
738
  // Mix in each Underscore method as a proxy to `Model#attributes`.
580
- _.each(modelMethods, function(method) {
581
- Model.prototype[method] = function() {
582
- var args = slice.call(arguments);
583
- args.unshift(this.attributes);
584
- return _[method].apply(_, args);
585
- };
586
- });
739
+ addUnderscoreMethods(Model, modelMethods, 'attributes');
587
740
 
588
741
  // Backbone.Collection
589
742
  // -------------------
590
743
 
591
744
  // If models tend to represent a single row of data, a Backbone Collection is
592
- // more analagous to a table full of data ... or a small slice or page of that
745
+ // more analogous to a table full of data ... or a small slice or page of that
593
746
  // table, or a collection of rows that belong together for a particular reason
594
747
  // -- all of the messages in this particular folder, all of the documents
595
748
  // belonging to this particular author, and so on. Collections maintain
@@ -611,6 +764,16 @@
611
764
  var setOptions = {add: true, remove: true, merge: true};
612
765
  var addOptions = {add: true, remove: false};
613
766
 
767
+ // Splices `insert` into `array` at index `at`.
768
+ var splice = function(array, insert, at) {
769
+ at = Math.min(Math.max(at, 0), array.length);
770
+ var tail = Array(array.length - at);
771
+ var length = insert.length;
772
+ for (var i = 0; i < tail.length; i++) tail[i] = array[i + at];
773
+ for (i = 0; i < length; i++) array[i + at] = insert[i];
774
+ for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
775
+ };
776
+
614
777
  // Define the Collection's inheritable methods.
615
778
  _.extend(Collection.prototype, Events, {
616
779
 
@@ -625,7 +788,7 @@
625
788
  // The JSON representation of a Collection is an array of the
626
789
  // models' attributes.
627
790
  toJSON: function(options) {
628
- return this.map(function(model){ return model.toJSON(options); });
791
+ return this.map(function(model) { return model.toJSON(options); });
629
792
  },
630
793
 
631
794
  // Proxy `Backbone.sync` by default.
@@ -633,32 +796,21 @@
633
796
  return Backbone.sync.apply(this, arguments);
634
797
  },
635
798
 
636
- // Add a model, or list of models to the set.
799
+ // Add a model, or list of models to the set. `models` may be Backbone
800
+ // Models or raw JavaScript objects to be converted to Models, or any
801
+ // combination of the two.
637
802
  add: function(models, options) {
638
803
  return this.set(models, _.extend({merge: false}, options, addOptions));
639
804
  },
640
805
 
641
806
  // Remove a model, or a list of models from the set.
642
807
  remove: function(models, options) {
808
+ options = _.extend({}, options);
643
809
  var singular = !_.isArray(models);
644
810
  models = singular ? [models] : _.clone(models);
645
- options || (options = {});
646
- var i, l, index, model;
647
- for (i = 0, l = models.length; i < l; i++) {
648
- model = models[i] = this.get(models[i]);
649
- if (!model) continue;
650
- delete this._byId[model.id];
651
- delete this._byId[model.cid];
652
- index = this.indexOf(model);
653
- this.models.splice(index, 1);
654
- this.length--;
655
- if (!options.silent) {
656
- options.index = index;
657
- model.trigger('remove', model, this, options);
658
- }
659
- this._removeReference(model, options);
660
- }
661
- return singular ? models[0] : models;
811
+ var removed = this._removeModels(models, options);
812
+ if (!options.silent && removed) this.trigger('update', this, options);
813
+ return singular ? removed[0] : removed;
662
814
  },
663
815
 
664
816
  // Update a collection by `set`-ing a new list of models, adding new ones,
@@ -666,78 +818,88 @@
666
818
  // already exist in the collection, as necessary. Similar to **Model#set**,
667
819
  // the core operation for updating the data contained by the collection.
668
820
  set: function(models, options) {
821
+ if (models == null) return;
822
+
669
823
  options = _.defaults({}, options, setOptions);
670
- if (options.parse) models = this.parse(models, options);
824
+ if (options.parse && !this._isModel(models)) models = this.parse(models, options);
825
+
671
826
  var singular = !_.isArray(models);
672
- models = singular ? (models ? [models] : []) : _.clone(models);
673
- var i, l, id, model, attrs, existing, sort;
827
+ models = singular ? [models] : models.slice();
828
+
674
829
  var at = options.at;
675
- var targetModel = this.model;
830
+ if (at != null) at = +at;
831
+ if (at < 0) at += this.length + 1;
832
+
833
+ var set = [];
834
+ var toAdd = [];
835
+ var toRemove = [];
836
+ var modelMap = {};
837
+
838
+ var add = options.add;
839
+ var merge = options.merge;
840
+ var remove = options.remove;
841
+
842
+ var sort = false;
676
843
  var sortable = this.comparator && (at == null) && options.sort !== false;
677
844
  var sortAttr = _.isString(this.comparator) ? this.comparator : null;
678
- var toAdd = [], toRemove = [], modelMap = {};
679
- var add = options.add, merge = options.merge, remove = options.remove;
680
- var order = !sortable && add && remove ? [] : false;
681
845
 
682
846
  // Turn bare objects into model references, and prevent invalid models
683
847
  // from being added.
684
- for (i = 0, l = models.length; i < l; i++) {
685
- attrs = models[i] || {};
686
- if (attrs instanceof Model) {
687
- id = model = attrs;
688
- } else {
689
- id = attrs[targetModel.prototype.idAttribute || 'id'];
690
- }
848
+ var model;
849
+ for (var i = 0; i < models.length; i++) {
850
+ model = models[i];
691
851
 
692
852
  // If a duplicate is found, prevent it from being added and
693
853
  // optionally merge it into the existing model.
694
- if (existing = this.get(id)) {
695
- if (remove) modelMap[existing.cid] = true;
696
- if (merge) {
697
- attrs = attrs === model ? model.attributes : attrs;
854
+ var existing = this.get(model);
855
+ if (existing) {
856
+ if (merge && model !== existing) {
857
+ var attrs = this._isModel(model) ? model.attributes : model;
698
858
  if (options.parse) attrs = existing.parse(attrs, options);
699
859
  existing.set(attrs, options);
700
- if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
860
+ if (sortable && !sort) sort = existing.hasChanged(sortAttr);
861
+ }
862
+ if (!modelMap[existing.cid]) {
863
+ modelMap[existing.cid] = true;
864
+ set.push(existing);
701
865
  }
702
866
  models[i] = existing;
703
867
 
704
868
  // If this is a new, valid model, push it to the `toAdd` list.
705
869
  } else if (add) {
706
- model = models[i] = this._prepareModel(attrs, options);
707
- if (!model) continue;
708
- toAdd.push(model);
709
- this._addReference(model, options);
870
+ model = models[i] = this._prepareModel(model, options);
871
+ if (model) {
872
+ toAdd.push(model);
873
+ this._addReference(model, options);
874
+ modelMap[model.cid] = true;
875
+ set.push(model);
876
+ }
710
877
  }
711
-
712
- // Do not add multiple models with the same `id`.
713
- model = existing || model;
714
- if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
715
- modelMap[model.id] = true;
716
878
  }
717
879
 
718
- // Remove nonexistent models if appropriate.
880
+ // Remove stale models.
719
881
  if (remove) {
720
- for (i = 0, l = this.length; i < l; ++i) {
721
- if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
882
+ for (i = 0; i < this.length; i++) {
883
+ model = this.models[i];
884
+ if (!modelMap[model.cid]) toRemove.push(model);
722
885
  }
723
- if (toRemove.length) this.remove(toRemove, options);
886
+ if (toRemove.length) this._removeModels(toRemove, options);
724
887
  }
725
888
 
726
889
  // See if sorting is needed, update `length` and splice in new models.
727
- if (toAdd.length || (order && order.length)) {
890
+ var orderChanged = false;
891
+ var replace = !sortable && add && remove;
892
+ if (set.length && replace) {
893
+ orderChanged = this.length != set.length || _.some(this.models, function(model, index) {
894
+ return model !== set[index];
895
+ });
896
+ this.models.length = 0;
897
+ splice(this.models, set, 0);
898
+ this.length = this.models.length;
899
+ } else if (toAdd.length) {
728
900
  if (sortable) sort = true;
729
- this.length += toAdd.length;
730
- if (at != null) {
731
- for (i = 0, l = toAdd.length; i < l; i++) {
732
- this.models.splice(at + i, 0, toAdd[i]);
733
- }
734
- } else {
735
- if (order) this.models.length = 0;
736
- var orderedModels = order || toAdd;
737
- for (i = 0, l = orderedModels.length; i < l; i++) {
738
- this.models.push(orderedModels[i]);
739
- }
740
- }
901
+ splice(this.models, toAdd, at == null ? this.length : at);
902
+ this.length = this.models.length;
741
903
  }
742
904
 
743
905
  // Silently sort the collection if appropriate.
@@ -745,10 +907,13 @@
745
907
 
746
908
  // Unless silenced, it's time to fire all appropriate add/sort events.
747
909
  if (!options.silent) {
748
- for (i = 0, l = toAdd.length; i < l; i++) {
749
- (model = toAdd[i]).trigger('add', model, this, options);
910
+ for (i = 0; i < toAdd.length; i++) {
911
+ if (at != null) options.index = at + i;
912
+ model = toAdd[i];
913
+ model.trigger('add', model, this, options);
750
914
  }
751
- if (sort || (order && order.length)) this.trigger('sort', this, options);
915
+ if (sort || orderChanged) this.trigger('sort', this, options);
916
+ if (toAdd.length || toRemove.length) this.trigger('update', this, options);
752
917
  }
753
918
 
754
919
  // Return the added (or merged) model (or models).
@@ -760,8 +925,8 @@
760
925
  // any granular `add` or `remove` events. Fires `reset` when finished.
761
926
  // Useful for bulk operations and optimizations.
762
927
  reset: function(models, options) {
763
- options || (options = {});
764
- for (var i = 0, l = this.models.length; i < l; i++) {
928
+ options = options ? _.clone(options) : {};
929
+ for (var i = 0; i < this.models.length; i++) {
765
930
  this._removeReference(this.models[i], options);
766
931
  }
767
932
  options.previousModels = this.models;
@@ -779,8 +944,7 @@
779
944
  // Remove a model from the end of the collection.
780
945
  pop: function(options) {
781
946
  var model = this.at(this.length - 1);
782
- this.remove(model, options);
783
- return model;
947
+ return this.remove(model, options);
784
948
  },
785
949
 
786
950
  // Add a model to the beginning of the collection.
@@ -791,8 +955,7 @@
791
955
  // Remove a model from the beginning of the collection.
792
956
  shift: function(options) {
793
957
  var model = this.at(0);
794
- this.remove(model, options);
795
- return model;
958
+ return this.remove(model, options);
796
959
  },
797
960
 
798
961
  // Slice out a sub-array of models from the collection.
@@ -803,24 +966,20 @@
803
966
  // Get a model from the set by id.
804
967
  get: function(obj) {
805
968
  if (obj == null) return void 0;
806
- return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
969
+ var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
970
+ return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
807
971
  },
808
972
 
809
973
  // Get the model at the given index.
810
974
  at: function(index) {
975
+ if (index < 0) index += this.length;
811
976
  return this.models[index];
812
977
  },
813
978
 
814
979
  // Return models with matching attributes. Useful for simple cases of
815
980
  // `filter`.
816
981
  where: function(attrs, first) {
817
- if (_.isEmpty(attrs)) return first ? void 0 : [];
818
- return this[first ? 'find' : 'filter'](function(model) {
819
- for (var key in attrs) {
820
- if (attrs[key] !== model.get(key)) return false;
821
- }
822
- return true;
823
- });
982
+ return this[first ? 'find' : 'filter'](attrs);
824
983
  },
825
984
 
826
985
  // Return the first model with matching attributes. Useful for simple cases
@@ -833,16 +992,19 @@
833
992
  // normal circumstances, as the set will maintain sort order as each item
834
993
  // is added.
835
994
  sort: function(options) {
836
- if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
995
+ var comparator = this.comparator;
996
+ if (!comparator) throw new Error('Cannot sort a set without a comparator');
837
997
  options || (options = {});
838
998
 
999
+ var length = comparator.length;
1000
+ if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
1001
+
839
1002
  // Run sort based on type of `comparator`.
840
- if (_.isString(this.comparator) || this.comparator.length === 1) {
841
- this.models = this.sortBy(this.comparator, this);
1003
+ if (length === 1 || _.isString(comparator)) {
1004
+ this.models = this.sortBy(comparator);
842
1005
  } else {
843
- this.models.sort(_.bind(this.comparator, this));
1006
+ this.models.sort(comparator);
844
1007
  }
845
-
846
1008
  if (!options.silent) this.trigger('sort', this, options);
847
1009
  return this;
848
1010
  },
@@ -856,14 +1018,13 @@
856
1018
  // collection when they arrive. If `reset: true` is passed, the response
857
1019
  // data will be passed through the `reset` method instead of `set`.
858
1020
  fetch: function(options) {
859
- options = options ? _.clone(options) : {};
860
- if (options.parse === void 0) options.parse = true;
1021
+ options = _.extend({parse: true}, options);
861
1022
  var success = options.success;
862
1023
  var collection = this;
863
1024
  options.success = function(resp) {
864
1025
  var method = options.reset ? 'reset' : 'set';
865
1026
  collection[method](resp, options);
866
- if (success) success(collection, resp, options);
1027
+ if (success) success.call(options.context, collection, resp, options);
867
1028
  collection.trigger('sync', collection, resp, options);
868
1029
  };
869
1030
  wrapError(this, options);
@@ -875,13 +1036,15 @@
875
1036
  // wait for the server to agree.
876
1037
  create: function(model, options) {
877
1038
  options = options ? _.clone(options) : {};
878
- if (!(model = this._prepareModel(model, options))) return false;
879
- if (!options.wait) this.add(model, options);
1039
+ var wait = options.wait;
1040
+ model = this._prepareModel(model, options);
1041
+ if (!model) return false;
1042
+ if (!wait) this.add(model, options);
880
1043
  var collection = this;
881
1044
  var success = options.success;
882
- options.success = function(model, resp) {
883
- if (options.wait) collection.add(model, options);
884
- if (success) success(model, resp, options);
1045
+ options.success = function(model, resp, callbackOpts) {
1046
+ if (wait) collection.add(model, callbackOpts);
1047
+ if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
885
1048
  };
886
1049
  model.save(null, options);
887
1050
  return model;
@@ -895,7 +1058,15 @@
895
1058
 
896
1059
  // Create a new collection with an identical list of models as this one.
897
1060
  clone: function() {
898
- return new this.constructor(this.models);
1061
+ return new this.constructor(this.models, {
1062
+ model: this.model,
1063
+ comparator: this.comparator
1064
+ });
1065
+ },
1066
+
1067
+ // Define how to uniquely identify models in the collection.
1068
+ modelId: function (attrs) {
1069
+ return attrs[this.model.prototype.idAttribute || 'id'];
899
1070
  },
900
1071
 
901
1072
  // Private method to reset all internal state. Called when the collection
@@ -909,7 +1080,10 @@
909
1080
  // Prepare a hash of attributes (or other model) to be added to this
910
1081
  // collection.
911
1082
  _prepareModel: function(attrs, options) {
912
- if (attrs instanceof Model) return attrs;
1083
+ if (this._isModel(attrs)) {
1084
+ if (!attrs.collection) attrs.collection = this;
1085
+ return attrs;
1086
+ }
913
1087
  options = options ? _.clone(options) : {};
914
1088
  options.collection = this;
915
1089
  var model = new this.model(attrs, options);
@@ -918,16 +1092,47 @@
918
1092
  return false;
919
1093
  },
920
1094
 
1095
+ // Internal method called by both remove and set.
1096
+ _removeModels: function(models, options) {
1097
+ var removed = [];
1098
+ for (var i = 0; i < models.length; i++) {
1099
+ var model = this.get(models[i]);
1100
+ if (!model) continue;
1101
+
1102
+ var index = this.indexOf(model);
1103
+ this.models.splice(index, 1);
1104
+ this.length--;
1105
+
1106
+ if (!options.silent) {
1107
+ options.index = index;
1108
+ model.trigger('remove', model, this, options);
1109
+ }
1110
+
1111
+ removed.push(model);
1112
+ this._removeReference(model, options);
1113
+ }
1114
+ return removed.length ? removed : false;
1115
+ },
1116
+
1117
+ // Method for checking whether an object should be considered a model for
1118
+ // the purposes of adding to the collection.
1119
+ _isModel: function (model) {
1120
+ return model instanceof Model;
1121
+ },
1122
+
921
1123
  // Internal method to create a model's ties to a collection.
922
1124
  _addReference: function(model, options) {
923
1125
  this._byId[model.cid] = model;
924
- if (model.id != null) this._byId[model.id] = model;
925
- if (!model.collection) model.collection = this;
1126
+ var id = this.modelId(model.attributes);
1127
+ if (id != null) this._byId[id] = model;
926
1128
  model.on('all', this._onModelEvent, this);
927
1129
  },
928
1130
 
929
1131
  // Internal method to sever a model's ties to a collection.
930
1132
  _removeReference: function(model, options) {
1133
+ delete this._byId[model.cid];
1134
+ var id = this.modelId(model.attributes);
1135
+ if (id != null) delete this._byId[id];
931
1136
  if (this === model.collection) delete model.collection;
932
1137
  model.off('all', this._onModelEvent, this);
933
1138
  },
@@ -939,9 +1144,13 @@
939
1144
  _onModelEvent: function(event, model, collection, options) {
940
1145
  if ((event === 'add' || event === 'remove') && collection !== this) return;
941
1146
  if (event === 'destroy') this.remove(model, options);
942
- if (model && event === 'change:' + model.idAttribute) {
943
- delete this._byId[model.previous(model.idAttribute)];
944
- if (model.id != null) this._byId[model.id] = model;
1147
+ if (event === 'change') {
1148
+ var prevId = this.modelId(model.previousAttributes());
1149
+ var id = this.modelId(model.attributes);
1150
+ if (prevId !== id) {
1151
+ if (prevId != null) delete this._byId[prevId];
1152
+ if (id != null) this._byId[id] = model;
1153
+ }
945
1154
  }
946
1155
  this.trigger.apply(this, arguments);
947
1156
  }
@@ -951,34 +1160,17 @@
951
1160
  // Underscore methods that we want to implement on the Collection.
952
1161
  // 90% of the core usefulness of Backbone Collections is actually implemented
953
1162
  // right here:
954
- var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
955
- 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
956
- 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
957
- 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
958
- 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
959
- 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
1163
+ var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
1164
+ foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
1165
+ select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
1166
+ contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
1167
+ head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
1168
+ without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
1169
+ isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
1170
+ sortBy: 3, indexBy: 3};
960
1171
 
961
1172
  // Mix in each Underscore method as a proxy to `Collection#models`.
962
- _.each(methods, function(method) {
963
- Collection.prototype[method] = function() {
964
- var args = slice.call(arguments);
965
- args.unshift(this.models);
966
- return _[method].apply(_, args);
967
- };
968
- });
969
-
970
- // Underscore methods that take a property name as an argument.
971
- var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
972
-
973
- // Use attributes instead of properties.
974
- _.each(attributeMethods, function(method) {
975
- Collection.prototype[method] = function(value, context) {
976
- var iterator = _.isFunction(value) ? value : function(model) {
977
- return model.get(value);
978
- };
979
- return _[method](this.models, iterator, context);
980
- };
981
- });
1173
+ addUnderscoreMethods(Collection, collectionMethods, 'models');
982
1174
 
983
1175
  // Backbone.View
984
1176
  // -------------
@@ -995,17 +1187,15 @@
995
1187
  // if an existing element is not provided...
996
1188
  var View = Backbone.View = function(options) {
997
1189
  this.cid = _.uniqueId('view');
998
- options || (options = {});
999
1190
  _.extend(this, _.pick(options, viewOptions));
1000
1191
  this._ensureElement();
1001
1192
  this.initialize.apply(this, arguments);
1002
- this.delegateEvents();
1003
1193
  };
1004
1194
 
1005
1195
  // Cached regex to split keys for `delegate`.
1006
1196
  var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1007
1197
 
1008
- // List of view options to be merged as properties.
1198
+ // List of view options to be set as properties.
1009
1199
  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1010
1200
 
1011
1201
  // Set up all inheritable **Backbone.View** properties and methods.
@@ -1034,21 +1224,37 @@
1034
1224
  // Remove this view by taking the element out of the DOM, and removing any
1035
1225
  // applicable Backbone.Events listeners.
1036
1226
  remove: function() {
1037
- this.$el.remove();
1227
+ this._removeElement();
1038
1228
  this.stopListening();
1039
1229
  return this;
1040
1230
  },
1041
1231
 
1042
- // Change the view's element (`this.el` property), including event
1043
- // re-delegation.
1044
- setElement: function(element, delegate) {
1045
- if (this.$el) this.undelegateEvents();
1046
- this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1047
- this.el = this.$el[0];
1048
- if (delegate !== false) this.delegateEvents();
1232
+ // Remove this view's element from the document and all event listeners
1233
+ // attached to it. Exposed for subclasses using an alternative DOM
1234
+ // manipulation API.
1235
+ _removeElement: function() {
1236
+ this.$el.remove();
1237
+ },
1238
+
1239
+ // Change the view's element (`this.el` property) and re-delegate the
1240
+ // view's events on the new element.
1241
+ setElement: function(element) {
1242
+ this.undelegateEvents();
1243
+ this._setElement(element);
1244
+ this.delegateEvents();
1049
1245
  return this;
1050
1246
  },
1051
1247
 
1248
+ // Creates the `this.el` and `this.$el` references for this view using the
1249
+ // given `el`. `el` can be a CSS selector or an HTML string, a jQuery
1250
+ // context or an element. Subclasses can override this to utilize an
1251
+ // alternative DOM manipulation API and are only required to set the
1252
+ // `this.el` property.
1253
+ _setElement: function(el) {
1254
+ this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
1255
+ this.el = this.$el[0];
1256
+ },
1257
+
1052
1258
  // Set callbacks, where `this.events` is a hash of
1053
1259
  //
1054
1260
  // *{"event selector": "callback"}*
@@ -1062,37 +1268,49 @@
1062
1268
  // pairs. Callbacks will be bound to the view, with `this` set properly.
1063
1269
  // Uses event delegation for efficiency.
1064
1270
  // Omitting the selector binds the event to `this.el`.
1065
- // This only works for delegate-able events: not `focus`, `blur`, and
1066
- // not `change`, `submit`, and `reset` in Internet Explorer.
1067
1271
  delegateEvents: function(events) {
1068
- if (!(events || (events = _.result(this, 'events')))) return this;
1272
+ events || (events = _.result(this, 'events'));
1273
+ if (!events) return this;
1069
1274
  this.undelegateEvents();
1070
1275
  for (var key in events) {
1071
1276
  var method = events[key];
1072
- if (!_.isFunction(method)) method = this[events[key]];
1277
+ if (!_.isFunction(method)) method = this[method];
1073
1278
  if (!method) continue;
1074
-
1075
1279
  var match = key.match(delegateEventSplitter);
1076
- var eventName = match[1], selector = match[2];
1077
- method = _.bind(method, this);
1078
- eventName += '.delegateEvents' + this.cid;
1079
- if (selector === '') {
1080
- this.$el.on(eventName, method);
1081
- } else {
1082
- this.$el.on(eventName, selector, method);
1083
- }
1280
+ this.delegate(match[1], match[2], _.bind(method, this));
1084
1281
  }
1085
1282
  return this;
1086
1283
  },
1087
1284
 
1088
- // Clears all callbacks previously bound to the view with `delegateEvents`.
1285
+ // Add a single event listener to the view's element (or a child element
1286
+ // using `selector`). This only works for delegate-able events: not `focus`,
1287
+ // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
1288
+ delegate: function(eventName, selector, listener) {
1289
+ this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
1290
+ return this;
1291
+ },
1292
+
1293
+ // Clears all callbacks previously bound to the view by `delegateEvents`.
1089
1294
  // You usually don't need to use this, but may wish to if you have multiple
1090
1295
  // Backbone views attached to the same DOM element.
1091
1296
  undelegateEvents: function() {
1092
- this.$el.off('.delegateEvents' + this.cid);
1297
+ if (this.$el) this.$el.off('.delegateEvents' + this.cid);
1298
+ return this;
1299
+ },
1300
+
1301
+ // A finer-grained `undelegateEvents` for removing a single delegated event.
1302
+ // `selector` and `listener` are both optional.
1303
+ undelegate: function(eventName, selector, listener) {
1304
+ this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
1093
1305
  return this;
1094
1306
  },
1095
1307
 
1308
+ // Produces a DOM element to be assigned to your view. Exposed for
1309
+ // subclasses using an alternative DOM manipulation API.
1310
+ _createElement: function(tagName) {
1311
+ return document.createElement(tagName);
1312
+ },
1313
+
1096
1314
  // Ensure that the View has a DOM element to render into.
1097
1315
  // If `this.el` is a string, pass it through `$()`, take the first
1098
1316
  // matching element, and re-assign it to `el`. Otherwise, create
@@ -1102,11 +1320,17 @@
1102
1320
  var attrs = _.extend({}, _.result(this, 'attributes'));
1103
1321
  if (this.id) attrs.id = _.result(this, 'id');
1104
1322
  if (this.className) attrs['class'] = _.result(this, 'className');
1105
- var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1106
- this.setElement($el, false);
1323
+ this.setElement(this._createElement(_.result(this, 'tagName')));
1324
+ this._setAttributes(attrs);
1107
1325
  } else {
1108
- this.setElement(_.result(this, 'el'), false);
1326
+ this.setElement(_.result(this, 'el'));
1109
1327
  }
1328
+ },
1329
+
1330
+ // Set attributes from a hash on this view's element. Exposed for
1331
+ // subclasses using an alternative DOM manipulation API.
1332
+ _setAttributes: function(attributes) {
1333
+ this.$el.attr(attributes);
1110
1334
  }
1111
1335
 
1112
1336
  });
@@ -1175,14 +1399,13 @@
1175
1399
  params.processData = false;
1176
1400
  }
1177
1401
 
1178
- // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1179
- // that still has ActiveX enabled by default, override jQuery to use that
1180
- // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
1181
- if (params.type === 'PATCH' && noXhrPatch) {
1182
- params.xhr = function() {
1183
- return new ActiveXObject("Microsoft.XMLHTTP");
1184
- };
1185
- }
1402
+ // Pass along `textStatus` and `errorThrown` from jQuery.
1403
+ var error = options.error;
1404
+ options.error = function(xhr, textStatus, errorThrown) {
1405
+ options.textStatus = textStatus;
1406
+ options.errorThrown = errorThrown;
1407
+ if (error) error.call(options.context, xhr, textStatus, errorThrown);
1408
+ };
1186
1409
 
1187
1410
  // Make the request, allowing the user to override any Ajax options.
1188
1411
  var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
@@ -1190,10 +1413,6 @@
1190
1413
  return xhr;
1191
1414
  };
1192
1415
 
1193
- var noXhrPatch =
1194
- typeof window !== 'undefined' && !!window.ActiveXObject &&
1195
- !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
1196
-
1197
1416
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1198
1417
  var methodMap = {
1199
1418
  'create': 'POST',
@@ -1251,17 +1470,18 @@
1251
1470
  var router = this;
1252
1471
  Backbone.history.route(route, function(fragment) {
1253
1472
  var args = router._extractParameters(route, fragment);
1254
- router.execute(callback, args);
1255
- router.trigger.apply(router, ['route:' + name].concat(args));
1256
- router.trigger('route', name, args);
1257
- Backbone.history.trigger('route', router, name, args);
1473
+ if (router.execute(callback, args, name) !== false) {
1474
+ router.trigger.apply(router, ['route:' + name].concat(args));
1475
+ router.trigger('route', name, args);
1476
+ Backbone.history.trigger('route', router, name, args);
1477
+ }
1258
1478
  });
1259
1479
  return this;
1260
1480
  },
1261
1481
 
1262
1482
  // Execute a route handler with the provided parameters. This is an
1263
1483
  // excellent place to do pre-route setup or post-route cleanup.
1264
- execute: function(callback, args) {
1484
+ execute: function(callback, args, name) {
1265
1485
  if (callback) callback.apply(this, args);
1266
1486
  },
1267
1487
 
@@ -1319,7 +1539,7 @@
1319
1539
  // falls back to polling.
1320
1540
  var History = Backbone.History = function() {
1321
1541
  this.handlers = [];
1322
- _.bindAll(this, 'checkUrl');
1542
+ this.checkUrl = _.bind(this.checkUrl, this);
1323
1543
 
1324
1544
  // Ensure that `History` can be used outside of the browser.
1325
1545
  if (typeof window !== 'undefined') {
@@ -1334,12 +1554,6 @@
1334
1554
  // Cached regex for stripping leading and trailing slashes.
1335
1555
  var rootStripper = /^\/+|\/+$/g;
1336
1556
 
1337
- // Cached regex for detecting MSIE.
1338
- var isExplorer = /msie [\w.]+/;
1339
-
1340
- // Cached regex for removing a trailing slash.
1341
- var trailingSlash = /\/$/;
1342
-
1343
1557
  // Cached regex for stripping urls of hash.
1344
1558
  var pathStripper = /#.*$/;
1345
1559
 
@@ -1355,7 +1569,29 @@
1355
1569
 
1356
1570
  // Are we at the app root?
1357
1571
  atRoot: function() {
1358
- return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
1572
+ var path = this.location.pathname.replace(/[^\/]$/, '$&/');
1573
+ return path === this.root && !this.getSearch();
1574
+ },
1575
+
1576
+ // Does the pathname match the root?
1577
+ matchRoot: function() {
1578
+ var path = this.decodeFragment(this.location.pathname);
1579
+ var root = path.slice(0, this.root.length - 1) + '/';
1580
+ return root === this.root;
1581
+ },
1582
+
1583
+ // Unicode characters in `location.pathname` are percent encoded so they're
1584
+ // decoded for comparison. `%25` should not be decoded since it may be part
1585
+ // of an encoded parameter.
1586
+ decodeFragment: function(fragment) {
1587
+ return decodeURI(fragment.replace(/%25/g, '%2525'));
1588
+ },
1589
+
1590
+ // In IE6, the hash fragment and search params are incorrect if the
1591
+ // fragment contains `?`.
1592
+ getSearch: function() {
1593
+ var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
1594
+ return match ? match[0] : '';
1359
1595
  },
1360
1596
 
1361
1597
  // Gets the true hash value. Cannot use location.hash directly due to bug
@@ -1365,14 +1601,19 @@
1365
1601
  return match ? match[1] : '';
1366
1602
  },
1367
1603
 
1368
- // Get the cross-browser normalized URL fragment, either from the URL,
1369
- // the hash, or the override.
1370
- getFragment: function(fragment, forcePushState) {
1604
+ // Get the pathname and search params, without the root.
1605
+ getPath: function() {
1606
+ var path = this.decodeFragment(
1607
+ this.location.pathname + this.getSearch()
1608
+ ).slice(this.root.length - 1);
1609
+ return path.charAt(0) === '/' ? path.slice(1) : path;
1610
+ },
1611
+
1612
+ // Get the cross-browser normalized URL fragment from the path or hash.
1613
+ getFragment: function(fragment) {
1371
1614
  if (fragment == null) {
1372
- if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1373
- fragment = decodeURI(this.location.pathname + this.location.search);
1374
- var root = this.root.replace(trailingSlash, '');
1375
- if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1615
+ if (this._usePushState || !this._wantsHashChange) {
1616
+ fragment = this.getPath();
1376
1617
  } else {
1377
1618
  fragment = this.getHash();
1378
1619
  }
@@ -1383,7 +1624,7 @@
1383
1624
  // Start the hash change handling, returning `true` if the current URL matches
1384
1625
  // an existing route, and `false` otherwise.
1385
1626
  start: function(options) {
1386
- if (History.started) throw new Error("Backbone.history has already been started");
1627
+ if (History.started) throw new Error('Backbone.history has already been started');
1387
1628
  History.started = true;
1388
1629
 
1389
1630
  // Figure out the initial configuration. Do we need an iframe?
@@ -1391,36 +1632,16 @@
1391
1632
  this.options = _.extend({root: '/'}, this.options, options);
1392
1633
  this.root = this.options.root;
1393
1634
  this._wantsHashChange = this.options.hashChange !== false;
1635
+ this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
1636
+ this._useHashChange = this._wantsHashChange && this._hasHashChange;
1394
1637
  this._wantsPushState = !!this.options.pushState;
1395
- this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1396
- var fragment = this.getFragment();
1397
- var docMode = document.documentMode;
1398
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1638
+ this._hasPushState = !!(this.history && this.history.pushState);
1639
+ this._usePushState = this._wantsPushState && this._hasPushState;
1640
+ this.fragment = this.getFragment();
1399
1641
 
1400
1642
  // Normalize root to always include a leading and trailing slash.
1401
1643
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1402
1644
 
1403
- if (oldIE && this._wantsHashChange) {
1404
- var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
1405
- this.iframe = frame.hide().appendTo('body')[0].contentWindow;
1406
- this.navigate(fragment);
1407
- }
1408
-
1409
- // Depending on whether we're using pushState or hashes, and whether
1410
- // 'onhashchange' is supported, determine how we check the URL state.
1411
- if (this._hasPushState) {
1412
- Backbone.$(window).on('popstate', this.checkUrl);
1413
- } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1414
- Backbone.$(window).on('hashchange', this.checkUrl);
1415
- } else if (this._wantsHashChange) {
1416
- this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1417
- }
1418
-
1419
- // Determine if we need to change the base url, for a pushState link
1420
- // opened by a non-pushState browser.
1421
- this.fragment = fragment;
1422
- var loc = this.location;
1423
-
1424
1645
  // Transition from hashChange to pushState or vice versa if both are
1425
1646
  // requested.
1426
1647
  if (this._wantsHashChange && this._wantsPushState) {
@@ -1428,27 +1649,75 @@
1428
1649
  // If we've started off with a route from a `pushState`-enabled
1429
1650
  // browser, but we're currently in a browser that doesn't support it...
1430
1651
  if (!this._hasPushState && !this.atRoot()) {
1431
- this.fragment = this.getFragment(null, true);
1432
- this.location.replace(this.root + '#' + this.fragment);
1652
+ var root = this.root.slice(0, -1) || '/';
1653
+ this.location.replace(root + '#' + this.getPath());
1433
1654
  // Return immediately as browser will do redirect to new url
1434
1655
  return true;
1435
1656
 
1436
1657
  // Or if we've started out with a hash-based route, but we're currently
1437
1658
  // in a browser where it could be `pushState`-based instead...
1438
- } else if (this._hasPushState && this.atRoot() && loc.hash) {
1439
- this.fragment = this.getHash().replace(routeStripper, '');
1440
- this.history.replaceState({}, document.title, this.root + this.fragment);
1659
+ } else if (this._hasPushState && this.atRoot()) {
1660
+ this.navigate(this.getHash(), {replace: true});
1441
1661
  }
1442
1662
 
1443
1663
  }
1444
1664
 
1665
+ // Proxy an iframe to handle location events if the browser doesn't
1666
+ // support the `hashchange` event, HTML5 history, or the user wants
1667
+ // `hashChange` but not `pushState`.
1668
+ if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
1669
+ this.iframe = document.createElement('iframe');
1670
+ this.iframe.src = 'javascript:0';
1671
+ this.iframe.style.display = 'none';
1672
+ this.iframe.tabIndex = -1;
1673
+ var body = document.body;
1674
+ // Using `appendChild` will throw on IE < 9 if the document is not ready.
1675
+ var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
1676
+ iWindow.document.open();
1677
+ iWindow.document.close();
1678
+ iWindow.location.hash = '#' + this.fragment;
1679
+ }
1680
+
1681
+ // Add a cross-platform `addEventListener` shim for older browsers.
1682
+ var addEventListener = window.addEventListener || function (eventName, listener) {
1683
+ return attachEvent('on' + eventName, listener);
1684
+ };
1685
+
1686
+ // Depending on whether we're using pushState or hashes, and whether
1687
+ // 'onhashchange' is supported, determine how we check the URL state.
1688
+ if (this._usePushState) {
1689
+ addEventListener('popstate', this.checkUrl, false);
1690
+ } else if (this._useHashChange && !this.iframe) {
1691
+ addEventListener('hashchange', this.checkUrl, false);
1692
+ } else if (this._wantsHashChange) {
1693
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1694
+ }
1695
+
1445
1696
  if (!this.options.silent) return this.loadUrl();
1446
1697
  },
1447
1698
 
1448
1699
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1449
1700
  // but possibly useful for unit testing Routers.
1450
1701
  stop: function() {
1451
- Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1702
+ // Add a cross-platform `removeEventListener` shim for older browsers.
1703
+ var removeEventListener = window.removeEventListener || function (eventName, listener) {
1704
+ return detachEvent('on' + eventName, listener);
1705
+ };
1706
+
1707
+ // Remove window listeners.
1708
+ if (this._usePushState) {
1709
+ removeEventListener('popstate', this.checkUrl, false);
1710
+ } else if (this._useHashChange && !this.iframe) {
1711
+ removeEventListener('hashchange', this.checkUrl, false);
1712
+ }
1713
+
1714
+ // Clean up the iframe if necessary.
1715
+ if (this.iframe) {
1716
+ document.body.removeChild(this.iframe);
1717
+ this.iframe = null;
1718
+ }
1719
+
1720
+ // Some environments will throw when clearing an undefined interval.
1452
1721
  if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
1453
1722
  History.started = false;
1454
1723
  },
@@ -1463,9 +1732,13 @@
1463
1732
  // calls `loadUrl`, normalizing across the hidden iframe.
1464
1733
  checkUrl: function(e) {
1465
1734
  var current = this.getFragment();
1735
+
1736
+ // If the user pressed the back button, the iframe's hash will have
1737
+ // changed and we should use that for comparison.
1466
1738
  if (current === this.fragment && this.iframe) {
1467
- current = this.getFragment(this.getHash(this.iframe));
1739
+ current = this.getHash(this.iframe.contentWindow);
1468
1740
  }
1741
+
1469
1742
  if (current === this.fragment) return false;
1470
1743
  if (this.iframe) this.navigate(current);
1471
1744
  this.loadUrl();
@@ -1475,8 +1748,10 @@
1475
1748
  // match, returns `true`. If no defined routes matches the fragment,
1476
1749
  // returns `false`.
1477
1750
  loadUrl: function(fragment) {
1751
+ // If the root doesn't match, no routes can match either.
1752
+ if (!this.matchRoot()) return false;
1478
1753
  fragment = this.fragment = this.getFragment(fragment);
1479
- return _.any(this.handlers, function(handler) {
1754
+ return _.some(this.handlers, function(handler) {
1480
1755
  if (handler.route.test(fragment)) {
1481
1756
  handler.callback(fragment);
1482
1757
  return true;
@@ -1495,31 +1770,42 @@
1495
1770
  if (!History.started) return false;
1496
1771
  if (!options || options === true) options = {trigger: !!options};
1497
1772
 
1498
- var url = this.root + (fragment = this.getFragment(fragment || ''));
1773
+ // Normalize the fragment.
1774
+ fragment = this.getFragment(fragment || '');
1499
1775
 
1500
- // Strip the hash for matching.
1501
- fragment = fragment.replace(pathStripper, '');
1776
+ // Don't include a trailing slash on the root.
1777
+ var root = this.root;
1778
+ if (fragment === '' || fragment.charAt(0) === '?') {
1779
+ root = root.slice(0, -1) || '/';
1780
+ }
1781
+ var url = root + fragment;
1782
+
1783
+ // Strip the hash and decode for matching.
1784
+ fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
1502
1785
 
1503
1786
  if (this.fragment === fragment) return;
1504
1787
  this.fragment = fragment;
1505
1788
 
1506
- // Don't include a trailing slash on the root.
1507
- if (fragment === '' && url !== '/') url = url.slice(0, -1);
1508
-
1509
1789
  // If pushState is available, we use it to set the fragment as a real URL.
1510
- if (this._hasPushState) {
1790
+ if (this._usePushState) {
1511
1791
  this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1512
1792
 
1513
1793
  // If hash changes haven't been explicitly disabled, update the hash
1514
1794
  // fragment to store history.
1515
1795
  } else if (this._wantsHashChange) {
1516
1796
  this._updateHash(this.location, fragment, options.replace);
1517
- if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1797
+ if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
1798
+ var iWindow = this.iframe.contentWindow;
1799
+
1518
1800
  // Opening and closing the iframe tricks IE7 and earlier to push a
1519
1801
  // history entry on hash-tag change. When replace is true, we don't
1520
1802
  // want this.
1521
- if(!options.replace) this.iframe.document.open().close();
1522
- this._updateHash(this.iframe.location, fragment, options.replace);
1803
+ if (!options.replace) {
1804
+ iWindow.document.open();
1805
+ iWindow.document.close();
1806
+ }
1807
+
1808
+ this._updateHash(iWindow.location, fragment, options.replace);
1523
1809
  }
1524
1810
 
1525
1811
  // If you've told us that you explicitly don't want fallback hashchange-
@@ -1550,7 +1836,7 @@
1550
1836
  // Helpers
1551
1837
  // -------
1552
1838
 
1553
- // Helper function to correctly set up the prototype chain, for subclasses.
1839
+ // Helper function to correctly set up the prototype chain for subclasses.
1554
1840
  // Similar to `goog.inherits`, but uses a hash of prototype properties and
1555
1841
  // class properties to be extended.
1556
1842
  var extend = function(protoProps, staticProps) {
@@ -1559,7 +1845,7 @@
1559
1845
 
1560
1846
  // The constructor function for the new subclass is either defined by you
1561
1847
  // (the "constructor" property in your `extend` definition), or defaulted
1562
- // by us to simply call the parent's constructor.
1848
+ // by us to simply call the parent constructor.
1563
1849
  if (protoProps && _.has(protoProps, 'constructor')) {
1564
1850
  child = protoProps.constructor;
1565
1851
  } else {
@@ -1570,7 +1856,7 @@
1570
1856
  _.extend(child, parent, staticProps);
1571
1857
 
1572
1858
  // Set the prototype chain to inherit from `parent`, without calling
1573
- // `parent`'s constructor function.
1859
+ // `parent` constructor function.
1574
1860
  var Surrogate = function(){ this.constructor = child; };
1575
1861
  Surrogate.prototype = parent.prototype;
1576
1862
  child.prototype = new Surrogate;
@@ -1598,7 +1884,7 @@
1598
1884
  var wrapError = function(model, options) {
1599
1885
  var error = options.error;
1600
1886
  options.error = function(resp) {
1601
- if (error) error(model, resp, options);
1887
+ if (error) error.call(options.context, model, resp, options);
1602
1888
  model.trigger('error', model, resp, options);
1603
1889
  };
1604
1890
  };