backbone-rails 0.3.3.1 → 0.5.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,4 +1,4 @@
1
- // Backbone.js 0.3.3
1
+ // Backbone.js 0.5.0
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,24 +9,37 @@
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.0';
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
44
  // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will
32
45
  // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
@@ -57,7 +70,7 @@
57
70
  // Passing `"all"` will bind the callback to all events fired.
58
71
  bind : function(ev, callback) {
59
72
  var calls = this._callbacks || (this._callbacks = {});
60
- var list = this._callbacks[ev] || (this._callbacks[ev] = []);
73
+ var list = calls[ev] || (calls[ev] = []);
61
74
  list.push(callback);
62
75
  return this;
63
76
  },
@@ -77,7 +90,7 @@
77
90
  if (!list) return this;
78
91
  for (var i = 0, l = list.length; i < l; i++) {
79
92
  if (callback === list[i]) {
80
- list.splice(i, 1);
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.apply(this, args);
118
+ }
119
+ }
103
120
  }
104
121
  }
105
122
  return this;
@@ -113,15 +130,20 @@
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();
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
- this.initialize(attributes, options);
146
+ this.initialize.apply(this, arguments);
125
147
  };
126
148
 
127
149
  // Attach all inheritable methods to the Model prototype.
@@ -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
- }
536
- var already = this.getByCid(model);
583
+ model = this._prepareModel(model, options);
584
+ if (!model) return false;
585
+ var already = this.getByCid(model) || this.get(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);
@@ -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,16 +730,21 @@
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
- var hashStrip = /^#*/;
741
+ var hashStrip = /^#*!?/;
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;
674
748
 
675
749
  // Set up all inheritable **Backbone.History** properties and methods.
676
750
  _.extend(Backbone.History.prototype, {
@@ -679,53 +753,87 @@
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 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 started = this.loadUrl() || this.loadUrl(window.location.hash);
805
+ var atRoot = window.location.pathname == this.options.root;
806
+ if (this._wantsPushState && !this._hasPushState && !atRoot) {
807
+ this.fragment = this.getFragment(null, true);
808
+ window.location = this.options.root + '#' + this.fragment;
809
+ } else if (this._wantsPushState && this._hasPushState && atRoot && window.location.hash) {
810
+ this.navigate(window.location.hash);
811
+ } else {
812
+ return started;
813
+ }
701
814
  },
702
815
 
703
- // Add a route to be tested when the hash changes. Routes are matched in the
704
- // order they are added.
816
+ // Add a route to be tested when the fragment changes. Routes added later may
817
+ // override previous routes.
705
818
  route : function(route, callback) {
706
- this.handlers.push({route : route, callback : callback});
819
+ this.handlers.unshift({route : route, callback : callback});
707
820
  },
708
821
 
709
822
  // Checks the current URL to see if it has changed, and if it has,
710
823
  // calls `loadUrl`, normalizing across the hidden iframe.
711
- checkUrl : function() {
824
+ checkUrl : function(e) {
712
825
  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();
826
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
827
+ if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
828
+ if (this.iframe) this.navigate(current);
829
+ this.loadUrl() || this.loadUrl(window.location.hash);
722
830
  },
723
831
 
724
832
  // Attempt to load the current URL fragment. If a route succeeds with a
725
833
  // match, returns `true`. If no defined routes matches the fragment,
726
834
  // returns `false`.
727
- loadUrl : function() {
728
- var fragment = this.fragment = this.getFragment();
835
+ loadUrl : function(fragmentOverride) {
836
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
729
837
  var matched = _.any(this.handlers, function(handler) {
730
838
  if (handler.route.test(fragment)) {
731
839
  handler.callback(fragment);
@@ -738,14 +846,22 @@
738
846
  // Save a fragment into the hash history. You are responsible for properly
739
847
  // URL-encoding the fragment in advance. This does not trigger
740
848
  // a `hashchange` event.
741
- saveLocation : function(fragment) {
849
+ navigate : function(fragment, triggerRoute) {
742
850
  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;
851
+ if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
852
+ if (this._hasPushState) {
853
+ var loc = window.location;
854
+ if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
855
+ this.fragment = fragment;
856
+ window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + fragment);
857
+ } else {
858
+ window.location.hash = this.fragment = fragment;
859
+ if (this.iframe && (fragment != this.getFragment(this.iframe.location.hash))) {
860
+ this.iframe.document.open().close();
861
+ this.iframe.location.hash = fragment;
862
+ }
748
863
  }
864
+ if (triggerRoute) this.loadUrl(fragment);
749
865
  }
750
866
 
751
867
  });
@@ -756,10 +872,11 @@
756
872
  // Creating a Backbone.View creates its initial element outside of the DOM,
757
873
  // if an existing element is not provided...
758
874
  Backbone.View = function(options) {
875
+ this.cid = _.uniqueId('view');
759
876
  this._configure(options || {});
760
877
  this._ensureElement();
761
878
  this.delegateEvents();
762
- this.initialize(options);
879
+ this.initialize.apply(this, arguments);
763
880
  };
764
881
 
765
882
  // Element lookup, scoped to DOM elements within the current view.
@@ -770,7 +887,10 @@
770
887
  };
771
888
 
772
889
  // Cached regex to split keys for `delegate`.
773
- var eventSplitter = /^(\w+)\s*(.*)$/;
890
+ var eventSplitter = /^(\S+)\s*(.*)$/;
891
+
892
+ // List of view options to be merged as properties.
893
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
774
894
 
775
895
  // Set up all inheritable **Backbone.View** properties and methods.
776
896
  _.extend(Backbone.View.prototype, Backbone.Events, {
@@ -827,12 +947,14 @@
827
947
  // not `change`, `submit`, and `reset` in Internet Explorer.
828
948
  delegateEvents : function(events) {
829
949
  if (!(events || (events = this.events))) return;
830
- $(this.el).unbind();
950
+ $(this.el).unbind('.delegateEvents' + this.cid);
831
951
  for (var key in events) {
832
- var methodName = events[key];
952
+ var method = this[events[key]];
953
+ if (!method) throw new Error('Event "' + events[key] + '" does not exist');
833
954
  var match = key.match(eventSplitter);
834
955
  var eventName = match[1], selector = match[2];
835
- var method = _.bind(this[methodName], this);
956
+ method = _.bind(method, this);
957
+ eventName += '.delegateEvents' + this.cid;
836
958
  if (selector === '') {
837
959
  $(this.el).bind(eventName, method);
838
960
  } else {
@@ -846,22 +968,26 @@
846
968
  // attached directly to the view.
847
969
  _configure : function(options) {
848
970
  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;
971
+ for (var i = 0, l = viewOptions.length; i < l; i++) {
972
+ var attr = viewOptions[i];
973
+ if (options[attr]) this[attr] = options[attr];
974
+ }
855
975
  this.options = options;
856
976
  },
857
977
 
858
978
  // Ensure that the View has a DOM element to render into.
979
+ // If `this.el` is a string, pass it through `$()`, take the first
980
+ // matching element, and re-assign it to `el`. Otherwise, create
981
+ // an element from the `id`, `className` and `tagName` proeprties.
859
982
  _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);
983
+ if (!this.el) {
984
+ var attrs = this.attributes || {};
985
+ if (this.id) attrs.id = this.id;
986
+ if (this.className) attrs['class'] = this.className;
987
+ this.el = this.make(this.tagName, attrs);
988
+ } else if (_.isString(this.el)) {
989
+ this.el = $(this.el).get(0);
990
+ }
865
991
  }
866
992
 
867
993
  });
@@ -869,13 +995,13 @@
869
995
  // The self-propagating extend function that Backbone classes use.
870
996
  var extend = function (protoProps, classProps) {
871
997
  var child = inherits(this, protoProps, classProps);
872
- child.extend = extend;
998
+ child.extend = this.extend;
873
999
  return child;
874
1000
  };
875
1001
 
876
1002
  // Set up inheritance for the model, collection, and view.
877
1003
  Backbone.Model.extend = Backbone.Collection.extend =
878
- Backbone.Controller.extend = Backbone.View.extend = extend;
1004
+ Backbone.Router.extend = Backbone.View.extend = extend;
879
1005
 
880
1006
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
881
1007
  var methodMap = {
@@ -903,28 +1029,32 @@
903
1029
  // `application/json` with the model in a param named `model`.
904
1030
  // Useful when interfacing with server-side languages like **PHP** that make
905
1031
  // it difficult to read the body of `PUT` requests.
906
- Backbone.sync = function(method, model, success, error) {
1032
+ Backbone.sync = function(method, model, options) {
907
1033
  var type = methodMap[method];
908
- var modelJSON = (method === 'create' || method === 'update') ?
909
- JSON.stringify(model.toJSON()) : null;
910
1034
 
911
1035
  // Default JSON-request options.
912
- var params = {
913
- url: getUrl(model),
1036
+ var params = _.extend({
914
1037
  type: type,
915
- contentType: 'application/json',
916
- data: modelJSON,
917
1038
  dataType: 'json',
918
- processData: false,
919
- success: success,
920
- error: error
921
- };
1039
+ processData: false
1040
+ }, options);
1041
+
1042
+ // Ensure that we have a URL.
1043
+ if (!params.url) {
1044
+ params.url = getUrl(model) || urlError();
1045
+ }
1046
+
1047
+ // Ensure that we have the appropriate request data.
1048
+ if (!params.data && model && (method == 'create' || method == 'update')) {
1049
+ params.contentType = 'application/json';
1050
+ params.data = JSON.stringify(model.toJSON());
1051
+ }
922
1052
 
923
1053
  // For older servers, emulate JSON by encoding the request into an HTML-form.
924
1054
  if (Backbone.emulateJSON) {
925
1055
  params.contentType = 'application/x-www-form-urlencoded';
926
1056
  params.processData = true;
927
- params.data = modelJSON ? {model : modelJSON} : {};
1057
+ params.data = params.data ? {model : params.data} : {};
928
1058
  }
929
1059
 
930
1060
  // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
@@ -934,13 +1064,13 @@
934
1064
  if (Backbone.emulateJSON) params.data._method = type;
935
1065
  params.type = 'POST';
936
1066
  params.beforeSend = function(xhr) {
937
- xhr.setRequestHeader("X-HTTP-Method-Override", type);
1067
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
938
1068
  };
939
1069
  }
940
1070
  }
941
1071
 
942
1072
  // Make the request.
943
- $.ajax(params);
1073
+ return $.ajax(params);
944
1074
  };
945
1075
 
946
1076
  // Helpers
@@ -964,6 +1094,9 @@
964
1094
  child = function(){ return parent.apply(this, arguments); };
965
1095
  }
966
1096
 
1097
+ // Inherit class (static) properties from parent.
1098
+ _.extend(child, parent);
1099
+
967
1100
  // Set the prototype chain to inherit from `parent`, without calling
968
1101
  // `parent`'s constructor function.
969
1102
  ctor.prototype = parent.prototype;
@@ -976,7 +1109,7 @@
976
1109
  // Add static properties to the constructor function, if supplied.
977
1110
  if (staticProps) _.extend(child, staticProps);
978
1111
 
979
- // Correctly set child's `prototype.constructor`, for `instanceof`.
1112
+ // Correctly set child's `prototype.constructor`.
980
1113
  child.prototype.constructor = child;
981
1114
 
982
1115
  // Set a convenience property in case the parent's prototype is needed later.
@@ -988,15 +1121,20 @@
988
1121
  // Helper function to get a URL from a Model or Collection as a property
989
1122
  // or as a function.
990
1123
  var getUrl = function(object) {
991
- if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
1124
+ if (!(object && object.url)) return null;
992
1125
  return _.isFunction(object.url) ? object.url() : object.url;
993
1126
  };
994
1127
 
1128
+ // Throw an error when a URL is needed, and none is supplied.
1129
+ var urlError = function() {
1130
+ throw new Error('A "url" property or function must be specified');
1131
+ };
1132
+
995
1133
  // Wrap an optional error callback with a fallback error event.
996
1134
  var wrapError = function(onError, model, options) {
997
1135
  return function(resp) {
998
1136
  if (onError) {
999
- onError(model, resp);
1137
+ onError(model, resp, options);
1000
1138
  } else {
1001
1139
  model.trigger('error', model, resp, options);
1002
1140
  }
@@ -1005,7 +1143,7 @@
1005
1143
 
1006
1144
  // Helper function to escape a string for HTML rendering.
1007
1145
  var escapeHTML = function(string) {
1008
- return string.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1146
+ 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
1147
  };
1010
1148
 
1011
- })();
1149
+ }).call(this);