backbone-rails 0.5.3.1 → 0.9.0

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.
@@ -1,20 +1,26 @@
1
- // Backbone.js 0.5.3
2
- // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
1
+ // Backbone.js 0.9.0
2
+ // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
3
3
  // Backbone may be freely distributed under the MIT license.
4
4
  // For all details and documentation:
5
- // http://documentcloud.github.com/backbone
5
+ // http://backbonejs.org
6
6
 
7
7
  (function(){
8
8
 
9
9
  // Initial Setup
10
10
  // -------------
11
11
 
12
- // Save a reference to the global object.
12
+ // Save a reference to the global object (`window` in the browser, `global`
13
+ // on the server).
13
14
  var root = this;
14
15
 
15
- // Save the previous value of the `Backbone` variable.
16
+ // Save the previous value of the `Backbone` variable, so that it can be
17
+ // restored later on, if `noConflict` is used.
16
18
  var previousBackbone = root.Backbone;
17
19
 
20
+ // Create a local reference to slice/splice.
21
+ var slice = Array.prototype.slice;
22
+ var splice = Array.prototype.splice;
23
+
18
24
  // The top-level namespace. All public Backbone classes and modules will
19
25
  // be attached to this. Exported for both CommonJS and the browser.
20
26
  var Backbone;
@@ -25,14 +31,14 @@
25
31
  }
26
32
 
27
33
  // Current version of the library. Keep in sync with `package.json`.
28
- Backbone.VERSION = '0.5.3';
34
+ Backbone.VERSION = '0.9.0';
29
35
 
30
36
  // Require Underscore, if we're on the server, and it's not already present.
31
37
  var _ = root._;
32
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
38
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
33
39
 
34
- // For Backbone's purposes, jQuery or Zepto owns the `$` variable.
35
- var $ = root.jQuery || root.Zepto;
40
+ // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
41
+ var $ = root.jQuery || root.Zepto || root.ender;
36
42
 
37
43
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
38
44
  // to its previous owner. Returns a reference to this Backbone object.
@@ -41,9 +47,9 @@
41
47
  return this;
42
48
  };
43
49
 
44
- // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
45
- // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
46
- // `X-Http-Method-Override` header.
50
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
51
+ // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
52
+ // set a `X-Http-Method-Override` header.
47
53
  Backbone.emulateHTTP = false;
48
54
 
49
55
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
@@ -56,43 +62,53 @@
56
62
  // -----------------
57
63
 
58
64
  // A module that can be mixed in to *any object* in order to provide it with
59
- // custom events. You may `bind` or `unbind` a callback function to an event;
60
- // `trigger`-ing an event fires all callbacks in succession.
65
+ // custom events. You may bind with `on` or remove with `off` callback functions
66
+ // to an event; trigger`-ing an event fires all callbacks in succession.
61
67
  //
62
68
  // var object = {};
63
69
  // _.extend(object, Backbone.Events);
64
- // object.bind('expand', function(){ alert('expanded'); });
70
+ // object.on('expand', function(){ alert('expanded'); });
65
71
  // object.trigger('expand');
66
72
  //
67
73
  Backbone.Events = {
68
74
 
69
- // Bind an event, specified by a string name, `ev`, to a `callback` function.
70
- // Passing `"all"` will bind the callback to all events fired.
71
- bind : function(ev, callback, context) {
75
+ // Bind an event, specified by a string name, `ev`, to a `callback`
76
+ // function. Passing `"all"` will bind the callback to all events fired.
77
+ on: function(events, callback, context) {
78
+ var ev;
79
+ events = events.split(/\s+/);
72
80
  var calls = this._callbacks || (this._callbacks = {});
73
- var list = calls[ev] || (calls[ev] = []);
74
- list.push([callback, context]);
81
+ while (ev = events.shift()) {
82
+ // Create an immutable callback list, allowing traversal during
83
+ // modification. The tail is an empty object that will always be used
84
+ // as the next node.
85
+ var list = calls[ev] || (calls[ev] = {});
86
+ var tail = list.tail || (list.tail = list.next = {});
87
+ tail.callback = callback;
88
+ tail.context = context;
89
+ list.tail = tail.next = {};
90
+ }
75
91
  return this;
76
92
  },
77
93
 
78
- // Remove one or many callbacks. If `callback` is null, removes all
79
- // callbacks for the event. If `ev` is null, removes all bound callbacks
80
- // for all events.
81
- unbind : function(ev, callback) {
82
- var calls;
83
- if (!ev) {
84
- this._callbacks = {};
94
+ // Remove one or many callbacks. If `context` is null, removes all callbacks
95
+ // with that function. If `callback` is null, removes all callbacks for the
96
+ // event. If `ev` is null, removes all bound callbacks for all events.
97
+ off: function(events, callback, context) {
98
+ var ev, calls, node;
99
+ if (!events) {
100
+ delete this._callbacks;
85
101
  } else if (calls = this._callbacks) {
86
- if (!callback) {
87
- calls[ev] = [];
88
- } else {
89
- var list = calls[ev];
90
- if (!list) return this;
91
- for (var i = 0, l = list.length; i < l; i++) {
92
- if (list[i] && callback === list[i][0]) {
93
- list[i] = null;
94
- break;
95
- }
102
+ events = events.split(/\s+/);
103
+ while (ev = events.shift()) {
104
+ node = calls[ev];
105
+ delete calls[ev];
106
+ if (!callback || !node) continue;
107
+ // Create a new list, omitting the indicated event/context pairs.
108
+ while ((node = node.next) && node.next) {
109
+ if (node.callback === callback &&
110
+ (!context || node.context === context)) continue;
111
+ this.on(ev, node.callback, node.context);
96
112
  }
97
113
  }
98
114
  }
@@ -102,21 +118,24 @@
102
118
  // Trigger an event, firing all bound callbacks. Callbacks are passed the
103
119
  // same arguments as `trigger` is, apart from the event name.
104
120
  // Listening for `"all"` passes the true event name as the first argument.
105
- trigger : function(eventName) {
106
- var list, calls, ev, callback, args;
107
- var both = 2;
121
+ trigger: function(events) {
122
+ var event, node, calls, tail, args, all, rest;
108
123
  if (!(calls = this._callbacks)) return this;
109
- while (both--) {
110
- ev = both ? eventName : 'all';
111
- if (list = calls[ev]) {
112
- for (var i = 0, l = list.length; i < l; i++) {
113
- if (!(callback = list[i])) {
114
- list.splice(i, 1); i--; l--;
115
- } else {
116
- args = both ? Array.prototype.slice.call(arguments, 1) : arguments;
117
- callback[0].apply(callback[1] || this, args);
118
- }
119
- }
124
+ all = calls['all'];
125
+ (events = events.split(/\s+/)).push(null);
126
+ // Save references to the current heads & tails.
127
+ while (event = events.shift()) {
128
+ if (all) events.push({next: all.next, tail: all.tail, event: event});
129
+ if (!(node = calls[event])) continue;
130
+ events.push({next: node.next, tail: node.tail});
131
+ }
132
+ // Traverse each list, stopping when the saved tail is reached.
133
+ rest = slice.call(arguments, 1);
134
+ while (node = events.pop()) {
135
+ tail = node.tail;
136
+ args = node.event ? [node.event].concat(rest) : rest;
137
+ while ((node = node.next) !== tail) {
138
+ node.callback.apply(node.context || this, args);
120
139
  }
121
140
  }
122
141
  return this;
@@ -124,6 +143,10 @@
124
143
 
125
144
  };
126
145
 
146
+ // Aliases for backwards compatibility.
147
+ Backbone.Events.bind = Backbone.Events.on;
148
+ Backbone.Events.unbind = Backbone.Events.off;
149
+
127
150
  // Backbone.Model
128
151
  // --------------
129
152
 
@@ -132,269 +155,274 @@
132
155
  Backbone.Model = function(attributes, options) {
133
156
  var defaults;
134
157
  attributes || (attributes = {});
135
- if (defaults = this.defaults) {
136
- if (_.isFunction(defaults)) defaults = defaults.call(this);
158
+ if (options && options.parse) attributes = this.parse(attributes);
159
+ if (defaults = getValue(this, 'defaults')) {
137
160
  attributes = _.extend({}, defaults, attributes);
138
161
  }
162
+ if (options && options.collection) this.collection = options.collection;
139
163
  this.attributes = {};
140
164
  this._escapedAttributes = {};
141
165
  this.cid = _.uniqueId('c');
142
- this.set(attributes, {silent : true});
143
- this._changed = false;
166
+ this._changed = {};
167
+ if (!this.set(attributes, {silent: true})) {
168
+ throw new Error("Can't create an invalid model");
169
+ }
170
+ this._changed = {};
144
171
  this._previousAttributes = _.clone(this.attributes);
145
- if (options && options.collection) this.collection = options.collection;
146
- this.initialize(attributes, options);
172
+ this.initialize.apply(this, arguments);
147
173
  };
148
174
 
149
175
  // Attach all inheritable methods to the Model prototype.
150
176
  _.extend(Backbone.Model.prototype, Backbone.Events, {
151
177
 
152
- // A snapshot of the model's previous attributes, taken immediately
153
- // after the last `"change"` event was fired.
154
- _previousAttributes : null,
155
-
156
- // Has the item been changed since the last `"change"` event?
157
- _changed : false,
158
-
159
178
  // The default name for the JSON `id` attribute is `"id"`. MongoDB and
160
179
  // CouchDB users may want to set this to `"_id"`.
161
- idAttribute : 'id',
180
+ idAttribute: 'id',
162
181
 
163
182
  // Initialize is an empty function by default. Override it with your own
164
183
  // initialization logic.
165
- initialize : function(){},
184
+ initialize: function(){},
166
185
 
167
186
  // Return a copy of the model's `attributes` object.
168
- toJSON : function() {
187
+ toJSON: function() {
169
188
  return _.clone(this.attributes);
170
189
  },
171
190
 
172
191
  // Get the value of an attribute.
173
- get : function(attr) {
192
+ get: function(attr) {
174
193
  return this.attributes[attr];
175
194
  },
176
195
 
177
196
  // Get the HTML-escaped value of an attribute.
178
- escape : function(attr) {
197
+ escape: function(attr) {
179
198
  var html;
180
199
  if (html = this._escapedAttributes[attr]) return html;
181
200
  var val = this.attributes[attr];
182
- return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
201
+ return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
183
202
  },
184
203
 
185
204
  // Returns `true` if the attribute contains a value that is not null
186
205
  // or undefined.
187
- has : function(attr) {
206
+ has: function(attr) {
188
207
  return this.attributes[attr] != null;
189
208
  },
190
209
 
191
- // Set a hash of model attributes on the object, firing `"change"` unless you
192
- // choose to silence it.
193
- set : function(attrs, options) {
210
+ // Set a hash of model attributes on the object, firing `"change"` unless
211
+ // you choose to silence it.
212
+ set: function(key, value, options) {
213
+ var attrs, attr, val;
214
+ if (_.isObject(key) || key == null) {
215
+ attrs = key;
216
+ options = value;
217
+ } else {
218
+ attrs = {};
219
+ attrs[key] = value;
220
+ }
194
221
 
195
222
  // Extract attributes and options.
196
223
  options || (options = {});
197
224
  if (!attrs) return this;
198
- if (attrs.attributes) attrs = attrs.attributes;
199
- var now = this.attributes, escaped = this._escapedAttributes;
225
+ if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
226
+ if (options.unset) for (var attr in attrs) attrs[attr] = void 0;
200
227
 
201
228
  // Run validation.
202
- if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
229
+ if (this.validate && !this._performValidation(attrs, options)) return false;
203
230
 
204
231
  // Check for changes of `id`.
205
232
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
206
233
 
207
- // We're about to start triggering change events.
234
+ var now = this.attributes;
235
+ var escaped = this._escapedAttributes;
236
+ var prev = this._previousAttributes || {};
208
237
  var alreadyChanging = this._changing;
209
238
  this._changing = true;
210
239
 
211
240
  // Update attributes.
212
- for (var attr in attrs) {
213
- var val = attrs[attr];
214
- if (!_.isEqual(now[attr], val)) {
215
- now[attr] = val;
216
- delete escaped[attr];
217
- this._changed = true;
218
- if (!options.silent) this.trigger('change:' + attr, this, val, options);
241
+ for (attr in attrs) {
242
+ val = attrs[attr];
243
+ if (!_.isEqual(now[attr], val)) delete escaped[attr];
244
+ options.unset ? delete now[attr] : now[attr] = val;
245
+ delete this._changed[attr];
246
+ if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
247
+ this._changed[attr] = val;
219
248
  }
220
249
  }
221
250
 
222
- // Fire the `"change"` event, if the model has been changed.
223
- if (!alreadyChanging && !options.silent && this._changed) this.change(options);
224
- this._changing = false;
251
+ // Fire the `"change"` events, if the model has been changed.
252
+ if (!alreadyChanging) {
253
+ if (!options.silent && this.hasChanged()) this.change(options);
254
+ this._changing = false;
255
+ }
225
256
  return this;
226
257
  },
227
258
 
228
259
  // Remove an attribute from the model, firing `"change"` unless you choose
229
260
  // to silence it. `unset` is a noop if the attribute doesn't exist.
230
- unset : function(attr, options) {
231
- if (!(attr in this.attributes)) return this;
232
- options || (options = {});
233
- var value = this.attributes[attr];
234
-
235
- // Run validation.
236
- var validObj = {};
237
- validObj[attr] = void 0;
238
- if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
239
-
240
- // Remove the attribute.
241
- delete this.attributes[attr];
242
- delete this._escapedAttributes[attr];
243
- if (attr == this.idAttribute) delete this.id;
244
- this._changed = true;
245
- if (!options.silent) {
246
- this.trigger('change:' + attr, this, void 0, options);
247
- this.change(options);
248
- }
249
- return this;
261
+ unset: function(attr, options) {
262
+ (options || (options = {})).unset = true;
263
+ return this.set(attr, null, options);
250
264
  },
251
265
 
252
266
  // Clear all attributes on the model, firing `"change"` unless you choose
253
267
  // to silence it.
254
- clear : function(options) {
255
- options || (options = {});
256
- var attr;
257
- var old = this.attributes;
258
-
259
- // Run validation.
260
- var validObj = {};
261
- for (attr in old) validObj[attr] = void 0;
262
- if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
263
-
264
- this.attributes = {};
265
- this._escapedAttributes = {};
266
- this._changed = true;
267
- if (!options.silent) {
268
- for (attr in old) {
269
- this.trigger('change:' + attr, this, void 0, options);
270
- }
271
- this.change(options);
272
- }
273
- return this;
268
+ clear: function(options) {
269
+ (options || (options = {})).unset = true;
270
+ return this.set(_.clone(this.attributes), options);
274
271
  },
275
272
 
276
273
  // Fetch the model from the server. If the server's representation of the
277
274
  // model differs from its current attributes, they will be overriden,
278
275
  // triggering a `"change"` event.
279
- fetch : function(options) {
280
- options || (options = {});
276
+ fetch: function(options) {
277
+ options = options ? _.clone(options) : {};
281
278
  var model = this;
282
279
  var success = options.success;
283
280
  options.success = function(resp, status, xhr) {
284
281
  if (!model.set(model.parse(resp, xhr), options)) return false;
285
282
  if (success) success(model, resp);
286
283
  };
287
- options.error = wrapError(options.error, model, options);
284
+ options.error = Backbone.wrapError(options.error, model, options);
288
285
  return (this.sync || Backbone.sync).call(this, 'read', this, options);
289
286
  },
290
287
 
291
288
  // Set a hash of model attributes, and sync the model to the server.
292
289
  // If the server returns an attributes hash that differs, the model's
293
290
  // state will be `set` again.
294
- save : function(attrs, options) {
295
- options || (options = {});
296
- if (attrs && !this.set(attrs, options)) return false;
291
+ save: function(key, value, options) {
292
+ var attrs;
293
+ if (_.isObject(key) || key == null) {
294
+ attrs = key;
295
+ options = value;
296
+ } else {
297
+ attrs = {};
298
+ attrs[key] = value;
299
+ }
300
+
301
+ options = options ? _.clone(options) : {};
302
+ if (attrs && !this[options.wait ? '_performValidation' : 'set'](attrs, options)) return false;
297
303
  var model = this;
298
304
  var success = options.success;
299
305
  options.success = function(resp, status, xhr) {
300
- if (!model.set(model.parse(resp, xhr), options)) return false;
301
- if (success) success(model, resp, xhr);
306
+ var serverAttrs = model.parse(resp, xhr);
307
+ if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
308
+ if (!model.set(serverAttrs, options)) return false;
309
+ if (success) {
310
+ success(model, resp);
311
+ } else {
312
+ model.trigger('sync', model, resp, options);
313
+ }
302
314
  };
303
- options.error = wrapError(options.error, model, options);
315
+ options.error = Backbone.wrapError(options.error, model, options);
304
316
  var method = this.isNew() ? 'create' : 'update';
305
317
  return (this.sync || Backbone.sync).call(this, method, this, options);
306
318
  },
307
319
 
308
- // Destroy this model on the server if it was already persisted. Upon success, the model is removed
309
- // from its collection, if it has one.
310
- destroy : function(options) {
311
- options || (options = {});
312
- if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
320
+ // Destroy this model on the server if it was already persisted.
321
+ // Optimistically removes the model from its collection, if it has one.
322
+ // If `wait: true` is passed, waits for the server to respond before removal.
323
+ destroy: function(options) {
324
+ options = options ? _.clone(options) : {};
313
325
  var model = this;
314
326
  var success = options.success;
315
- options.success = function(resp) {
327
+
328
+ var triggerDestroy = function() {
316
329
  model.trigger('destroy', model, model.collection, options);
317
- if (success) success(model, resp);
318
330
  };
319
- options.error = wrapError(options.error, model, options);
320
- return (this.sync || Backbone.sync).call(this, 'delete', this, options);
331
+
332
+ if (this.isNew()) return triggerDestroy();
333
+ options.success = function(resp) {
334
+ if (options.wait) triggerDestroy();
335
+ if (success) {
336
+ success(model, resp);
337
+ } else {
338
+ model.trigger('sync', model, resp, options);
339
+ }
340
+ };
341
+ options.error = Backbone.wrapError(options.error, model, options);
342
+ var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
343
+ if (!options.wait) triggerDestroy();
344
+ return xhr;
321
345
  },
322
346
 
323
347
  // Default URL for the model's representation on the server -- if you're
324
348
  // using Backbone's restful methods, override this to change the endpoint
325
349
  // that will be called.
326
- url : function() {
327
- var base = getUrl(this.collection) || this.urlRoot || urlError();
350
+ url: function() {
351
+ var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();
328
352
  if (this.isNew()) return base;
329
353
  return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
330
354
  },
331
355
 
332
356
  // **parse** converts a response into the hash of attributes to be `set` on
333
357
  // the model. The default implementation is just to pass the response along.
334
- parse : function(resp, xhr) {
358
+ parse: function(resp, xhr) {
335
359
  return resp;
336
360
  },
337
361
 
338
362
  // Create a new model with identical attributes to this one.
339
- clone : function() {
340
- return new this.constructor(this);
363
+ clone: function() {
364
+ return new this.constructor(this.attributes);
341
365
  },
342
366
 
343
367
  // A model is new if it has never been saved to the server, and lacks an id.
344
- isNew : function() {
368
+ isNew: function() {
345
369
  return this.id == null;
346
370
  },
347
371
 
348
- // Call this method to manually fire a `change` event for this model.
372
+ // Call this method to manually fire a `"change"` event for this model and
373
+ // a `"change:attribute"` event for each changed attribute.
349
374
  // Calling this will cause all objects observing the model to update.
350
- change : function(options) {
375
+ change: function(options) {
376
+ for (var attr in this._changed) {
377
+ this.trigger('change:' + attr, this, this._changed[attr], options);
378
+ }
351
379
  this.trigger('change', this, options);
352
380
  this._previousAttributes = _.clone(this.attributes);
353
- this._changed = false;
381
+ this._changed = {};
354
382
  },
355
383
 
356
384
  // Determine if the model has changed since the last `"change"` event.
357
385
  // If you specify an attribute name, determine if that attribute has changed.
358
- hasChanged : function(attr) {
359
- if (attr) return this._previousAttributes[attr] != this.attributes[attr];
360
- return this._changed;
386
+ hasChanged: function(attr) {
387
+ if (attr) return _.has(this._changed, attr);
388
+ return !_.isEmpty(this._changed);
361
389
  },
362
390
 
363
- // Return an object containing all the attributes that have changed, or false
364
- // if there are no changed attributes. Useful for determining what parts of a
365
- // view need to be updated and/or what attributes need to be persisted to
366
- // the server.
367
- changedAttributes : function(now) {
368
- now || (now = this.attributes);
369
- var old = this._previousAttributes;
370
- var changed = false;
371
- for (var attr in now) {
372
- if (!_.isEqual(old[attr], now[attr])) {
373
- changed = changed || {};
374
- changed[attr] = now[attr];
375
- }
391
+ // Return an object containing all the attributes that have changed, or
392
+ // false if there are no changed attributes. Useful for determining what
393
+ // parts of a view need to be updated and/or what attributes need to be
394
+ // persisted to the server. Unset attributes will be set to undefined.
395
+ // You can also pass an attributes object to diff against the model,
396
+ // determining if there *would be* a change.
397
+ changedAttributes: function(diff) {
398
+ if (!diff) return this.hasChanged() ? _.clone(this._changed) : false;
399
+ var val, changed = false, old = this._previousAttributes;
400
+ for (var attr in diff) {
401
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
402
+ (changed || (changed = {}))[attr] = val;
376
403
  }
377
404
  return changed;
378
405
  },
379
406
 
380
407
  // Get the previous value of an attribute, recorded at the time the last
381
408
  // `"change"` event was fired.
382
- previous : function(attr) {
409
+ previous: function(attr) {
383
410
  if (!attr || !this._previousAttributes) return null;
384
411
  return this._previousAttributes[attr];
385
412
  },
386
413
 
387
414
  // Get all of the attributes of the model at the time of the previous
388
415
  // `"change"` event.
389
- previousAttributes : function() {
416
+ previousAttributes: function() {
390
417
  return _.clone(this._previousAttributes);
391
418
  },
392
419
 
393
420
  // Run validation against a set of incoming attributes, returning `true`
394
421
  // if all is well. If a specific `error` callback has been passed,
395
422
  // call that instead of firing the general `"error"` event.
396
- _performValidation : function(attrs, options) {
397
- var error = this.validate(attrs);
423
+ _performValidation: function(attrs, options) {
424
+ var newAttrs = _.extend({}, this.attributes, attrs);
425
+ var error = this.validate(newAttrs, options);
398
426
  if (error) {
399
427
  if (options.error) {
400
428
  options.error(this, error, options);
@@ -417,10 +445,9 @@
417
445
  Backbone.Collection = function(models, options) {
418
446
  options || (options = {});
419
447
  if (options.comparator) this.comparator = options.comparator;
420
- _.bindAll(this, '_onModelEvent', '_removeReference');
421
448
  this._reset();
422
- if (models) this.reset(models, {silent: true});
423
449
  this.initialize.apply(this, arguments);
450
+ if (models) this.reset(models, {silent: true, parse: options.parse});
424
451
  };
425
452
 
426
453
  // Define the Collection's inheritable methods.
@@ -428,52 +455,92 @@
428
455
 
429
456
  // The default model for a collection is just a **Backbone.Model**.
430
457
  // This should be overridden in most cases.
431
- model : Backbone.Model,
458
+ model: Backbone.Model,
432
459
 
433
460
  // Initialize is an empty function by default. Override it with your own
434
461
  // initialization logic.
435
- initialize : function(){},
462
+ initialize: function(){},
436
463
 
437
464
  // The JSON representation of a Collection is an array of the
438
465
  // models' attributes.
439
- toJSON : function() {
466
+ toJSON: function() {
440
467
  return this.map(function(model){ return model.toJSON(); });
441
468
  },
442
469
 
443
470
  // Add a model, or list of models to the set. Pass **silent** to avoid
444
- // firing the `added` event for every new model.
445
- add : function(models, options) {
446
- if (_.isArray(models)) {
447
- for (var i = 0, l = models.length; i < l; i++) {
448
- this._add(models[i], options);
471
+ // firing the `add` event for every new model.
472
+ add: function(models, options) {
473
+ var i, index, length, model, cid, id, cids = {}, ids = {};
474
+ options || (options = {});
475
+ models = _.isArray(models) ? models.slice() : [models];
476
+
477
+ // Begin by turning bare objects into model references, and preventing
478
+ // invalid models or duplicate models from being added.
479
+ for (i = 0, length = models.length; i < length; i++) {
480
+ if (!(model = models[i] = this._prepareModel(models[i], options))) {
481
+ throw new Error("Can't add an invalid model to a collection");
449
482
  }
450
- } else {
451
- this._add(models, options);
483
+ if (cids[cid = model.cid] || this._byCid[cid] ||
484
+ (((id = model.id) != null) && (ids[id] || this._byId[id]))) {
485
+ throw new Error("Can't add the same model to a collection twice");
486
+ }
487
+ cids[cid] = ids[id] = model;
488
+ }
489
+
490
+ // Listen to added models' events, and index models for lookup by
491
+ // `id` and by `cid`.
492
+ for (i = 0; i < length; i++) {
493
+ (model = models[i]).on('all', this._onModelEvent, this);
494
+ this._byCid[model.cid] = model;
495
+ if (model.id != null) this._byId[model.id] = model;
496
+ }
497
+
498
+ // Insert models into the collection, re-sorting if needed, and triggering
499
+ // `add` events unless silenced.
500
+ this.length += length;
501
+ index = options.at != null ? options.at : this.models.length;
502
+ splice.apply(this.models, [index, 0].concat(models));
503
+ if (this.comparator) this.sort({silent: true});
504
+ if (options.silent) return this;
505
+ for (i = 0, length = this.models.length; i < length; i++) {
506
+ if (!cids[(model = this.models[i]).cid]) continue;
507
+ options.index = i;
508
+ model.trigger('add', model, this, options);
452
509
  }
453
510
  return this;
454
511
  },
455
512
 
456
513
  // Remove a model, or a list of models from the set. Pass silent to avoid
457
- // firing the `removed` event for every model removed.
458
- remove : function(models, options) {
459
- if (_.isArray(models)) {
460
- for (var i = 0, l = models.length; i < l; i++) {
461
- this._remove(models[i], options);
514
+ // firing the `remove` event for every model removed.
515
+ remove: function(models, options) {
516
+ var i, l, index, model;
517
+ options || (options = {});
518
+ models = _.isArray(models) ? models.slice() : [models];
519
+ for (i = 0, l = models.length; i < l; i++) {
520
+ model = this.getByCid(models[i]) || this.get(models[i]);
521
+ if (!model) continue;
522
+ delete this._byId[model.id];
523
+ delete this._byCid[model.cid];
524
+ index = this.indexOf(model);
525
+ this.models.splice(index, 1);
526
+ this.length--;
527
+ if (!options.silent) {
528
+ options.index = index;
529
+ model.trigger('remove', model, this, options);
462
530
  }
463
- } else {
464
- this._remove(models, options);
531
+ this._removeReference(model);
465
532
  }
466
533
  return this;
467
534
  },
468
535
 
469
536
  // Get a model from the set by id.
470
- get : function(id) {
537
+ get: function(id) {
471
538
  if (id == null) return null;
472
539
  return this._byId[id.id != null ? id.id : id];
473
540
  },
474
541
 
475
542
  // Get a model from the set by client id.
476
- getByCid : function(cid) {
543
+ getByCid: function(cid) {
477
544
  return cid && this._byCid[cid.cid || cid];
478
545
  },
479
546
 
@@ -482,30 +549,38 @@
482
549
  return this.models[index];
483
550
  },
484
551
 
485
- // Force the collection to re-sort itself. You don't need to call this under normal
486
- // circumstances, as the set will maintain sort order as each item is added.
487
- sort : function(options) {
552
+ // Force the collection to re-sort itself. You don't need to call this under
553
+ // normal circumstances, as the set will maintain sort order as each item
554
+ // is added.
555
+ sort: function(options) {
488
556
  options || (options = {});
489
557
  if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
490
- this.models = this.sortBy(this.comparator);
558
+ var boundComparator = _.bind(this.comparator, this);
559
+ if (this.comparator.length == 1) {
560
+ this.models = this.sortBy(boundComparator);
561
+ } else {
562
+ this.models.sort(boundComparator);
563
+ }
491
564
  if (!options.silent) this.trigger('reset', this, options);
492
565
  return this;
493
566
  },
494
567
 
495
568
  // Pluck an attribute from each model in the collection.
496
- pluck : function(attr) {
569
+ pluck: function(attr) {
497
570
  return _.map(this.models, function(model){ return model.get(attr); });
498
571
  },
499
572
 
500
573
  // When you have more items than you want to add or remove individually,
501
574
  // you can reset the entire set with a new list of models, without firing
502
- // any `added` or `removed` events. Fires `reset` when finished.
503
- reset : function(models, options) {
575
+ // any `add` or `remove` events. Fires `reset` when finished.
576
+ reset: function(models, options) {
504
577
  models || (models = []);
505
578
  options || (options = {});
506
- this.each(this._removeReference);
579
+ for (var i = 0, l = this.models.length; i < l; i++) {
580
+ this._removeReference(this.models[i]);
581
+ }
507
582
  this._reset();
508
- this.add(models, {silent: true});
583
+ this.add(models, {silent: true, parse: options.parse});
509
584
  if (!options.silent) this.trigger('reset', this, options);
510
585
  return this;
511
586
  },
@@ -513,30 +588,36 @@
513
588
  // Fetch the default set of models for this collection, resetting the
514
589
  // collection when they arrive. If `add: true` is passed, appends the
515
590
  // models to the collection instead of resetting.
516
- fetch : function(options) {
517
- options || (options = {});
591
+ fetch: function(options) {
592
+ options = options ? _.clone(options) : {};
593
+ if (options.parse === undefined) options.parse = true;
518
594
  var collection = this;
519
595
  var success = options.success;
520
596
  options.success = function(resp, status, xhr) {
521
597
  collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
522
598
  if (success) success(collection, resp);
523
599
  };
524
- options.error = wrapError(options.error, collection, options);
600
+ options.error = Backbone.wrapError(options.error, collection, options);
525
601
  return (this.sync || Backbone.sync).call(this, 'read', this, options);
526
602
  },
527
603
 
528
- // Create a new instance of a model in this collection. After the model
529
- // has been created on the server, it will be added to the collection.
530
- // Returns the model, or 'false' if validation on a new model fails.
531
- create : function(model, options) {
604
+ // Create a new instance of a model in this collection. Add the model to the
605
+ // collection immediately, unless `wait: true` is passed, in which case we
606
+ // wait for the server to agree.
607
+ create: function(model, options) {
532
608
  var coll = this;
533
- options || (options = {});
609
+ options = options ? _.clone(options) : {};
534
610
  model = this._prepareModel(model, options);
535
611
  if (!model) return false;
612
+ if (!options.wait) coll.add(model, options);
536
613
  var success = options.success;
537
614
  options.success = function(nextModel, resp, xhr) {
538
- coll.add(nextModel, options);
539
- if (success) success(nextModel, resp, xhr);
615
+ if (options.wait) coll.add(nextModel, options);
616
+ if (success) {
617
+ success(nextModel, resp);
618
+ } else {
619
+ nextModel.trigger('sync', model, resp, options);
620
+ }
540
621
  };
541
622
  model.save(null, options);
542
623
  return model;
@@ -544,7 +625,7 @@
544
625
 
545
626
  // **parse** converts a response into a list of models to be added to the
546
627
  // collection. The default implementation is just to pass it through.
547
- parse : function(resp, xhr) {
628
+ parse: function(resp, xhr) {
548
629
  return resp;
549
630
  },
550
631
 
@@ -556,77 +637,42 @@
556
637
  },
557
638
 
558
639
  // Reset all internal state. Called when the collection is reset.
559
- _reset : function(options) {
640
+ _reset: function(options) {
560
641
  this.length = 0;
561
642
  this.models = [];
562
643
  this._byId = {};
563
644
  this._byCid = {};
564
645
  },
565
646
 
566
- // Prepare a model to be added to this collection
647
+ // Prepare a model or hash of attributes to be added to this collection.
567
648
  _prepareModel: function(model, options) {
568
649
  if (!(model instanceof Backbone.Model)) {
569
650
  var attrs = model;
570
- model = new this.model(attrs, {collection: this});
571
- if (model.validate && !model._performValidation(attrs, options)) model = false;
651
+ options.collection = this;
652
+ model = new this.model(attrs, options);
653
+ if (model.validate && !model._performValidation(model.attributes, options)) model = false;
572
654
  } else if (!model.collection) {
573
655
  model.collection = this;
574
656
  }
575
657
  return model;
576
658
  },
577
659
 
578
- // Internal implementation of adding a single model to the set, updating
579
- // hash indexes for `id` and `cid` lookups.
580
- // Returns the model, or 'false' if validation on a new model fails.
581
- _add : function(model, options) {
582
- options || (options = {});
583
- model = this._prepareModel(model, options);
584
- if (!model) return false;
585
- var already = this.getByCid(model);
586
- if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
587
- this._byId[model.id] = model;
588
- this._byCid[model.cid] = model;
589
- var index = options.at != null ? options.at :
590
- this.comparator ? this.sortedIndex(model, this.comparator) :
591
- this.length;
592
- this.models.splice(index, 0, model);
593
- model.bind('all', this._onModelEvent);
594
- this.length++;
595
- if (!options.silent) model.trigger('add', model, this, options);
596
- return model;
597
- },
598
-
599
- // Internal implementation of removing a single model from the set, updating
600
- // hash indexes for `id` and `cid` lookups.
601
- _remove : function(model, options) {
602
- options || (options = {});
603
- model = this.getByCid(model) || this.get(model);
604
- if (!model) return null;
605
- delete this._byId[model.id];
606
- delete this._byCid[model.cid];
607
- this.models.splice(this.indexOf(model), 1);
608
- this.length--;
609
- if (!options.silent) model.trigger('remove', model, this, options);
610
- this._removeReference(model);
611
- return model;
612
- },
613
-
614
660
  // Internal method to remove a model's ties to a collection.
615
- _removeReference : function(model) {
661
+ _removeReference: function(model) {
616
662
  if (this == model.collection) {
617
663
  delete model.collection;
618
664
  }
619
- model.unbind('all', this._onModelEvent);
665
+ model.off('all', this._onModelEvent, this);
620
666
  },
621
667
 
622
668
  // Internal method called every time a model in the set fires an event.
623
669
  // Sets need to update their indexes when models change ids. All other
624
670
  // events simply proxy through. "add" and "remove" events that originate
625
671
  // in other collections are ignored.
626
- _onModelEvent : function(ev, model, collection, options) {
672
+ _onModelEvent: function(ev, model, collection, options) {
627
673
  if ((ev == 'add' || ev == 'remove') && collection != this) return;
628
674
  if (ev == 'destroy') {
629
- this._remove(model, options);
675
+ this.remove(model, options);
630
676
  }
631
677
  if (model && ev === 'change:' + model.idAttribute) {
632
678
  delete this._byId[model.previous(model.idAttribute)];
@@ -638,10 +684,11 @@
638
684
  });
639
685
 
640
686
  // Underscore methods that we want to implement on the Collection.
641
- var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
642
- 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
643
- 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
644
- 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
687
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
688
+ 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
689
+ 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
690
+ 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
691
+ 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
645
692
 
646
693
  // Mix in each Underscore method as a proxy to `Collection#models`.
647
694
  _.each(methods, function(method) {
@@ -664,8 +711,8 @@
664
711
 
665
712
  // Cached regular expressions for matching named param parts and splatted
666
713
  // parts of route strings.
667
- var namedParam = /:([\w\d]+)/g;
668
- var splatParam = /\*([\w\d]+)/g;
714
+ var namedParam = /:\w+/g;
715
+ var splatParam = /\*\w+/g;
669
716
  var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
670
717
 
671
718
  // Set up all inheritable **Backbone.Router** properties and methods.
@@ -673,7 +720,7 @@
673
720
 
674
721
  // Initialize is an empty function by default. Override it with your own
675
722
  // initialization logic.
676
- initialize : function(){},
723
+ initialize: function(){},
677
724
 
678
725
  // Manually bind a single named route to a callback. For example:
679
726
  //
@@ -681,25 +728,28 @@
681
728
  // ...
682
729
  // });
683
730
  //
684
- route : function(route, name, callback) {
731
+ route: function(route, name, callback) {
685
732
  Backbone.history || (Backbone.history = new Backbone.History);
686
733
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
734
+ if (!callback) callback = this[name];
687
735
  Backbone.history.route(route, _.bind(function(fragment) {
688
736
  var args = this._extractParameters(route, fragment);
689
- callback.apply(this, args);
737
+ callback && callback.apply(this, args);
690
738
  this.trigger.apply(this, ['route:' + name].concat(args));
739
+ Backbone.history.trigger('route', this, name, args);
691
740
  }, this));
741
+ return this;
692
742
  },
693
743
 
694
744
  // Simple proxy to `Backbone.history` to save a fragment into the history.
695
- navigate : function(fragment, triggerRoute) {
696
- Backbone.history.navigate(fragment, triggerRoute);
745
+ navigate: function(fragment, options) {
746
+ Backbone.history.navigate(fragment, options);
697
747
  },
698
748
 
699
749
  // Bind all defined routes to `Backbone.history`. We have to reverse the
700
750
  // order of the routes here to support behavior where the most general
701
751
  // routes can be defined at the bottom of the route map.
702
- _bindRoutes : function() {
752
+ _bindRoutes: function() {
703
753
  if (!this.routes) return;
704
754
  var routes = [];
705
755
  for (var route in this.routes) {
@@ -712,16 +762,16 @@
712
762
 
713
763
  // Convert a route string into a regular expression, suitable for matching
714
764
  // against the current location hash.
715
- _routeToRegExp : function(route) {
716
- route = route.replace(escapeRegExp, "\\$&")
717
- .replace(namedParam, "([^\/]*)")
718
- .replace(splatParam, "(.*?)");
765
+ _routeToRegExp: function(route) {
766
+ route = route.replace(escapeRegExp, '\\$&')
767
+ .replace(namedParam, '([^\/]+)')
768
+ .replace(splatParam, '(.*?)');
719
769
  return new RegExp('^' + route + '$');
720
770
  },
721
771
 
722
772
  // Given a route, and a URL fragment that it matches, return the array of
723
773
  // extracted parameters.
724
- _extractParameters : function(route, fragment) {
774
+ _extractParameters: function(route, fragment) {
725
775
  return route.exec(fragment).slice(1);
726
776
  }
727
777
 
@@ -737,8 +787,8 @@
737
787
  _.bindAll(this, 'checkUrl');
738
788
  };
739
789
 
740
- // Cached regex for cleaning hashes.
741
- var hashStrip = /^#*/;
790
+ // Cached regex for cleaning leading hashes and slashes .
791
+ var routeStripper = /^[#\/]/;
742
792
 
743
793
  // Cached regex for detecting MSIE.
744
794
  var isExplorer = /msie [\w.]+/;
@@ -747,7 +797,7 @@
747
797
  var historyStarted = false;
748
798
 
749
799
  // Set up all inheritable **Backbone.History** properties and methods.
750
- _.extend(Backbone.History.prototype, {
800
+ _.extend(Backbone.History.prototype, Backbone.Events, {
751
801
 
752
802
  // The default interval to poll for hash changes, if necessary, is
753
803
  // twenty times a second.
@@ -755,28 +805,30 @@
755
805
 
756
806
  // Get the cross-browser normalized URL fragment, either from the URL,
757
807
  // the hash, or the override.
758
- getFragment : function(fragment, forcePushState) {
808
+ getFragment: function(fragment, forcePushState) {
759
809
  if (fragment == null) {
760
810
  if (this._hasPushState || forcePushState) {
761
811
  fragment = window.location.pathname;
762
812
  var search = window.location.search;
763
813
  if (search) fragment += search;
764
- if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
765
814
  } else {
766
815
  fragment = window.location.hash;
767
816
  }
768
817
  }
769
- return decodeURIComponent(fragment.replace(hashStrip, ''));
818
+ fragment = decodeURIComponent(fragment.replace(routeStripper, ''));
819
+ if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
820
+ return fragment;
770
821
  },
771
822
 
772
823
  // Start the hash change handling, returning `true` if the current URL matches
773
824
  // an existing route, and `false` otherwise.
774
- start : function(options) {
825
+ start: function(options) {
775
826
 
776
827
  // Figure out the initial configuration. Do we need an iframe?
777
828
  // Is pushState desired ... is it available?
778
829
  if (historyStarted) throw new Error("Backbone.history has already been started");
779
830
  this.options = _.extend({}, {root: '/'}, this.options, options);
831
+ this._wantsHashChange = this.options.hashChange !== false;
780
832
  this._wantsPushState = !!this.options.pushState;
781
833
  this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
782
834
  var fragment = this.getFragment();
@@ -791,10 +843,10 @@
791
843
  // 'onhashchange' is supported, determine how we check the URL state.
792
844
  if (this._hasPushState) {
793
845
  $(window).bind('popstate', this.checkUrl);
794
- } else if ('onhashchange' in window && !oldIE) {
846
+ } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
795
847
  $(window).bind('hashchange', this.checkUrl);
796
- } else {
797
- setInterval(this.checkUrl, this.interval);
848
+ } else if (this._wantsHashChange) {
849
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
798
850
  }
799
851
 
800
852
  // Determine if we need to change the base url, for a pushState link
@@ -803,13 +855,19 @@
803
855
  historyStarted = true;
804
856
  var loc = window.location;
805
857
  var atRoot = loc.pathname == this.options.root;
806
- if (this._wantsPushState && !this._hasPushState && !atRoot) {
858
+
859
+ // If we've started off with a route from a `pushState`-enabled browser,
860
+ // but we're currently in a browser that doesn't support it...
861
+ if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
807
862
  this.fragment = this.getFragment(null, true);
808
863
  window.location.replace(this.options.root + '#' + this.fragment);
809
864
  // Return immediately as browser will do redirect to new url
810
865
  return true;
866
+
867
+ // Or if we've started out with a hash-based route, but we're currently
868
+ // in a browser where it could be `pushState`-based instead...
811
869
  } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
812
- this.fragment = loc.hash.replace(hashStrip, '');
870
+ this.fragment = loc.hash.replace(routeStripper, '');
813
871
  window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
814
872
  }
815
873
 
@@ -818,15 +876,23 @@
818
876
  }
819
877
  },
820
878
 
821
- // Add a route to be tested when the fragment changes. Routes added later may
822
- // override previous routes.
823
- route : function(route, callback) {
824
- this.handlers.unshift({route : route, callback : callback});
879
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
880
+ // but possibly useful for unit testing Routers.
881
+ stop: function() {
882
+ $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
883
+ clearInterval(this._checkUrlInterval);
884
+ historyStarted = false;
885
+ },
886
+
887
+ // Add a route to be tested when the fragment changes. Routes added later
888
+ // may override previous routes.
889
+ route: function(route, callback) {
890
+ this.handlers.unshift({route: route, callback: callback});
825
891
  },
826
892
 
827
893
  // Checks the current URL to see if it has changed, and if it has,
828
894
  // calls `loadUrl`, normalizing across the hidden iframe.
829
- checkUrl : function(e) {
895
+ checkUrl: function(e) {
830
896
  var current = this.getFragment();
831
897
  if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
832
898
  if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
@@ -837,7 +903,7 @@
837
903
  // Attempt to load the current URL fragment. If a route succeeds with a
838
904
  // match, returns `true`. If no defined routes matches the fragment,
839
905
  // returns `false`.
840
- loadUrl : function(fragmentOverride) {
906
+ loadUrl: function(fragmentOverride) {
841
907
  var fragment = this.fragment = this.getFragment(fragmentOverride);
842
908
  var matched = _.any(this.handlers, function(handler) {
843
909
  if (handler.route.test(fragment)) {
@@ -848,27 +914,54 @@
848
914
  return matched;
849
915
  },
850
916
 
851
- // Save a fragment into the hash history. You are responsible for properly
852
- // URL-encoding the fragment in advance. This does not trigger
853
- // a `hashchange` event.
854
- navigate : function(fragment, triggerRoute) {
855
- var frag = (fragment || '').replace(hashStrip, '');
917
+ // Save a fragment into the hash history, or replace the URL state if the
918
+ // 'replace' option is passed. You are responsible for properly URL-encoding
919
+ // the fragment in advance.
920
+ //
921
+ // The options object can contain `trigger: true` if you wish to have the
922
+ // route callback be fired (not usually desirable), or `replace: true`, if
923
+ // you which to modify the current URL without adding an entry to the history.
924
+ navigate: function(fragment, options) {
925
+ if (!historyStarted) return false;
926
+ if (!options || options === true) options = {trigger: options};
927
+ var frag = (fragment || '').replace(routeStripper, '');
856
928
  if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
929
+
930
+ // If pushState is available, we use it to set the fragment as a real URL.
857
931
  if (this._hasPushState) {
858
- var loc = window.location;
859
932
  if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
860
933
  this.fragment = frag;
861
- window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
862
- } else {
863
- window.location.hash = this.fragment = frag;
934
+ window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
935
+
936
+ // If hash changes haven't been explicitly disabled, update the hash
937
+ // fragment to store history.
938
+ } else if (this._wantsHashChange) {
939
+ this.fragment = frag;
940
+ this._updateHash(window.location, frag, options.replace);
864
941
  if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
865
- this.iframe.document.open().close();
866
- this.iframe.location.hash = frag;
942
+ // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
943
+ // When replace is true, we don't want this.
944
+ if(!options.replace) this.iframe.document.open().close();
945
+ this._updateHash(this.iframe.location, frag, options.replace);
867
946
  }
947
+
948
+ // If you've told us that you explicitly don't want fallback hashchange-
949
+ // based history, then `navigate` becomes a page refresh.
950
+ } else {
951
+ window.location.assign(this.options.root + fragment);
868
952
  }
869
- if (triggerRoute) this.loadUrl(fragment);
870
- }
953
+ if (options.trigger) this.loadUrl(fragment);
954
+ },
871
955
 
956
+ // Update the hash location, either replacing the current entry, or adding
957
+ // a new one to the browser history.
958
+ _updateHash: function(location, fragment, replace) {
959
+ if (replace) {
960
+ location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
961
+ } else {
962
+ location.hash = fragment;
963
+ }
964
+ }
872
965
  });
873
966
 
874
967
  // Backbone.View
@@ -880,15 +973,8 @@
880
973
  this.cid = _.uniqueId('view');
881
974
  this._configure(options || {});
882
975
  this._ensureElement();
883
- this.delegateEvents();
884
976
  this.initialize.apply(this, arguments);
885
- };
886
-
887
- // Element lookup, scoped to DOM elements within the current view.
888
- // This should be prefered to global lookups, if you're dealing with
889
- // a specific view.
890
- var selectorDelegate = function(selector) {
891
- return $(selector, this.el);
977
+ this.delegateEvents();
892
978
  };
893
979
 
894
980
  // Cached regex to split keys for `delegate`.
@@ -901,26 +987,29 @@
901
987
  _.extend(Backbone.View.prototype, Backbone.Events, {
902
988
 
903
989
  // The default `tagName` of a View's element is `"div"`.
904
- tagName : 'div',
990
+ tagName: 'div',
905
991
 
906
- // Attach the `selectorDelegate` function as the `$` property.
907
- $ : selectorDelegate,
992
+ // jQuery delegate for element lookup, scoped to DOM elements within the
993
+ // current view. This should be prefered to global lookups where possible.
994
+ $: function(selector) {
995
+ return this.$el.find(selector);
996
+ },
908
997
 
909
998
  // Initialize is an empty function by default. Override it with your own
910
999
  // initialization logic.
911
- initialize : function(){},
1000
+ initialize: function(){},
912
1001
 
913
1002
  // **render** is the core function that your view should override, in order
914
1003
  // to populate its element (`this.el`), with the appropriate HTML. The
915
1004
  // convention is for **render** to always return `this`.
916
- render : function() {
1005
+ render: function() {
917
1006
  return this;
918
1007
  },
919
1008
 
920
1009
  // Remove this view from the DOM. Note that the view isn't present in the
921
1010
  // DOM by default, so calling this method may be a no-op.
922
- remove : function() {
923
- $(this.el).remove();
1011
+ remove: function() {
1012
+ this.$el.remove();
924
1013
  return this;
925
1014
  },
926
1015
 
@@ -929,20 +1018,29 @@
929
1018
  //
930
1019
  // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
931
1020
  //
932
- make : function(tagName, attributes, content) {
1021
+ make: function(tagName, attributes, content) {
933
1022
  var el = document.createElement(tagName);
934
1023
  if (attributes) $(el).attr(attributes);
935
1024
  if (content) $(el).html(content);
936
1025
  return el;
937
1026
  },
938
1027
 
939
- // Set callbacks, where `this.callbacks` is a hash of
1028
+ // Change the view's element (`this.el` property), including event
1029
+ // re-delegation.
1030
+ setElement: function(element, delegate) {
1031
+ this.$el = $(element);
1032
+ this.el = this.$el[0];
1033
+ if (delegate !== false) this.delegateEvents();
1034
+ },
1035
+
1036
+ // Set callbacks, where `this.events` is a hash of
940
1037
  //
941
1038
  // *{"event selector": "callback"}*
942
1039
  //
943
1040
  // {
944
1041
  // 'mousedown .title': 'edit',
945
1042
  // 'click .button': 'save'
1043
+ // 'click .open': function(e) { ... }
946
1044
  // }
947
1045
  //
948
1046
  // pairs. Callbacks will be bound to the view, with `this` set properly.
@@ -950,29 +1048,36 @@
950
1048
  // Omitting the selector binds the event to `this.el`.
951
1049
  // This only works for delegate-able events: not `focus`, `blur`, and
952
1050
  // not `change`, `submit`, and `reset` in Internet Explorer.
953
- delegateEvents : function(events) {
954
- if (!(events || (events = this.events))) return;
955
- if (_.isFunction(events)) events = events.call(this);
956
- $(this.el).unbind('.delegateEvents' + this.cid);
1051
+ delegateEvents: function(events) {
1052
+ if (!(events || (events = getValue(this, 'events')))) return;
1053
+ this.undelegateEvents();
957
1054
  for (var key in events) {
958
- var method = this[events[key]];
1055
+ var method = events[key];
1056
+ if (!_.isFunction(method)) method = this[events[key]];
959
1057
  if (!method) throw new Error('Event "' + events[key] + '" does not exist');
960
1058
  var match = key.match(eventSplitter);
961
1059
  var eventName = match[1], selector = match[2];
962
1060
  method = _.bind(method, this);
963
1061
  eventName += '.delegateEvents' + this.cid;
964
1062
  if (selector === '') {
965
- $(this.el).bind(eventName, method);
1063
+ this.$el.bind(eventName, method);
966
1064
  } else {
967
- $(this.el).delegate(selector, eventName, method);
1065
+ this.$el.delegate(selector, eventName, method);
968
1066
  }
969
1067
  }
970
1068
  },
971
1069
 
1070
+ // Clears all callbacks previously bound to the view with `delegateEvents`.
1071
+ // You usually don't need to use this, but may wish to if you have multiple
1072
+ // Backbone views attached to the same DOM element.
1073
+ undelegateEvents: function() {
1074
+ this.$el.unbind('.delegateEvents' + this.cid);
1075
+ },
1076
+
972
1077
  // Performs the initial configuration of a View with a set of options.
973
1078
  // Keys with special meaning *(model, collection, id, className)*, are
974
1079
  // attached directly to the view.
975
- _configure : function(options) {
1080
+ _configure: function(options) {
976
1081
  if (this.options) options = _.extend({}, this.options, options);
977
1082
  for (var i = 0, l = viewOptions.length; i < l; i++) {
978
1083
  var attr = viewOptions[i];
@@ -984,15 +1089,15 @@
984
1089
  // Ensure that the View has a DOM element to render into.
985
1090
  // If `this.el` is a string, pass it through `$()`, take the first
986
1091
  // matching element, and re-assign it to `el`. Otherwise, create
987
- // an element from the `id`, `className` and `tagName` proeprties.
988
- _ensureElement : function() {
1092
+ // an element from the `id`, `className` and `tagName` properties.
1093
+ _ensureElement: function() {
989
1094
  if (!this.el) {
990
- var attrs = this.attributes || {};
1095
+ var attrs = getValue(this, 'attributes') || {};
991
1096
  if (this.id) attrs.id = this.id;
992
1097
  if (this.className) attrs['class'] = this.className;
993
- this.el = this.make(this.tagName, attrs);
994
- } else if (_.isString(this.el)) {
995
- this.el = $(this.el).get(0);
1098
+ this.setElement(this.make(this.tagName, attrs), false);
1099
+ } else {
1100
+ this.setElement(this.el, false);
996
1101
  }
997
1102
  }
998
1103
 
@@ -1009,20 +1114,20 @@
1009
1114
  Backbone.Model.extend = Backbone.Collection.extend =
1010
1115
  Backbone.Router.extend = Backbone.View.extend = extend;
1011
1116
 
1117
+ // Backbone.sync
1118
+ // -------------
1119
+
1012
1120
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1013
1121
  var methodMap = {
1014
1122
  'create': 'POST',
1015
1123
  'update': 'PUT',
1016
1124
  'delete': 'DELETE',
1017
- 'read' : 'GET'
1125
+ 'read': 'GET'
1018
1126
  };
1019
1127
 
1020
- // Backbone.sync
1021
- // -------------
1022
-
1023
1128
  // Override this function to change the manner in which Backbone persists
1024
1129
  // models to the server. You will be passed the type of request, and the
1025
- // model in question. By default, uses makes a RESTful Ajax request
1130
+ // model in question. By default, makes a RESTful Ajax request
1026
1131
  // to the model's `url()`. Some possible customizations could be:
1027
1132
  //
1028
1133
  // * Use `setTimeout` to batch rapid-fire updates into a single request.
@@ -1031,26 +1136,23 @@
1031
1136
  //
1032
1137
  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1033
1138
  // as `POST`, with a `_method` parameter containing the true HTTP method,
1034
- // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
1035
- // `application/json` with the model in a param named `model`.
1139
+ // as well as all requests with the body as `application/x-www-form-urlencoded`
1140
+ // instead of `application/json` with the model in a param named `model`.
1036
1141
  // Useful when interfacing with server-side languages like **PHP** that make
1037
1142
  // it difficult to read the body of `PUT` requests.
1038
1143
  Backbone.sync = function(method, model, options) {
1039
1144
  var type = methodMap[method];
1040
1145
 
1041
1146
  // Default JSON-request options.
1042
- var params = _.extend({
1043
- type: type,
1044
- dataType: 'json'
1045
- }, options);
1147
+ var params = {type: type, dataType: 'json'};
1046
1148
 
1047
1149
  // Ensure that we have a URL.
1048
- if (!params.url) {
1049
- params.url = getUrl(model) || urlError();
1150
+ if (!options.url) {
1151
+ params.url = getValue(model, 'url') || urlError();
1050
1152
  }
1051
1153
 
1052
1154
  // Ensure that we have the appropriate request data.
1053
- if (!params.data && model && (method == 'create' || method == 'update')) {
1155
+ if (!options.data && model && (method == 'create' || method == 'update')) {
1054
1156
  params.contentType = 'application/json';
1055
1157
  params.data = JSON.stringify(model.toJSON());
1056
1158
  }
@@ -1058,7 +1160,7 @@
1058
1160
  // For older servers, emulate JSON by encoding the request into an HTML-form.
1059
1161
  if (Backbone.emulateJSON) {
1060
1162
  params.contentType = 'application/x-www-form-urlencoded';
1061
- params.data = params.data ? {model : params.data} : {};
1163
+ params.data = params.data ? {model: params.data} : {};
1062
1164
  }
1063
1165
 
1064
1166
  // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
@@ -1078,8 +1180,20 @@
1078
1180
  params.processData = false;
1079
1181
  }
1080
1182
 
1081
- // Make the request.
1082
- return $.ajax(params);
1183
+ // Make the request, allowing the user to override any Ajax options.
1184
+ return $.ajax(_.extend(params, options));
1185
+ };
1186
+
1187
+ // Wrap an optional error callback with a fallback error event.
1188
+ Backbone.wrapError = function(onError, originalModel, options) {
1189
+ return function(model, resp) {
1190
+ var resp = model === originalModel ? resp : model;
1191
+ if (onError) {
1192
+ onError(model, resp, options);
1193
+ } else {
1194
+ originalModel.trigger('error', model, resp, options);
1195
+ }
1196
+ };
1083
1197
  };
1084
1198
 
1085
1199
  // Helpers
@@ -1096,11 +1210,11 @@
1096
1210
 
1097
1211
  // The constructor function for the new subclass is either defined by you
1098
1212
  // (the "constructor" property in your `extend` definition), or defaulted
1099
- // by us to simply call `super()`.
1213
+ // by us to simply call the parent's constructor.
1100
1214
  if (protoProps && protoProps.hasOwnProperty('constructor')) {
1101
1215
  child = protoProps.constructor;
1102
1216
  } else {
1103
- child = function(){ return parent.apply(this, arguments); };
1217
+ child = function(){ parent.apply(this, arguments); };
1104
1218
  }
1105
1219
 
1106
1220
  // Inherit class (static) properties from parent.
@@ -1127,11 +1241,11 @@
1127
1241
  return child;
1128
1242
  };
1129
1243
 
1130
- // Helper function to get a URL from a Model or Collection as a property
1244
+ // Helper function to get a value from a Backbone object as a property
1131
1245
  // or as a function.
1132
- var getUrl = function(object) {
1133
- if (!(object && object.url)) return null;
1134
- return _.isFunction(object.url) ? object.url() : object.url;
1246
+ var getValue = function(object, prop) {
1247
+ if (!(object && object[prop])) return null;
1248
+ return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1135
1249
  };
1136
1250
 
1137
1251
  // Throw an error when a URL is needed, and none is supplied.
@@ -1139,20 +1253,4 @@
1139
1253
  throw new Error('A "url" property or function must be specified');
1140
1254
  };
1141
1255
 
1142
- // Wrap an optional error callback with a fallback error event.
1143
- var wrapError = function(onError, model, options) {
1144
- return function(resp) {
1145
- if (onError) {
1146
- onError(model, resp, options);
1147
- } else {
1148
- model.trigger('error', model, resp, options);
1149
- }
1150
- };
1151
- };
1152
-
1153
- // Helper function to escape a string for HTML rendering.
1154
- var escapeHTML = function(string) {
1155
- return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
1156
- };
1157
-
1158
- }).call(this);
1256
+ }).call(this);