backbone-rails 0.3.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);