backbonejs-rails 0.0.3 → 0.0.4

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,4 +1,4 @@
1
- // Backbone.js 0.3.3
1
+ // Backbone.js 0.5.3
2
2
  // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3
3
  // Backbone may be freely distributed under the MIT license.
4
4
  // For all details and documentation:
@@ -9,26 +9,39 @@
9
9
  // Initial Setup
10
10
  // -------------
11
11
 
12
+ // Save a reference to the global object.
13
+ var root = this;
14
+
15
+ // Save the previous value of the `Backbone` variable.
16
+ var previousBackbone = root.Backbone;
17
+
12
18
  // The top-level namespace. All public Backbone classes and modules will
13
19
  // be attached to this. Exported for both CommonJS and the browser.
14
20
  var Backbone;
15
21
  if (typeof exports !== 'undefined') {
16
22
  Backbone = exports;
17
23
  } else {
18
- Backbone = this.Backbone = {};
24
+ Backbone = root.Backbone = {};
19
25
  }
20
26
 
21
27
  // Current version of the library. Keep in sync with `package.json`.
22
- Backbone.VERSION = '0.3.3';
28
+ Backbone.VERSION = '0.5.3';
23
29
 
24
30
  // Require Underscore, if we're on the server, and it's not already present.
25
- var _ = this._;
26
- if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;
31
+ var _ = root._;
32
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
27
33
 
28
- // For Backbone's purposes, either jQuery or Zepto owns the `$` variable.
29
- var $ = this.jQuery || this.Zepto;
34
+ // For Backbone's purposes, jQuery or Zepto owns the `$` variable.
35
+ var $ = root.jQuery || root.Zepto;
36
+
37
+ // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
38
+ // to its previous owner. Returns a reference to this Backbone object.
39
+ Backbone.noConflict = function() {
40
+ root.Backbone = previousBackbone;
41
+ return this;
42
+ };
30
43
 
31
- // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will
44
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
32
45
  // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
33
46
  // `X-Http-Method-Override` header.
34
47
  Backbone.emulateHTTP = false;
@@ -55,10 +68,10 @@
55
68
 
56
69
  // Bind an event, specified by a string name, `ev`, to a `callback` function.
57
70
  // Passing `"all"` will bind the callback to all events fired.
58
- bind : function(ev, callback) {
71
+ bind : function(ev, callback, context) {
59
72
  var calls = this._callbacks || (this._callbacks = {});
60
- var list = this._callbacks[ev] || (this._callbacks[ev] = []);
61
- list.push(callback);
73
+ var list = calls[ev] || (calls[ev] = []);
74
+ list.push([callback, context]);
62
75
  return this;
63
76
  },
64
77
 
@@ -76,8 +89,8 @@
76
89
  var list = calls[ev];
77
90
  if (!list) return this;
78
91
  for (var i = 0, l = list.length; i < l; i++) {
79
- if (callback === list[i]) {
80
- list.splice(i, 1);
92
+ if (list[i] && callback === list[i][0]) {
93
+ list[i] = null;
81
94
  break;
82
95
  }
83
96
  }
@@ -89,17 +102,21 @@
89
102
  // Trigger an event, firing all bound callbacks. Callbacks are passed the
90
103
  // same arguments as `trigger` is, apart from the event name.
91
104
  // Listening for `"all"` passes the true event name as the first argument.
92
- trigger : function(ev) {
93
- var list, calls, i, l;
105
+ trigger : function(eventName) {
106
+ var list, calls, ev, callback, args;
107
+ var both = 2;
94
108
  if (!(calls = this._callbacks)) return this;
95
- if (list = calls[ev]) {
96
- for (i = 0, l = list.length; i < l; i++) {
97
- list[i].apply(this, Array.prototype.slice.call(arguments, 1));
98
- }
99
- }
100
- if (list = calls['all']) {
101
- for (i = 0, l = list.length; i < l; i++) {
102
- list[i].apply(this, arguments);
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
+ }
103
120
  }
104
121
  }
105
122
  return this;
@@ -113,12 +130,17 @@
113
130
  // Create a new model, with defined attributes. A client id (`cid`)
114
131
  // is automatically generated and assigned for you.
115
132
  Backbone.Model = function(attributes, options) {
133
+ var defaults;
116
134
  attributes || (attributes = {});
117
- if (this.defaults) attributes = _.extend({}, this.defaults, attributes);
135
+ if (defaults = this.defaults) {
136
+ if (_.isFunction(defaults)) defaults = defaults.call(this);
137
+ attributes = _.extend({}, defaults, attributes);
138
+ }
118
139
  this.attributes = {};
119
140
  this._escapedAttributes = {};
120
141
  this.cid = _.uniqueId('c');
121
142
  this.set(attributes, {silent : true});
143
+ this._changed = false;
122
144
  this._previousAttributes = _.clone(this.attributes);
123
145
  if (options && options.collection) this.collection = options.collection;
124
146
  this.initialize(attributes, options);
@@ -134,6 +156,10 @@
134
156
  // Has the item been changed since the last `"change"` event?
135
157
  _changed : false,
136
158
 
159
+ // The default name for the JSON `id` attribute is `"id"`. MongoDB and
160
+ // CouchDB users may want to set this to `"_id"`.
161
+ idAttribute : 'id',
162
+
137
163
  // Initialize is an empty function by default. Override it with your own
138
164
  // initialization logic.
139
165
  initialize : function(){},
@@ -153,7 +179,13 @@
153
179
  var html;
154
180
  if (html = this._escapedAttributes[attr]) return html;
155
181
  var val = this.attributes[attr];
156
- return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val);
182
+ return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
183
+ },
184
+
185
+ // Returns `true` if the attribute contains a value that is not null
186
+ // or undefined.
187
+ has : function(attr) {
188
+ return this.attributes[attr] != null;
157
189
  },
158
190
 
159
191
  // Set a hash of model attributes on the object, firing `"change"` unless you
@@ -170,7 +202,11 @@
170
202
  if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
171
203
 
172
204
  // Check for changes of `id`.
173
- if ('id' in attrs) this.id = attrs.id;
205
+ if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
206
+
207
+ // We're about to start triggering change events.
208
+ var alreadyChanging = this._changing;
209
+ this._changing = true;
174
210
 
175
211
  // Update attributes.
176
212
  for (var attr in attrs) {
@@ -178,21 +214,21 @@
178
214
  if (!_.isEqual(now[attr], val)) {
179
215
  now[attr] = val;
180
216
  delete escaped[attr];
181
- if (!options.silent) {
182
- this._changed = true;
183
- this.trigger('change:' + attr, this, val, options);
184
- }
217
+ this._changed = true;
218
+ if (!options.silent) this.trigger('change:' + attr, this, val, options);
185
219
  }
186
220
  }
187
221
 
188
222
  // Fire the `"change"` event, if the model has been changed.
189
- if (!options.silent && this._changed) this.change(options);
223
+ if (!alreadyChanging && !options.silent && this._changed) this.change(options);
224
+ this._changing = false;
190
225
  return this;
191
226
  },
192
227
 
193
228
  // Remove an attribute from the model, firing `"change"` unless you choose
194
- // to silence it.
229
+ // to silence it. `unset` is a noop if the attribute doesn't exist.
195
230
  unset : function(attr, options) {
231
+ if (!(attr in this.attributes)) return this;
196
232
  options || (options = {});
197
233
  var value = this.attributes[attr];
198
234
 
@@ -204,8 +240,9 @@
204
240
  // Remove the attribute.
205
241
  delete this.attributes[attr];
206
242
  delete this._escapedAttributes[attr];
243
+ if (attr == this.idAttribute) delete this.id;
244
+ this._changed = true;
207
245
  if (!options.silent) {
208
- this._changed = true;
209
246
  this.trigger('change:' + attr, this, void 0, options);
210
247
  this.change(options);
211
248
  }
@@ -216,6 +253,7 @@
216
253
  // to silence it.
217
254
  clear : function(options) {
218
255
  options || (options = {});
256
+ var attr;
219
257
  var old = this.attributes;
220
258
 
221
259
  // Run validation.
@@ -225,8 +263,8 @@
225
263
 
226
264
  this.attributes = {};
227
265
  this._escapedAttributes = {};
266
+ this._changed = true;
228
267
  if (!options.silent) {
229
- this._changed = true;
230
268
  for (attr in old) {
231
269
  this.trigger('change:' + attr, this, void 0, options);
232
270
  }
@@ -241,13 +279,13 @@
241
279
  fetch : function(options) {
242
280
  options || (options = {});
243
281
  var model = this;
244
- var success = function(resp) {
245
- if (!model.set(model.parse(resp), options)) return false;
246
- if (options.success) options.success(model, resp);
282
+ var success = options.success;
283
+ options.success = function(resp, status, xhr) {
284
+ if (!model.set(model.parse(resp, xhr), options)) return false;
285
+ if (success) success(model, resp);
247
286
  };
248
- var error = wrapError(options.error, model, options);
249
- (this.sync || Backbone.sync)('read', this, success, error);
250
- return this;
287
+ options.error = wrapError(options.error, model, options);
288
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);
251
289
  },
252
290
 
253
291
  // Set a hash of model attributes, and sync the model to the server.
@@ -257,42 +295,43 @@
257
295
  options || (options = {});
258
296
  if (attrs && !this.set(attrs, options)) return false;
259
297
  var model = this;
260
- var success = function(resp) {
261
- if (!model.set(model.parse(resp), options)) return false;
262
- if (options.success) options.success(model, resp);
298
+ var success = options.success;
299
+ options.success = function(resp, status, xhr) {
300
+ if (!model.set(model.parse(resp, xhr), options)) return false;
301
+ if (success) success(model, resp, xhr);
263
302
  };
264
- var error = wrapError(options.error, model, options);
303
+ options.error = wrapError(options.error, model, options);
265
304
  var method = this.isNew() ? 'create' : 'update';
266
- (this.sync || Backbone.sync)(method, this, success, error);
267
- return this;
305
+ return (this.sync || Backbone.sync).call(this, method, this, options);
268
306
  },
269
307
 
270
- // Destroy this model on the server. Upon success, the model is removed
308
+ // Destroy this model on the server if it was already persisted. Upon success, the model is removed
271
309
  // from its collection, if it has one.
272
310
  destroy : function(options) {
273
311
  options || (options = {});
312
+ if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
274
313
  var model = this;
275
- var success = function(resp) {
276
- if (model.collection) model.collection.remove(model);
277
- if (options.success) options.success(model, resp);
314
+ var success = options.success;
315
+ options.success = function(resp) {
316
+ model.trigger('destroy', model, model.collection, options);
317
+ if (success) success(model, resp);
278
318
  };
279
- var error = wrapError(options.error, model, options);
280
- (this.sync || Backbone.sync)('delete', this, success, error);
281
- return this;
319
+ options.error = wrapError(options.error, model, options);
320
+ return (this.sync || Backbone.sync).call(this, 'delete', this, options);
282
321
  },
283
322
 
284
323
  // Default URL for the model's representation on the server -- if you're
285
324
  // using Backbone's restful methods, override this to change the endpoint
286
325
  // that will be called.
287
326
  url : function() {
288
- var base = getUrl(this.collection);
327
+ var base = getUrl(this.collection) || this.urlRoot || urlError();
289
328
  if (this.isNew()) return base;
290
- return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
329
+ return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
291
330
  },
292
331
 
293
332
  // **parse** converts a response into the hash of attributes to be `set` on
294
333
  // the model. The default implementation is just to pass the response along.
295
- parse : function(resp) {
334
+ parse : function(resp, xhr) {
296
335
  return resp;
297
336
  },
298
337
 
@@ -301,10 +340,9 @@
301
340
  return new this.constructor(this);
302
341
  },
303
342
 
304
- // A model is new if it has never been saved to the server, and has a negative
305
- // ID.
343
+ // A model is new if it has never been saved to the server, and lacks an id.
306
344
  isNew : function() {
307
- return !this.id;
345
+ return this.id == null;
308
346
  },
309
347
 
310
348
  // Call this method to manually fire a `change` event for this model.
@@ -359,7 +397,7 @@
359
397
  var error = this.validate(attrs);
360
398
  if (error) {
361
399
  if (options.error) {
362
- options.error(this, error);
400
+ options.error(this, error, options);
363
401
  } else {
364
402
  this.trigger('error', this, error, options);
365
403
  }
@@ -378,14 +416,11 @@
378
416
  // its models in sort order, as they're added and removed.
379
417
  Backbone.Collection = function(models, options) {
380
418
  options || (options = {});
381
- if (options.comparator) {
382
- this.comparator = options.comparator;
383
- delete options.comparator;
384
- }
385
- this._boundOnModelEvent = _.bind(this._onModelEvent, this);
419
+ if (options.comparator) this.comparator = options.comparator;
420
+ _.bindAll(this, '_onModelEvent', '_removeReference');
386
421
  this._reset();
387
- if (models) this.refresh(models, {silent: true});
388
- this.initialize(models, options);
422
+ if (models) this.reset(models, {silent: true});
423
+ this.initialize.apply(this, arguments);
389
424
  };
390
425
 
391
426
  // Define the Collection's inheritable methods.
@@ -453,7 +488,7 @@
453
488
  options || (options = {});
454
489
  if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
455
490
  this.models = this.sortBy(this.comparator);
456
- if (!options.silent) this.trigger('refresh', this, options);
491
+ if (!options.silent) this.trigger('reset', this, options);
457
492
  return this;
458
493
  },
459
494
 
@@ -463,51 +498,53 @@
463
498
  },
464
499
 
465
500
  // When you have more items than you want to add or remove individually,
466
- // you can refresh the entire set with a new list of models, without firing
467
- // any `added` or `removed` events. Fires `refresh` when finished.
468
- refresh : function(models, options) {
501
+ // 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) {
469
504
  models || (models = []);
470
505
  options || (options = {});
506
+ this.each(this._removeReference);
471
507
  this._reset();
472
508
  this.add(models, {silent: true});
473
- if (!options.silent) this.trigger('refresh', this, options);
509
+ if (!options.silent) this.trigger('reset', this, options);
474
510
  return this;
475
511
  },
476
512
 
477
- // Fetch the default set of models for this collection, refreshing the
478
- // collection when they arrive.
513
+ // Fetch the default set of models for this collection, resetting the
514
+ // collection when they arrive. If `add: true` is passed, appends the
515
+ // models to the collection instead of resetting.
479
516
  fetch : function(options) {
480
517
  options || (options = {});
481
518
  var collection = this;
482
- var success = function(resp) {
483
- collection.refresh(collection.parse(resp));
484
- if (options.success) options.success(collection, resp);
519
+ var success = options.success;
520
+ options.success = function(resp, status, xhr) {
521
+ collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
522
+ if (success) success(collection, resp);
485
523
  };
486
- var error = wrapError(options.error, collection, options);
487
- (this.sync || Backbone.sync)('read', this, success, error);
488
- return this;
524
+ options.error = wrapError(options.error, collection, options);
525
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);
489
526
  },
490
527
 
491
528
  // Create a new instance of a model in this collection. After the model
492
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.
493
531
  create : function(model, options) {
494
532
  var coll = this;
495
533
  options || (options = {});
496
- if (!(model instanceof Backbone.Model)) {
497
- model = new this.model(model, {collection: coll});
498
- } else {
499
- model.collection = coll;
500
- }
501
- var success = function(nextModel, resp) {
502
- coll.add(nextModel);
503
- if (options.success) options.success(nextModel, resp);
534
+ model = this._prepareModel(model, options);
535
+ if (!model) return false;
536
+ var success = options.success;
537
+ options.success = function(nextModel, resp, xhr) {
538
+ coll.add(nextModel, options);
539
+ if (success) success(nextModel, resp, xhr);
504
540
  };
505
- return model.save(null, {success : success, error : options.error});
541
+ model.save(null, options);
542
+ return model;
506
543
  },
507
544
 
508
545
  // **parse** converts a response into a list of models to be added to the
509
546
  // collection. The default implementation is just to pass it through.
510
- parse : function(resp) {
547
+ parse : function(resp, xhr) {
511
548
  return resp;
512
549
  },
513
550
 
@@ -518,7 +555,7 @@
518
555
  return _(this.models).chain();
519
556
  },
520
557
 
521
- // Reset all internal state. Called when the collection is refreshed.
558
+ // Reset all internal state. Called when the collection is reset.
522
559
  _reset : function(options) {
523
560
  this.length = 0;
524
561
  this.models = [];
@@ -526,21 +563,34 @@
526
563
  this._byCid = {};
527
564
  },
528
565
 
566
+ // Prepare a model to be added to this collection
567
+ _prepareModel: function(model, options) {
568
+ if (!(model instanceof Backbone.Model)) {
569
+ var attrs = model;
570
+ model = new this.model(attrs, {collection: this});
571
+ if (model.validate && !model._performValidation(attrs, options)) model = false;
572
+ } else if (!model.collection) {
573
+ model.collection = this;
574
+ }
575
+ return model;
576
+ },
577
+
529
578
  // Internal implementation of adding a single model to the set, updating
530
579
  // hash indexes for `id` and `cid` lookups.
580
+ // Returns the model, or 'false' if validation on a new model fails.
531
581
  _add : function(model, options) {
532
582
  options || (options = {});
533
- if (!(model instanceof Backbone.Model)) {
534
- model = new this.model(model, {collection: this});
535
- }
583
+ model = this._prepareModel(model, options);
584
+ if (!model) return false;
536
585
  var already = this.getByCid(model);
537
586
  if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
538
587
  this._byId[model.id] = model;
539
588
  this._byCid[model.cid] = model;
540
- model.collection = this;
541
- var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
589
+ var index = options.at != null ? options.at :
590
+ this.comparator ? this.sortedIndex(model, this.comparator) :
591
+ this.length;
542
592
  this.models.splice(index, 0, model);
543
- model.bind('all', this._boundOnModelEvent);
593
+ model.bind('all', this._onModelEvent);
544
594
  this.length++;
545
595
  if (!options.silent) model.trigger('add', model, this, options);
546
596
  return model;
@@ -554,20 +604,32 @@
554
604
  if (!model) return null;
555
605
  delete this._byId[model.id];
556
606
  delete this._byCid[model.cid];
557
- delete model.collection;
558
607
  this.models.splice(this.indexOf(model), 1);
559
608
  this.length--;
560
609
  if (!options.silent) model.trigger('remove', model, this, options);
561
- model.unbind('all', this._boundOnModelEvent);
610
+ this._removeReference(model);
562
611
  return model;
563
612
  },
564
613
 
614
+ // Internal method to remove a model's ties to a collection.
615
+ _removeReference : function(model) {
616
+ if (this == model.collection) {
617
+ delete model.collection;
618
+ }
619
+ model.unbind('all', this._onModelEvent);
620
+ },
621
+
565
622
  // Internal method called every time a model in the set fires an event.
566
623
  // Sets need to update their indexes when models change ids. All other
567
- // events simply proxy through.
568
- _onModelEvent : function(ev, model) {
569
- if (ev === 'change:id') {
570
- delete this._byId[model.previous('id')];
624
+ // events simply proxy through. "add" and "remove" events that originate
625
+ // in other collections are ignored.
626
+ _onModelEvent : function(ev, model, collection, options) {
627
+ if ((ev == 'add' || ev == 'remove') && collection != this) return;
628
+ if (ev == 'destroy') {
629
+ this._remove(model, options);
630
+ }
631
+ if (model && ev === 'change:' + model.idAttribute) {
632
+ delete this._byId[model.previous(model.idAttribute)];
571
633
  this._byId[model.id] = model;
572
634
  }
573
635
  this.trigger.apply(this, arguments);
@@ -578,8 +640,8 @@
578
640
  // Underscore methods that we want to implement on the Collection.
579
641
  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
580
642
  'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
581
- 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
582
- 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];
643
+ 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
644
+ 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
583
645
 
584
646
  // Mix in each Underscore method as a proxy to `Collection#models`.
585
647
  _.each(methods, function(method) {
@@ -588,25 +650,26 @@
588
650
  };
589
651
  });
590
652
 
591
- // Backbone.Controller
653
+ // Backbone.Router
592
654
  // -------------------
593
655
 
594
- // Controllers map faux-URLs to actions, and fire events when routes are
656
+ // Routers map faux-URLs to actions, and fire events when routes are
595
657
  // matched. Creating a new one sets its `routes` hash, if not set statically.
596
- Backbone.Controller = function(options) {
658
+ Backbone.Router = function(options) {
597
659
  options || (options = {});
598
660
  if (options.routes) this.routes = options.routes;
599
661
  this._bindRoutes();
600
- this.initialize(options);
662
+ this.initialize.apply(this, arguments);
601
663
  };
602
664
 
603
665
  // Cached regular expressions for matching named param parts and splatted
604
666
  // parts of route strings.
605
- var namedParam = /:([\w\d]+)/g;
606
- var splatParam = /\*([\w\d]+)/g;
667
+ var namedParam = /:([\w\d]+)/g;
668
+ var splatParam = /\*([\w\d]+)/g;
669
+ var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
607
670
 
608
- // Set up all inheritable **Backbone.Controller** properties and methods.
609
- _.extend(Backbone.Controller.prototype, Backbone.Events, {
671
+ // Set up all inheritable **Backbone.Router** properties and methods.
672
+ _.extend(Backbone.Router.prototype, Backbone.Events, {
610
673
 
611
674
  // Initialize is an empty function by default. Override it with your own
612
675
  // initialization logic.
@@ -628,25 +691,31 @@
628
691
  }, this));
629
692
  },
630
693
 
631
- // Simple proxy to `Backbone.history` to save a fragment into the history,
632
- // without triggering routes.
633
- saveLocation : function(fragment) {
634
- Backbone.history.saveLocation(fragment);
694
+ // Simple proxy to `Backbone.history` to save a fragment into the history.
695
+ navigate : function(fragment, triggerRoute) {
696
+ Backbone.history.navigate(fragment, triggerRoute);
635
697
  },
636
698
 
637
- // Bind all defined routes to `Backbone.history`.
699
+ // Bind all defined routes to `Backbone.history`. We have to reverse the
700
+ // order of the routes here to support behavior where the most general
701
+ // routes can be defined at the bottom of the route map.
638
702
  _bindRoutes : function() {
639
703
  if (!this.routes) return;
704
+ var routes = [];
640
705
  for (var route in this.routes) {
641
- var name = this.routes[route];
642
- this.route(route, name, this[name]);
706
+ routes.unshift([route, this.routes[route]]);
707
+ }
708
+ for (var i = 0, l = routes.length; i < l; i++) {
709
+ this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
643
710
  }
644
711
  },
645
712
 
646
713
  // Convert a route string into a regular expression, suitable for matching
647
- // against the current location fragment.
714
+ // against the current location hash.
648
715
  _routeToRegExp : function(route) {
649
- route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)");
716
+ route = route.replace(escapeRegExp, "\\$&")
717
+ .replace(namedParam, "([^\/]*)")
718
+ .replace(splatParam, "(.*?)");
650
719
  return new RegExp('^' + route + '$');
651
720
  },
652
721
 
@@ -661,17 +730,22 @@
661
730
  // Backbone.History
662
731
  // ----------------
663
732
 
664
- // Handles cross-browser history management, based on URL hashes. If the
733
+ // Handles cross-browser history management, based on URL fragments. If the
665
734
  // browser does not support `onhashchange`, falls back to polling.
666
735
  Backbone.History = function() {
667
736
  this.handlers = [];
668
- this.fragment = this.getFragment();
669
737
  _.bindAll(this, 'checkUrl');
670
738
  };
671
739
 
672
740
  // Cached regex for cleaning hashes.
673
741
  var hashStrip = /^#*/;
674
742
 
743
+ // Cached regex for detecting MSIE.
744
+ var isExplorer = /msie [\w.]+/;
745
+
746
+ // Has the history handling already been started?
747
+ var historyStarted = false;
748
+
675
749
  // Set up all inheritable **Backbone.History** properties and methods.
676
750
  _.extend(Backbone.History.prototype, {
677
751
 
@@ -679,53 +753,92 @@
679
753
  // twenty times a second.
680
754
  interval: 50,
681
755
 
682
- // Get the cross-browser normalized URL fragment.
683
- getFragment : function(loc) {
684
- return (loc || window.location).hash.replace(hashStrip, '');
756
+ // Get the cross-browser normalized URL fragment, either from the URL,
757
+ // the hash, or the override.
758
+ getFragment : function(fragment, forcePushState) {
759
+ if (fragment == null) {
760
+ if (this._hasPushState || forcePushState) {
761
+ fragment = window.location.pathname;
762
+ var search = window.location.search;
763
+ if (search) fragment += search;
764
+ if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
765
+ } else {
766
+ fragment = window.location.hash;
767
+ }
768
+ }
769
+ return decodeURIComponent(fragment.replace(hashStrip, ''));
685
770
  },
686
771
 
687
772
  // Start the hash change handling, returning `true` if the current URL matches
688
773
  // an existing route, and `false` otherwise.
689
- start : function() {
690
- var docMode = document.documentMode;
691
- var oldIE = ($.browser.msie && (!docMode || docMode <= 7));
774
+ start : function(options) {
775
+
776
+ // Figure out the initial configuration. Do we need an iframe?
777
+ // Is pushState desired ... is it available?
778
+ if (historyStarted) throw new Error("Backbone.history has already been started");
779
+ this.options = _.extend({}, {root: '/'}, this.options, options);
780
+ this._wantsPushState = !!this.options.pushState;
781
+ this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
782
+ var fragment = this.getFragment();
783
+ var docMode = document.documentMode;
784
+ var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
692
785
  if (oldIE) {
693
786
  this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
787
+ this.navigate(fragment);
694
788
  }
695
- if ('onhashchange' in window && !oldIE) {
789
+
790
+ // Depending on whether we're using pushState or hashes, and whether
791
+ // 'onhashchange' is supported, determine how we check the URL state.
792
+ if (this._hasPushState) {
793
+ $(window).bind('popstate', this.checkUrl);
794
+ } else if ('onhashchange' in window && !oldIE) {
696
795
  $(window).bind('hashchange', this.checkUrl);
697
796
  } else {
698
797
  setInterval(this.checkUrl, this.interval);
699
798
  }
700
- return this.loadUrl();
799
+
800
+ // Determine if we need to change the base url, for a pushState link
801
+ // opened by a non-pushState browser.
802
+ this.fragment = fragment;
803
+ historyStarted = true;
804
+ var loc = window.location;
805
+ var atRoot = loc.pathname == this.options.root;
806
+ if (this._wantsPushState && !this._hasPushState && !atRoot) {
807
+ this.fragment = this.getFragment(null, true);
808
+ window.location.replace(this.options.root + '#' + this.fragment);
809
+ // Return immediately as browser will do redirect to new url
810
+ return true;
811
+ } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
812
+ this.fragment = loc.hash.replace(hashStrip, '');
813
+ window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
814
+ }
815
+
816
+ if (!this.options.silent) {
817
+ return this.loadUrl();
818
+ }
701
819
  },
702
820
 
703
- // Add a route to be tested when the hash changes. Routes are matched in the
704
- // order they are added.
821
+ // Add a route to be tested when the fragment changes. Routes added later may
822
+ // override previous routes.
705
823
  route : function(route, callback) {
706
- this.handlers.push({route : route, callback : callback});
824
+ this.handlers.unshift({route : route, callback : callback});
707
825
  },
708
826
 
709
827
  // Checks the current URL to see if it has changed, and if it has,
710
828
  // calls `loadUrl`, normalizing across the hidden iframe.
711
- checkUrl : function() {
829
+ checkUrl : function(e) {
712
830
  var current = this.getFragment();
713
- if (current == this.fragment && this.iframe) {
714
- current = this.getFragment(this.iframe.location);
715
- }
716
- if (current == this.fragment ||
717
- current == decodeURIComponent(this.fragment)) return false;
718
- if (this.iframe) {
719
- window.location.hash = this.iframe.location.hash = current;
720
- }
721
- this.loadUrl();
831
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
832
+ if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
833
+ if (this.iframe) this.navigate(current);
834
+ this.loadUrl() || this.loadUrl(window.location.hash);
722
835
  },
723
836
 
724
837
  // Attempt to load the current URL fragment. If a route succeeds with a
725
838
  // match, returns `true`. If no defined routes matches the fragment,
726
839
  // returns `false`.
727
- loadUrl : function() {
728
- var fragment = this.fragment = this.getFragment();
840
+ loadUrl : function(fragmentOverride) {
841
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
729
842
  var matched = _.any(this.handlers, function(handler) {
730
843
  if (handler.route.test(fragment)) {
731
844
  handler.callback(fragment);
@@ -738,14 +851,22 @@
738
851
  // Save a fragment into the hash history. You are responsible for properly
739
852
  // URL-encoding the fragment in advance. This does not trigger
740
853
  // a `hashchange` event.
741
- saveLocation : function(fragment) {
742
- fragment = (fragment || '').replace(hashStrip, '');
743
- if (this.fragment == fragment) return;
744
- window.location.hash = this.fragment = fragment;
745
- if (this.iframe && (fragment != this.getFragment(this.iframe.location))) {
746
- this.iframe.document.open().close();
747
- this.iframe.location.hash = fragment;
854
+ navigate : function(fragment, triggerRoute) {
855
+ var frag = (fragment || '').replace(hashStrip, '');
856
+ if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
857
+ if (this._hasPushState) {
858
+ var loc = window.location;
859
+ if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
860
+ this.fragment = frag;
861
+ window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
862
+ } else {
863
+ window.location.hash = this.fragment = frag;
864
+ if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
865
+ this.iframe.document.open().close();
866
+ this.iframe.location.hash = frag;
867
+ }
748
868
  }
869
+ if (triggerRoute) this.loadUrl(fragment);
749
870
  }
750
871
 
751
872
  });
@@ -756,10 +877,11 @@
756
877
  // Creating a Backbone.View creates its initial element outside of the DOM,
757
878
  // if an existing element is not provided...
758
879
  Backbone.View = function(options) {
880
+ this.cid = _.uniqueId('view');
759
881
  this._configure(options || {});
760
882
  this._ensureElement();
761
883
  this.delegateEvents();
762
- this.initialize(options);
884
+ this.initialize.apply(this, arguments);
763
885
  };
764
886
 
765
887
  // Element lookup, scoped to DOM elements within the current view.
@@ -770,7 +892,10 @@
770
892
  };
771
893
 
772
894
  // Cached regex to split keys for `delegate`.
773
- var eventSplitter = /^(\w+)\s*(.*)$/;
895
+ var eventSplitter = /^(\S+)\s*(.*)$/;
896
+
897
+ // List of view options to be merged as properties.
898
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
774
899
 
775
900
  // Set up all inheritable **Backbone.View** properties and methods.
776
901
  _.extend(Backbone.View.prototype, Backbone.Events, {
@@ -827,12 +952,15 @@
827
952
  // not `change`, `submit`, and `reset` in Internet Explorer.
828
953
  delegateEvents : function(events) {
829
954
  if (!(events || (events = this.events))) return;
830
- $(this.el).unbind();
955
+ if (_.isFunction(events)) events = events.call(this);
956
+ $(this.el).unbind('.delegateEvents' + this.cid);
831
957
  for (var key in events) {
832
- var methodName = events[key];
958
+ var method = this[events[key]];
959
+ if (!method) throw new Error('Event "' + events[key] + '" does not exist');
833
960
  var match = key.match(eventSplitter);
834
961
  var eventName = match[1], selector = match[2];
835
- var method = _.bind(this[methodName], this);
962
+ method = _.bind(method, this);
963
+ eventName += '.delegateEvents' + this.cid;
836
964
  if (selector === '') {
837
965
  $(this.el).bind(eventName, method);
838
966
  } else {
@@ -846,22 +974,26 @@
846
974
  // attached directly to the view.
847
975
  _configure : function(options) {
848
976
  if (this.options) options = _.extend({}, this.options, options);
849
- if (options.model) this.model = options.model;
850
- if (options.collection) this.collection = options.collection;
851
- if (options.el) this.el = options.el;
852
- if (options.id) this.id = options.id;
853
- if (options.className) this.className = options.className;
854
- if (options.tagName) this.tagName = options.tagName;
977
+ for (var i = 0, l = viewOptions.length; i < l; i++) {
978
+ var attr = viewOptions[i];
979
+ if (options[attr]) this[attr] = options[attr];
980
+ }
855
981
  this.options = options;
856
982
  },
857
983
 
858
984
  // Ensure that the View has a DOM element to render into.
985
+ // If `this.el` is a string, pass it through `$()`, take the first
986
+ // matching element, and re-assign it to `el`. Otherwise, create
987
+ // an element from the `id`, `className` and `tagName` proeprties.
859
988
  _ensureElement : function() {
860
- if (this.el) return;
861
- var attrs = {};
862
- if (this.id) attrs.id = this.id;
863
- if (this.className) attrs["class"] = this.className;
864
- this.el = this.make(this.tagName, attrs);
989
+ if (!this.el) {
990
+ var attrs = this.attributes || {};
991
+ if (this.id) attrs.id = this.id;
992
+ 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);
996
+ }
865
997
  }
866
998
 
867
999
  });
@@ -869,13 +1001,13 @@
869
1001
  // The self-propagating extend function that Backbone classes use.
870
1002
  var extend = function (protoProps, classProps) {
871
1003
  var child = inherits(this, protoProps, classProps);
872
- child.extend = extend;
1004
+ child.extend = this.extend;
873
1005
  return child;
874
1006
  };
875
1007
 
876
1008
  // Set up inheritance for the model, collection, and view.
877
1009
  Backbone.Model.extend = Backbone.Collection.extend =
878
- Backbone.Controller.extend = Backbone.View.extend = extend;
1010
+ Backbone.Router.extend = Backbone.View.extend = extend;
879
1011
 
880
1012
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
881
1013
  var methodMap = {
@@ -903,28 +1035,30 @@
903
1035
  // `application/json` with the model in a param named `model`.
904
1036
  // Useful when interfacing with server-side languages like **PHP** that make
905
1037
  // it difficult to read the body of `PUT` requests.
906
- Backbone.sync = function(method, model, success, error) {
1038
+ Backbone.sync = function(method, model, options) {
907
1039
  var type = methodMap[method];
908
- var modelJSON = (method === 'create' || method === 'update') ?
909
- JSON.stringify(model.toJSON()) : null;
910
1040
 
911
1041
  // Default JSON-request options.
912
- var params = {
913
- url: getUrl(model),
1042
+ var params = _.extend({
914
1043
  type: type,
915
- contentType: 'application/json',
916
- data: modelJSON,
917
- dataType: 'json',
918
- processData: false,
919
- success: success,
920
- error: error
921
- };
1044
+ dataType: 'json'
1045
+ }, options);
1046
+
1047
+ // Ensure that we have a URL.
1048
+ if (!params.url) {
1049
+ params.url = getUrl(model) || urlError();
1050
+ }
1051
+
1052
+ // Ensure that we have the appropriate request data.
1053
+ if (!params.data && model && (method == 'create' || method == 'update')) {
1054
+ params.contentType = 'application/json';
1055
+ params.data = JSON.stringify(model.toJSON());
1056
+ }
922
1057
 
923
1058
  // For older servers, emulate JSON by encoding the request into an HTML-form.
924
1059
  if (Backbone.emulateJSON) {
925
1060
  params.contentType = 'application/x-www-form-urlencoded';
926
- params.processData = true;
927
- params.data = modelJSON ? {model : modelJSON} : {};
1061
+ params.data = params.data ? {model : params.data} : {};
928
1062
  }
929
1063
 
930
1064
  // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
@@ -934,13 +1068,18 @@
934
1068
  if (Backbone.emulateJSON) params.data._method = type;
935
1069
  params.type = 'POST';
936
1070
  params.beforeSend = function(xhr) {
937
- xhr.setRequestHeader("X-HTTP-Method-Override", type);
1071
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
938
1072
  };
939
1073
  }
940
1074
  }
941
1075
 
1076
+ // Don't process data on a non-GET request.
1077
+ if (params.type !== 'GET' && !Backbone.emulateJSON) {
1078
+ params.processData = false;
1079
+ }
1080
+
942
1081
  // Make the request.
943
- $.ajax(params);
1082
+ return $.ajax(params);
944
1083
  };
945
1084
 
946
1085
  // Helpers
@@ -964,6 +1103,9 @@
964
1103
  child = function(){ return parent.apply(this, arguments); };
965
1104
  }
966
1105
 
1106
+ // Inherit class (static) properties from parent.
1107
+ _.extend(child, parent);
1108
+
967
1109
  // Set the prototype chain to inherit from `parent`, without calling
968
1110
  // `parent`'s constructor function.
969
1111
  ctor.prototype = parent.prototype;
@@ -976,7 +1118,7 @@
976
1118
  // Add static properties to the constructor function, if supplied.
977
1119
  if (staticProps) _.extend(child, staticProps);
978
1120
 
979
- // Correctly set child's `prototype.constructor`, for `instanceof`.
1121
+ // Correctly set child's `prototype.constructor`.
980
1122
  child.prototype.constructor = child;
981
1123
 
982
1124
  // Set a convenience property in case the parent's prototype is needed later.
@@ -988,15 +1130,20 @@
988
1130
  // Helper function to get a URL from a Model or Collection as a property
989
1131
  // or as a function.
990
1132
  var getUrl = function(object) {
991
- if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
1133
+ if (!(object && object.url)) return null;
992
1134
  return _.isFunction(object.url) ? object.url() : object.url;
993
1135
  };
994
1136
 
1137
+ // Throw an error when a URL is needed, and none is supplied.
1138
+ var urlError = function() {
1139
+ throw new Error('A "url" property or function must be specified');
1140
+ };
1141
+
995
1142
  // Wrap an optional error callback with a fallback error event.
996
1143
  var wrapError = function(onError, model, options) {
997
1144
  return function(resp) {
998
1145
  if (onError) {
999
- onError(model, resp);
1146
+ onError(model, resp, options);
1000
1147
  } else {
1001
1148
  model.trigger('error', model, resp, options);
1002
1149
  }
@@ -1005,7 +1152,7 @@
1005
1152
 
1006
1153
  // Helper function to escape a string for HTML rendering.
1007
1154
  var escapeHTML = function(string) {
1008
- return string.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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;');
1009
1156
  };
1010
1157
 
1011
- })();
1158
+ }).call(this);