backbone-support 0.4.0 → 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,8 +1,4 @@
1
1
  var Helpers = {
2
- sleep: function() {
3
- waits(10);
4
- },
5
-
6
2
  setup: function() {
7
3
  this.teardown();
8
4
  this.append("test");
@@ -24,7 +24,7 @@ describe('Support.Observer', function() {
24
24
  var view, spy, source, callback;
25
25
  beforeEach(function() { Helpers.setup();
26
26
  view = new normalView();
27
- spy = sinon.spy(view, "bindTo");
27
+ spy = sinon.spy(view, "listenTo");
28
28
  callback = sinon.spy();
29
29
 
30
30
  source = new Backbone.Model({
@@ -36,63 +36,37 @@ describe('Support.Observer', function() {
36
36
  view, spy, source, callback = null;
37
37
  });
38
38
 
39
- it("should add the event to the bindings for the view", function() {
40
- view.bindTo(source, 'foobar', callback);
41
- expect(view.bindings.length).toEqual(1);
42
- });
43
-
44
- it("binds the event to the source object", function() {
45
- var mock = sinon.mock(source).expects('bind').once();
46
-
39
+ it("calls listenTo on this", function() {
47
40
  view.bindTo(source, 'change:title', callback);
48
-
49
- mock.verify();
41
+ expect(spy.called).toBeTruthy();
50
42
  });
51
43
  });
52
44
 
53
45
  describe("#unbindFromAll", function() {
54
46
  var view, spy, mock;
47
+
55
48
  beforeEach(function() {
56
49
  view = new normalView();
57
50
  spy = sinon.spy(view, 'unbindFromAll');
51
+ stopListeningSpy = sinon.spy(view, 'stopListening');
58
52
  callback = sinon.spy();
59
53
  source = new Backbone.Model({
60
54
  title: 'Model or Collection'
61
55
  });
62
- unbindSpy = sinon.spy(source, 'unbind');
63
-
64
- runs(function() {
65
- view.render();
66
- view.bindTo(source, 'foo', callback);
67
- view.bindTo(source, 'bar', callback);
68
- expect(view.bindings.length).toEqual(2);
69
- });
70
56
 
71
- Helpers.sleep();
57
+ view.render();
58
+ view.bindTo(source, 'foo', callback);
59
+ view.bindTo(source, 'bar', callback);
72
60
 
73
- runs(function() {
74
- view.leave();
75
- });
76
-
77
- Helpers.sleep();
61
+ view.leave();
78
62
  });
79
63
 
80
64
  it("calls the unbindFromAll method when leaving the view", function() {
81
- runs(function() {
82
- expect(spy.called).toBeTruthy();
83
- });
65
+ expect(spy.called).toBeTruthy();
84
66
  });
85
67
 
86
- it("calls unbind on the source object", function() {
87
- runs(function() {
88
- expect(unbindSpy.calledTwice).toBeTruthy();
89
- });
90
- });
91
-
92
- it("removes all the views bindings attached with bindTo", function() {
93
- runs(function() {
94
- expect(view.bindings.length).toEqual(0);
95
- });
68
+ it("calls stopListening on this", function() {
69
+ expect(stopListeningSpy.called).toBeTruthy();
96
70
  });
97
71
  });
98
72
 
@@ -11,17 +11,44 @@
11
11
  # - dist/**/*.js
12
12
  #
13
13
  src_files:
14
- - spec/javascripts/support/jquery.js
15
- - vendor/assets/javascripts/underscore.js
16
- - vendor/assets/javascripts/backbone.js
17
- - lib/assets/javascripts/backbone-support.js
18
- - lib/assets/javascripts/backbone-support/support.js
19
- - lib/assets/javascripts/backbone-support/observer.js
20
- - lib/**/*.js
14
+ - spec/javascripts/support/jquery.js
15
+ - vendor/assets/javascripts/underscore.js
16
+ - vendor/assets/javascripts/backbone.js
17
+ - lib/assets/javascripts/backbone-support.js
18
+ - lib/assets/javascripts/backbone-support/support.js
19
+ - lib/assets/javascripts/backbone-support/observer.js
20
+ - lib/**/*.js
21
+
22
+ # stylesheets
23
+ #
24
+ # Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
25
+ # Default: []
26
+ #
27
+ # EXAMPLE:
28
+ #
29
+ # stylesheets:
30
+ # - css/style.css
31
+ # - stylesheets/*.css
32
+ #
33
+ stylesheets:
34
+ - stylesheets/**/*.css
35
+
36
+ # helpers
37
+ #
38
+ # Return an array of filepaths relative to spec_dir to include before jasmine specs.
39
+ # Default: ["helpers/**/*.js"]
40
+ #
41
+ # EXAMPLE:
42
+ #
43
+ # helpers:
44
+ # - helpers/**/*.js
45
+ #
46
+ helpers:
47
+ - 'helpers/**/*.js'
21
48
 
22
49
  # spec_files
23
50
  #
24
- # Return an array of filepaths relative to spec_dir to include.
51
+ # Return an array of filepaths relative to spec_dir to include.
25
52
  # Default: ["**/*[sS]pec.js"]
26
53
  #
27
54
  # EXAMPLE:
@@ -30,6 +57,7 @@ src_files:
30
57
  # - **/*[sS]pec.js
31
58
  #
32
59
  spec_files:
60
+ - '**/*[sS]pec.js'
33
61
 
34
62
  # src_dir
35
63
  #
@@ -52,3 +80,51 @@ src_dir:
52
80
  # spec_dir: spec/javascripts
53
81
  #
54
82
  spec_dir:
83
+
84
+ # spec_helper
85
+ #
86
+ # Ruby file that Jasmine server will require before starting.
87
+ # Returned relative to your root path
88
+ # Default spec/javascripts/support/jasmine_helper.rb
89
+ #
90
+ # EXAMPLE:
91
+ #
92
+ # spec_helper: spec/javascripts/support/jasmine_helper.rb
93
+ #
94
+ spec_helper: spec/javascripts/support/jasmine_helper.rb
95
+
96
+ # boot_dir
97
+ #
98
+ # Boot directory path. Your boot_files must be returned relative to this path.
99
+ # Default: Built in boot file
100
+ #
101
+ # EXAMPLE:
102
+ #
103
+ # boot_dir: spec/javascripts/support/boot
104
+ #
105
+ boot_dir:
106
+
107
+ # boot_files
108
+ #
109
+ # Return an array of filepaths relative to boot_dir to include in order to boot Jasmine
110
+ # Default: Built in boot file
111
+ #
112
+ # EXAMPLE
113
+ #
114
+ # boot_files:
115
+ # - '**/*.js'
116
+ #
117
+ boot_files:
118
+
119
+ # rack_options
120
+ #
121
+ # Extra options to be passed to the rack server
122
+ # by default, Port and AccessLog are passed.
123
+ #
124
+ # This is an advanced options, and left empty by default
125
+ #
126
+ # EXAMPLE
127
+ #
128
+ # rack_options:
129
+ # server: 'thin'
130
+
@@ -0,0 +1,15 @@
1
+ #Use this file to set/override Jasmine configuration options
2
+ #You can remove it if you don't need it.
3
+ #This file is loaded *after* jasmine.yml is interpreted.
4
+ #
5
+ #Example: using a different boot file.
6
+ #Jasmine.configure do |config|
7
+ # config.boot_dir = '/absolute/path/to/boot_dir'
8
+ # config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] }
9
+ #end
10
+ #
11
+ #Example: prevent PhantomJS auto install, uses PhantomJS already on your path.
12
+ #Jasmine.configure do |config|
13
+ # config.prevent_phantom_js_auto_install = true
14
+ #end
15
+ #
@@ -60,77 +60,60 @@ describe("Support.SwappingRouter", function() {
60
60
  Helpers.teardown();
61
61
  });
62
62
 
63
- it("should be a backbone router", function() {
63
+ it("should be a backbone router", function(done) {
64
64
  var spy = sinon.spy();
65
65
  router.bind("route:index", spy);
66
66
 
67
- runs(function() {
68
- window.location.hash = "#test"
69
- });
70
-
71
- Helpers.sleep();
67
+ window.location.hash = "#test";
72
68
 
73
- runs(function() {
69
+ setTimeout(function() {
74
70
  expect(spy.called).toBeTruthy();
75
- });
71
+ done();
72
+ }, 30);
76
73
  });
77
74
 
78
- it("renders and swaps backbone views", function() {
79
- runs(function() {
80
- window.location.hash = "#red"
81
- });
82
-
83
- Helpers.sleep();
75
+ it("renders and swaps backbone views", function(done) {
76
+ window.location.hash = "#red";
84
77
 
85
- runs(function() {
78
+ setTimeout(function() {
86
79
  expect($("#test").text()).toEqual("Red!");
87
- });
88
-
89
- Helpers.sleep();
90
80
 
91
- runs(function() {
92
- window.location.hash = "#blue"
93
- });
81
+ window.location.hash = "#blue";
94
82
 
95
- Helpers.sleep();
83
+ setTimeout(function() {
84
+ expect($("#test").text()).toEqual("Blue!");
96
85
 
97
- runs(function() {
98
- expect($("#test").text()).toEqual("Blue!");
99
- });
86
+ done();
87
+ }, 30);
88
+ }, 30);
100
89
  });
101
90
 
102
- it("calls leave if it exists on a view", function() {
91
+ it("calls leave if it exists on a view", function(done) {
103
92
  var spy = sinon.spy(leaveViewInstance, "leave");
104
93
 
105
- runs(function() {
106
- window.location.hash = "#leave"
107
- });
108
-
109
- Helpers.sleep();
94
+ window.location.hash = "#leave";
110
95
 
111
- runs(function() {
112
- window.location.hash = "#red"
113
- });
96
+ setTimeout(function() {
97
+ window.location.hash = "#red";
114
98
 
115
- Helpers.sleep();
99
+ setTimeout(function() {
100
+ expect(spy.called).toBeTruthy();
101
+ expect($("#test").text()).toEqual("Red!");
116
102
 
117
- runs(function() {
118
- expect($("#test").text()).toEqual("Red!");
119
- expect(spy.called).toBeTruthy();
120
- });
103
+ done();
104
+ }, 30);
105
+ }, 30);
121
106
  });
122
107
 
123
- it("calls .swapped on the view after swapping", function() {
108
+ it("calls .swapped on the view after swapping", function(done) {
124
109
  var spy = sinon.spy(leaveViewInstance, "swapped");
125
110
 
126
- runs(function() {
127
- window.location.hash = "#leave"
128
- });
129
-
130
- Helpers.sleep()
111
+ window.location.hash = "#leave";
131
112
 
132
- runs(function() {
113
+ setTimeout(function() {
133
114
  expect(spy.called).toBeTruthy()
115
+
116
+ done();
134
117
  });
135
118
  });
136
119
  });
@@ -1,19 +1,35 @@
1
- // Backbone.js 1.0.0
1
+ // Backbone.js 1.1.2
2
2
 
3
- // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.
3
+ // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4
4
  // Backbone may be freely distributed under the MIT license.
5
5
  // For all details and documentation:
6
6
  // http://backbonejs.org
7
7
 
8
- (function(){
8
+ (function(root, factory) {
9
+
10
+ // Set up Backbone appropriately for the environment. Start with AMD.
11
+ if (typeof define === 'function' && define.amd) {
12
+ define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
13
+ // Export global even in AMD case in case this script is loaded with
14
+ // others that may still expect a global Backbone.
15
+ root.Backbone = factory(root, exports, _, $);
16
+ });
17
+
18
+ // Next for Node.js or CommonJS. jQuery may not be needed as a module.
19
+ } else if (typeof exports !== 'undefined') {
20
+ var _ = require('underscore');
21
+ factory(root, exports, _);
22
+
23
+ // Finally, as a browser global.
24
+ } else {
25
+ root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
26
+ }
27
+
28
+ }(this, function(root, Backbone, _, $) {
9
29
 
10
30
  // Initial Setup
11
31
  // -------------
12
32
 
13
- // Save a reference to the global object (`window` in the browser, `exports`
14
- // on the server).
15
- var root = this;
16
-
17
33
  // Save the previous value of the `Backbone` variable, so that it can be
18
34
  // restored later on, if `noConflict` is used.
19
35
  var previousBackbone = root.Backbone;
@@ -24,25 +40,12 @@
24
40
  var slice = array.slice;
25
41
  var splice = array.splice;
26
42
 
27
- // The top-level namespace. All public Backbone classes and modules will
28
- // be attached to this. Exported for both the browser and the server.
29
- var Backbone;
30
- if (typeof exports !== 'undefined') {
31
- Backbone = exports;
32
- } else {
33
- Backbone = root.Backbone = {};
34
- }
35
-
36
43
  // Current version of the library. Keep in sync with `package.json`.
37
- Backbone.VERSION = '1.0.0';
38
-
39
- // Require Underscore, if we're on the server, and it's not already present.
40
- var _ = root._;
41
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
44
+ Backbone.VERSION = '1.1.2';
42
45
 
43
46
  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
44
47
  // the `$` variable.
45
- Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
48
+ Backbone.$ = $;
46
49
 
47
50
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
48
51
  // to its previous owner. Returns a reference to this Backbone object.
@@ -52,7 +55,7 @@
52
55
  };
53
56
 
54
57
  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
55
- // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
58
+ // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
56
59
  // set a `X-Http-Method-Override` header.
57
60
  Backbone.emulateHTTP = false;
58
61
 
@@ -108,10 +111,9 @@
108
111
  var retain, ev, events, names, i, l, j, k;
109
112
  if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
110
113
  if (!name && !callback && !context) {
111
- this._events = {};
114
+ this._events = void 0;
112
115
  return this;
113
116
  }
114
-
115
117
  names = name ? [name] : _.keys(this._events);
116
118
  for (i = 0, l = names.length; i < l; i++) {
117
119
  name = names[i];
@@ -151,14 +153,15 @@
151
153
  // Tell this object to stop listening to either specific events ... or
152
154
  // to every object it's currently listening to.
153
155
  stopListening: function(obj, name, callback) {
154
- var listeners = this._listeners;
155
- if (!listeners) return this;
156
- var deleteListener = !name && !callback;
157
- if (typeof name === 'object') callback = this;
158
- if (obj) (listeners = {})[obj._listenerId] = obj;
159
- for (var id in listeners) {
160
- listeners[id].off(name, callback, this);
161
- if (deleteListener) delete this._listeners[id];
156
+ var listeningTo = this._listeningTo;
157
+ if (!listeningTo) return this;
158
+ var remove = !name && !callback;
159
+ if (!callback && typeof name === 'object') callback = this;
160
+ if (obj) (listeningTo = {})[obj._listenId] = obj;
161
+ for (var id in listeningTo) {
162
+ obj = listeningTo[id];
163
+ obj.off(name, callback, this);
164
+ if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
162
165
  }
163
166
  return this;
164
167
  }
@@ -204,7 +207,7 @@
204
207
  case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
205
208
  case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
206
209
  case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
207
- default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
210
+ default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
208
211
  }
209
212
  };
210
213
 
@@ -215,10 +218,10 @@
215
218
  // listening to.
216
219
  _.each(listenMethods, function(implementation, method) {
217
220
  Events[method] = function(obj, name, callback) {
218
- var listeners = this._listeners || (this._listeners = {});
219
- var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
220
- listeners[id] = obj;
221
- if (typeof name === 'object') callback = this;
221
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
222
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
223
+ listeningTo[id] = obj;
224
+ if (!callback && typeof name === 'object') callback = this;
222
225
  obj[implementation](name, callback, this);
223
226
  return this;
224
227
  };
@@ -243,24 +246,18 @@
243
246
  // Create a new model with the specified attributes. A client id (`cid`)
244
247
  // is automatically generated and assigned for you.
245
248
  var Model = Backbone.Model = function(attributes, options) {
246
- var defaults;
247
249
  var attrs = attributes || {};
248
250
  options || (options = {});
249
251
  this.cid = _.uniqueId('c');
250
252
  this.attributes = {};
251
- _.extend(this, _.pick(options, modelOptions));
253
+ if (options.collection) this.collection = options.collection;
252
254
  if (options.parse) attrs = this.parse(attrs, options) || {};
253
- if (defaults = _.result(this, 'defaults')) {
254
- attrs = _.defaults({}, attrs, defaults);
255
- }
255
+ attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
256
256
  this.set(attrs, options);
257
257
  this.changed = {};
258
258
  this.initialize.apply(this, arguments);
259
259
  };
260
260
 
261
- // A list of options to be attached directly to the model, if provided.
262
- var modelOptions = ['url', 'urlRoot', 'collection'];
263
-
264
261
  // Attach all inheritable methods to the Model prototype.
265
262
  _.extend(Model.prototype, Events, {
266
263
 
@@ -355,7 +352,7 @@
355
352
 
356
353
  // Trigger all relevant attribute changes.
357
354
  if (!silent) {
358
- if (changes.length) this._pending = true;
355
+ if (changes.length) this._pending = options;
359
356
  for (var i = 0, l = changes.length; i < l; i++) {
360
357
  this.trigger('change:' + changes[i], this, current[changes[i]], options);
361
358
  }
@@ -366,6 +363,7 @@
366
363
  if (changing) return this;
367
364
  if (!silent) {
368
365
  while (this._pending) {
366
+ options = this._pending;
369
367
  this._pending = false;
370
368
  this.trigger('change', this, options);
371
369
  }
@@ -456,13 +454,16 @@
456
454
  (attrs = {})[key] = val;
457
455
  }
458
456
 
459
- // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
460
- if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
461
-
462
457
  options = _.extend({validate: true}, options);
463
458
 
464
- // Do not persist invalid models.
465
- if (!this._validate(attrs, options)) return false;
459
+ // If we're not waiting and attributes exist, save acts as
460
+ // `set(attr).save(null, opts)` with validation. Otherwise, check if
461
+ // the model will be valid when the attributes, if any, are set.
462
+ if (attrs && !options.wait) {
463
+ if (!this.set(attrs, options)) return false;
464
+ } else {
465
+ if (!this._validate(attrs, options)) return false;
466
+ }
466
467
 
467
468
  // Set temporary attributes if `{wait: true}`.
468
469
  if (attrs && options.wait) {
@@ -530,9 +531,12 @@
530
531
  // using Backbone's restful methods, override this to change the endpoint
531
532
  // that will be called.
532
533
  url: function() {
533
- var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
534
+ var base =
535
+ _.result(this, 'urlRoot') ||
536
+ _.result(this.collection, 'url') ||
537
+ urlError();
534
538
  if (this.isNew()) return base;
535
- return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
539
+ return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
536
540
  },
537
541
 
538
542
  // **parse** converts a response into the hash of attributes to be `set` on
@@ -548,7 +552,7 @@
548
552
 
549
553
  // A model is new if it has never been saved to the server, and lacks an id.
550
554
  isNew: function() {
551
- return this.id == null;
555
+ return !this.has(this.idAttribute);
552
556
  },
553
557
 
554
558
  // Check if the model is currently in a valid state.
@@ -563,7 +567,7 @@
563
567
  attrs = _.extend({}, this.attributes, attrs);
564
568
  var error = this.validationError = this.validate(attrs, options) || null;
565
569
  if (!error) return true;
566
- this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
570
+ this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
567
571
  return false;
568
572
  }
569
573
 
@@ -596,7 +600,6 @@
596
600
  // its models in sort order, as they're added and removed.
597
601
  var Collection = Backbone.Collection = function(models, options) {
598
602
  options || (options = {});
599
- if (options.url) this.url = options.url;
600
603
  if (options.model) this.model = options.model;
601
604
  if (options.comparator !== void 0) this.comparator = options.comparator;
602
605
  this._reset();
@@ -606,7 +609,7 @@
606
609
 
607
610
  // Default options for `Collection#set`.
608
611
  var setOptions = {add: true, remove: true, merge: true};
609
- var addOptions = {add: true, merge: false, remove: false};
612
+ var addOptions = {add: true, remove: false};
610
613
 
611
614
  // Define the Collection's inheritable methods.
612
615
  _.extend(Collection.prototype, Events, {
@@ -632,16 +635,17 @@
632
635
 
633
636
  // Add a model, or list of models to the set.
634
637
  add: function(models, options) {
635
- return this.set(models, _.defaults(options || {}, addOptions));
638
+ return this.set(models, _.extend({merge: false}, options, addOptions));
636
639
  },
637
640
 
638
641
  // Remove a model, or a list of models from the set.
639
642
  remove: function(models, options) {
640
- models = _.isArray(models) ? models.slice() : [models];
643
+ var singular = !_.isArray(models);
644
+ models = singular ? [models] : _.clone(models);
641
645
  options || (options = {});
642
646
  var i, l, index, model;
643
647
  for (i = 0, l = models.length; i < l; i++) {
644
- model = this.get(models[i]);
648
+ model = models[i] = this.get(models[i]);
645
649
  if (!model) continue;
646
650
  delete this._byId[model.id];
647
651
  delete this._byId[model.cid];
@@ -652,9 +656,9 @@
652
656
  options.index = index;
653
657
  model.trigger('remove', model, this, options);
654
658
  }
655
- this._removeReference(model);
659
+ this._removeReference(model, options);
656
660
  }
657
- return this;
661
+ return singular ? models[0] : models;
658
662
  },
659
663
 
660
664
  // Update a collection by `set`-ing a new list of models, adding new ones,
@@ -662,43 +666,57 @@
662
666
  // already exist in the collection, as necessary. Similar to **Model#set**,
663
667
  // the core operation for updating the data contained by the collection.
664
668
  set: function(models, options) {
665
- options = _.defaults(options || {}, setOptions);
669
+ options = _.defaults({}, options, setOptions);
666
670
  if (options.parse) models = this.parse(models, options);
667
- if (!_.isArray(models)) models = models ? [models] : [];
668
- var i, l, model, attrs, existing, sort;
671
+ var singular = !_.isArray(models);
672
+ models = singular ? (models ? [models] : []) : _.clone(models);
673
+ var i, l, id, model, attrs, existing, sort;
669
674
  var at = options.at;
675
+ var targetModel = this.model;
670
676
  var sortable = this.comparator && (at == null) && options.sort !== false;
671
677
  var sortAttr = _.isString(this.comparator) ? this.comparator : null;
672
678
  var toAdd = [], toRemove = [], modelMap = {};
679
+ var add = options.add, merge = options.merge, remove = options.remove;
680
+ var order = !sortable && add && remove ? [] : false;
673
681
 
674
682
  // Turn bare objects into model references, and prevent invalid models
675
683
  // from being added.
676
684
  for (i = 0, l = models.length; i < l; i++) {
677
- if (!(model = this._prepareModel(models[i], options))) continue;
685
+ attrs = models[i] || {};
686
+ if (attrs instanceof Model) {
687
+ id = model = attrs;
688
+ } else {
689
+ id = attrs[targetModel.prototype.idAttribute || 'id'];
690
+ }
678
691
 
679
692
  // If a duplicate is found, prevent it from being added and
680
693
  // optionally merge it into the existing model.
681
- if (existing = this.get(model)) {
682
- if (options.remove) modelMap[existing.cid] = true;
683
- if (options.merge) {
684
- existing.set(model.attributes, options);
694
+ if (existing = this.get(id)) {
695
+ if (remove) modelMap[existing.cid] = true;
696
+ if (merge) {
697
+ attrs = attrs === model ? model.attributes : attrs;
698
+ if (options.parse) attrs = existing.parse(attrs, options);
699
+ existing.set(attrs, options);
685
700
  if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
686
701
  }
702
+ models[i] = existing;
687
703
 
688
- // This is a new model, push it to the `toAdd` list.
689
- } else if (options.add) {
704
+ // If this is a new, valid model, push it to the `toAdd` list.
705
+ } else if (add) {
706
+ model = models[i] = this._prepareModel(attrs, options);
707
+ if (!model) continue;
690
708
  toAdd.push(model);
691
-
692
- // Listen to added models' events, and index models for lookup by
693
- // `id` and by `cid`.
694
- model.on('all', this._onModelEvent, this);
695
- this._byId[model.cid] = model;
696
- if (model.id != null) this._byId[model.id] = model;
709
+ this._addReference(model, options);
697
710
  }
711
+
712
+ // Do not add multiple models with the same `id`.
713
+ model = existing || model;
714
+ if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
715
+ modelMap[model.id] = true;
698
716
  }
699
717
 
700
718
  // Remove nonexistent models if appropriate.
701
- if (options.remove) {
719
+ if (remove) {
702
720
  for (i = 0, l = this.length; i < l; ++i) {
703
721
  if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
704
722
  }
@@ -706,29 +724,35 @@
706
724
  }
707
725
 
708
726
  // See if sorting is needed, update `length` and splice in new models.
709
- if (toAdd.length) {
727
+ if (toAdd.length || (order && order.length)) {
710
728
  if (sortable) sort = true;
711
729
  this.length += toAdd.length;
712
730
  if (at != null) {
713
- splice.apply(this.models, [at, 0].concat(toAdd));
731
+ for (i = 0, l = toAdd.length; i < l; i++) {
732
+ this.models.splice(at + i, 0, toAdd[i]);
733
+ }
714
734
  } else {
715
- push.apply(this.models, toAdd);
735
+ if (order) this.models.length = 0;
736
+ var orderedModels = order || toAdd;
737
+ for (i = 0, l = orderedModels.length; i < l; i++) {
738
+ this.models.push(orderedModels[i]);
739
+ }
716
740
  }
717
741
  }
718
742
 
719
743
  // Silently sort the collection if appropriate.
720
744
  if (sort) this.sort({silent: true});
721
745
 
722
- if (options.silent) return this;
723
-
724
- // Trigger `add` events.
725
- for (i = 0, l = toAdd.length; i < l; i++) {
726
- (model = toAdd[i]).trigger('add', model, this, options);
746
+ // Unless silenced, it's time to fire all appropriate add/sort events.
747
+ if (!options.silent) {
748
+ for (i = 0, l = toAdd.length; i < l; i++) {
749
+ (model = toAdd[i]).trigger('add', model, this, options);
750
+ }
751
+ if (sort || (order && order.length)) this.trigger('sort', this, options);
727
752
  }
728
753
 
729
- // Trigger `sort` if the collection was sorted.
730
- if (sort) this.trigger('sort', this, options);
731
- return this;
754
+ // Return the added (or merged) model (or models).
755
+ return singular ? models[0] : models;
732
756
  },
733
757
 
734
758
  // When you have more items than you want to add or remove individually,
@@ -738,20 +762,18 @@
738
762
  reset: function(models, options) {
739
763
  options || (options = {});
740
764
  for (var i = 0, l = this.models.length; i < l; i++) {
741
- this._removeReference(this.models[i]);
765
+ this._removeReference(this.models[i], options);
742
766
  }
743
767
  options.previousModels = this.models;
744
768
  this._reset();
745
- this.add(models, _.extend({silent: true}, options));
769
+ models = this.add(models, _.extend({silent: true}, options));
746
770
  if (!options.silent) this.trigger('reset', this, options);
747
- return this;
771
+ return models;
748
772
  },
749
773
 
750
774
  // Add a model to the end of the collection.
751
775
  push: function(model, options) {
752
- model = this._prepareModel(model, options);
753
- this.add(model, _.extend({at: this.length}, options));
754
- return model;
776
+ return this.add(model, _.extend({at: this.length}, options));
755
777
  },
756
778
 
757
779
  // Remove a model from the end of the collection.
@@ -763,9 +785,7 @@
763
785
 
764
786
  // Add a model to the beginning of the collection.
765
787
  unshift: function(model, options) {
766
- model = this._prepareModel(model, options);
767
- this.add(model, _.extend({at: 0}, options));
768
- return model;
788
+ return this.add(model, _.extend({at: 0}, options));
769
789
  },
770
790
 
771
791
  // Remove a model from the beginning of the collection.
@@ -776,14 +796,14 @@
776
796
  },
777
797
 
778
798
  // Slice out a sub-array of models from the collection.
779
- slice: function(begin, end) {
780
- return this.models.slice(begin, end);
799
+ slice: function() {
800
+ return slice.apply(this.models, arguments);
781
801
  },
782
802
 
783
803
  // Get a model from the set by id.
784
804
  get: function(obj) {
785
805
  if (obj == null) return void 0;
786
- return this._byId[obj.id != null ? obj.id : obj.cid || obj];
806
+ return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
787
807
  },
788
808
 
789
809
  // Get the model at the given index.
@@ -827,16 +847,6 @@
827
847
  return this;
828
848
  },
829
849
 
830
- // Figure out the smallest index at which a model should be inserted so as
831
- // to maintain order.
832
- sortedIndex: function(model, value, context) {
833
- value || (value = this.comparator);
834
- var iterator = _.isFunction(value) ? value : function(model) {
835
- return model.get(value);
836
- };
837
- return _.sortedIndex(this.models, model, iterator, context);
838
- },
839
-
840
850
  // Pluck an attribute from each model in the collection.
841
851
  pluck: function(attr) {
842
852
  return _.invoke(this.models, 'get', attr);
@@ -869,7 +879,7 @@
869
879
  if (!options.wait) this.add(model, options);
870
880
  var collection = this;
871
881
  var success = options.success;
872
- options.success = function(resp) {
882
+ options.success = function(model, resp) {
873
883
  if (options.wait) collection.add(model, options);
874
884
  if (success) success(model, resp, options);
875
885
  };
@@ -899,22 +909,25 @@
899
909
  // Prepare a hash of attributes (or other model) to be added to this
900
910
  // collection.
901
911
  _prepareModel: function(attrs, options) {
902
- if (attrs instanceof Model) {
903
- if (!attrs.collection) attrs.collection = this;
904
- return attrs;
905
- }
906
- options || (options = {});
912
+ if (attrs instanceof Model) return attrs;
913
+ options = options ? _.clone(options) : {};
907
914
  options.collection = this;
908
915
  var model = new this.model(attrs, options);
909
- if (!model._validate(attrs, options)) {
910
- this.trigger('invalid', this, attrs, options);
911
- return false;
912
- }
913
- return model;
916
+ if (!model.validationError) return model;
917
+ this.trigger('invalid', this, model.validationError, options);
918
+ return false;
919
+ },
920
+
921
+ // Internal method to create a model's ties to a collection.
922
+ _addReference: function(model, options) {
923
+ this._byId[model.cid] = model;
924
+ if (model.id != null) this._byId[model.id] = model;
925
+ if (!model.collection) model.collection = this;
926
+ model.on('all', this._onModelEvent, this);
914
927
  },
915
928
 
916
929
  // Internal method to sever a model's ties to a collection.
917
- _removeReference: function(model) {
930
+ _removeReference: function(model, options) {
918
931
  if (this === model.collection) delete model.collection;
919
932
  model.off('all', this._onModelEvent, this);
920
933
  },
@@ -942,8 +955,8 @@
942
955
  'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
943
956
  'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
944
957
  'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
945
- 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
946
- 'isEmpty', 'chain'];
958
+ 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
959
+ 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
947
960
 
948
961
  // Mix in each Underscore method as a proxy to `Collection#models`.
949
962
  _.each(methods, function(method) {
@@ -955,7 +968,7 @@
955
968
  });
956
969
 
957
970
  // Underscore methods that take a property name as an argument.
958
- var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
971
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
959
972
 
960
973
  // Use attributes instead of properties.
961
974
  _.each(attributeMethods, function(method) {
@@ -982,7 +995,8 @@
982
995
  // if an existing element is not provided...
983
996
  var View = Backbone.View = function(options) {
984
997
  this.cid = _.uniqueId('view');
985
- this._configure(options || {});
998
+ options || (options = {});
999
+ _.extend(this, _.pick(options, viewOptions));
986
1000
  this._ensureElement();
987
1001
  this.initialize.apply(this, arguments);
988
1002
  this.delegateEvents();
@@ -1001,7 +1015,7 @@
1001
1015
  tagName: 'div',
1002
1016
 
1003
1017
  // jQuery delegate for element lookup, scoped to DOM elements within the
1004
- // current view. This should be prefered to global lookups where possible.
1018
+ // current view. This should be preferred to global lookups where possible.
1005
1019
  $: function(selector) {
1006
1020
  return this.$el.find(selector);
1007
1021
  },
@@ -1041,7 +1055,7 @@
1041
1055
  //
1042
1056
  // {
1043
1057
  // 'mousedown .title': 'edit',
1044
- // 'click .button': 'save'
1058
+ // 'click .button': 'save',
1045
1059
  // 'click .open': function(e) { ... }
1046
1060
  // }
1047
1061
  //
@@ -1079,16 +1093,6 @@
1079
1093
  return this;
1080
1094
  },
1081
1095
 
1082
- // Performs the initial configuration of a View with a set of options.
1083
- // Keys with special meaning *(e.g. model, collection, id, className)* are
1084
- // attached directly to the view. See `viewOptions` for an exhaustive
1085
- // list.
1086
- _configure: function(options) {
1087
- if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1088
- _.extend(this, _.pick(options, viewOptions));
1089
- this.options = options;
1090
- },
1091
-
1092
1096
  // Ensure that the View has a DOM element to render into.
1093
1097
  // If `this.el` is a string, pass it through `$()`, take the first
1094
1098
  // matching element, and re-assign it to `el`. Otherwise, create
@@ -1174,8 +1178,7 @@
1174
1178
  // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1175
1179
  // that still has ActiveX enabled by default, override jQuery to use that
1176
1180
  // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
1177
- if (params.type === 'PATCH' && window.ActiveXObject &&
1178
- !(window.external && window.external.msActiveXFilteringEnabled)) {
1181
+ if (params.type === 'PATCH' && noXhrPatch) {
1179
1182
  params.xhr = function() {
1180
1183
  return new ActiveXObject("Microsoft.XMLHTTP");
1181
1184
  };
@@ -1187,6 +1190,10 @@
1187
1190
  return xhr;
1188
1191
  };
1189
1192
 
1193
+ var noXhrPatch =
1194
+ typeof window !== 'undefined' && !!window.ActiveXObject &&
1195
+ !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
1196
+
1190
1197
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1191
1198
  var methodMap = {
1192
1199
  'create': 'POST',
@@ -1244,7 +1251,7 @@
1244
1251
  var router = this;
1245
1252
  Backbone.history.route(route, function(fragment) {
1246
1253
  var args = router._extractParameters(route, fragment);
1247
- callback && callback.apply(router, args);
1254
+ router.execute(callback, args);
1248
1255
  router.trigger.apply(router, ['route:' + name].concat(args));
1249
1256
  router.trigger('route', name, args);
1250
1257
  Backbone.history.trigger('route', router, name, args);
@@ -1252,6 +1259,12 @@
1252
1259
  return this;
1253
1260
  },
1254
1261
 
1262
+ // Execute a route handler with the provided parameters. This is an
1263
+ // excellent place to do pre-route setup or post-route cleanup.
1264
+ execute: function(callback, args) {
1265
+ if (callback) callback.apply(this, args);
1266
+ },
1267
+
1255
1268
  // Simple proxy to `Backbone.history` to save a fragment into the history.
1256
1269
  navigate: function(fragment, options) {
1257
1270
  Backbone.history.navigate(fragment, options);
@@ -1275,11 +1288,11 @@
1275
1288
  _routeToRegExp: function(route) {
1276
1289
  route = route.replace(escapeRegExp, '\\$&')
1277
1290
  .replace(optionalParam, '(?:$1)?')
1278
- .replace(namedParam, function(match, optional){
1279
- return optional ? match : '([^\/]+)';
1291
+ .replace(namedParam, function(match, optional) {
1292
+ return optional ? match : '([^/?]+)';
1280
1293
  })
1281
- .replace(splatParam, '(.*?)');
1282
- return new RegExp('^' + route + '$');
1294
+ .replace(splatParam, '([^?]*?)');
1295
+ return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
1283
1296
  },
1284
1297
 
1285
1298
  // Given a route, and a URL fragment that it matches, return the array of
@@ -1287,7 +1300,9 @@
1287
1300
  // treated as `null` to normalize cross-browser behavior.
1288
1301
  _extractParameters: function(route, fragment) {
1289
1302
  var params = route.exec(fragment).slice(1);
1290
- return _.map(params, function(param) {
1303
+ return _.map(params, function(param, i) {
1304
+ // Don't decode the search params.
1305
+ if (i === params.length - 1) return param || null;
1291
1306
  return param ? decodeURIComponent(param) : null;
1292
1307
  });
1293
1308
  }
@@ -1325,6 +1340,9 @@
1325
1340
  // Cached regex for removing a trailing slash.
1326
1341
  var trailingSlash = /\/$/;
1327
1342
 
1343
+ // Cached regex for stripping urls of hash.
1344
+ var pathStripper = /#.*$/;
1345
+
1328
1346
  // Has the history handling already been started?
1329
1347
  History.started = false;
1330
1348
 
@@ -1335,6 +1353,11 @@
1335
1353
  // twenty times a second.
1336
1354
  interval: 50,
1337
1355
 
1356
+ // Are we at the app root?
1357
+ atRoot: function() {
1358
+ return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
1359
+ },
1360
+
1338
1361
  // Gets the true hash value. Cannot use location.hash directly due to bug
1339
1362
  // in Firefox where location.hash will always be decoded.
1340
1363
  getHash: function(window) {
@@ -1347,9 +1370,9 @@
1347
1370
  getFragment: function(fragment, forcePushState) {
1348
1371
  if (fragment == null) {
1349
1372
  if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1350
- fragment = this.location.pathname;
1373
+ fragment = decodeURI(this.location.pathname + this.location.search);
1351
1374
  var root = this.root.replace(trailingSlash, '');
1352
- if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
1375
+ if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1353
1376
  } else {
1354
1377
  fragment = this.getHash();
1355
1378
  }
@@ -1365,7 +1388,7 @@
1365
1388
 
1366
1389
  // Figure out the initial configuration. Do we need an iframe?
1367
1390
  // Is pushState desired ... is it available?
1368
- this.options = _.extend({}, {root: '/'}, this.options, options);
1391
+ this.options = _.extend({root: '/'}, this.options, options);
1369
1392
  this.root = this.options.root;
1370
1393
  this._wantsHashChange = this.options.hashChange !== false;
1371
1394
  this._wantsPushState = !!this.options.pushState;
@@ -1378,7 +1401,8 @@
1378
1401
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1379
1402
 
1380
1403
  if (oldIE && this._wantsHashChange) {
1381
- this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1404
+ var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
1405
+ this.iframe = frame.hide().appendTo('body')[0].contentWindow;
1382
1406
  this.navigate(fragment);
1383
1407
  }
1384
1408
 
@@ -1396,21 +1420,26 @@
1396
1420
  // opened by a non-pushState browser.
1397
1421
  this.fragment = fragment;
1398
1422
  var loc = this.location;
1399
- var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1400
-
1401
- // If we've started off with a route from a `pushState`-enabled browser,
1402
- // but we're currently in a browser that doesn't support it...
1403
- if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1404
- this.fragment = this.getFragment(null, true);
1405
- this.location.replace(this.root + this.location.search + '#' + this.fragment);
1406
- // Return immediately as browser will do redirect to new url
1407
- return true;
1408
1423
 
1409
- // Or if we've started out with a hash-based route, but we're currently
1410
- // in a browser where it could be `pushState`-based instead...
1411
- } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1412
- this.fragment = this.getHash().replace(routeStripper, '');
1413
- this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1424
+ // Transition from hashChange to pushState or vice versa if both are
1425
+ // requested.
1426
+ if (this._wantsHashChange && this._wantsPushState) {
1427
+
1428
+ // If we've started off with a route from a `pushState`-enabled
1429
+ // browser, but we're currently in a browser that doesn't support it...
1430
+ if (!this._hasPushState && !this.atRoot()) {
1431
+ this.fragment = this.getFragment(null, true);
1432
+ this.location.replace(this.root + '#' + this.fragment);
1433
+ // Return immediately as browser will do redirect to new url
1434
+ return true;
1435
+
1436
+ // Or if we've started out with a hash-based route, but we're currently
1437
+ // in a browser where it could be `pushState`-based instead...
1438
+ } else if (this._hasPushState && this.atRoot() && loc.hash) {
1439
+ this.fragment = this.getHash().replace(routeStripper, '');
1440
+ this.history.replaceState({}, document.title, this.root + this.fragment);
1441
+ }
1442
+
1414
1443
  }
1415
1444
 
1416
1445
  if (!this.options.silent) return this.loadUrl();
@@ -1420,7 +1449,7 @@
1420
1449
  // but possibly useful for unit testing Routers.
1421
1450
  stop: function() {
1422
1451
  Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1423
- clearInterval(this._checkUrlInterval);
1452
+ if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
1424
1453
  History.started = false;
1425
1454
  },
1426
1455
 
@@ -1439,21 +1468,20 @@
1439
1468
  }
1440
1469
  if (current === this.fragment) return false;
1441
1470
  if (this.iframe) this.navigate(current);
1442
- this.loadUrl() || this.loadUrl(this.getHash());
1471
+ this.loadUrl();
1443
1472
  },
1444
1473
 
1445
1474
  // Attempt to load the current URL fragment. If a route succeeds with a
1446
1475
  // match, returns `true`. If no defined routes matches the fragment,
1447
1476
  // returns `false`.
1448
- loadUrl: function(fragmentOverride) {
1449
- var fragment = this.fragment = this.getFragment(fragmentOverride);
1450
- var matched = _.any(this.handlers, function(handler) {
1477
+ loadUrl: function(fragment) {
1478
+ fragment = this.fragment = this.getFragment(fragment);
1479
+ return _.any(this.handlers, function(handler) {
1451
1480
  if (handler.route.test(fragment)) {
1452
1481
  handler.callback(fragment);
1453
1482
  return true;
1454
1483
  }
1455
1484
  });
1456
- return matched;
1457
1485
  },
1458
1486
 
1459
1487
  // Save a fragment into the hash history, or replace the URL state if the
@@ -1465,11 +1493,18 @@
1465
1493
  // you wish to modify the current URL without adding an entry to the history.
1466
1494
  navigate: function(fragment, options) {
1467
1495
  if (!History.started) return false;
1468
- if (!options || options === true) options = {trigger: options};
1469
- fragment = this.getFragment(fragment || '');
1496
+ if (!options || options === true) options = {trigger: !!options};
1497
+
1498
+ var url = this.root + (fragment = this.getFragment(fragment || ''));
1499
+
1500
+ // Strip the hash for matching.
1501
+ fragment = fragment.replace(pathStripper, '');
1502
+
1470
1503
  if (this.fragment === fragment) return;
1471
1504
  this.fragment = fragment;
1472
- var url = this.root + fragment;
1505
+
1506
+ // Don't include a trailing slash on the root.
1507
+ if (fragment === '' && url !== '/') url = url.slice(0, -1);
1473
1508
 
1474
1509
  // If pushState is available, we use it to set the fragment as a real URL.
1475
1510
  if (this._hasPushState) {
@@ -1492,7 +1527,7 @@
1492
1527
  } else {
1493
1528
  return this.location.assign(url);
1494
1529
  }
1495
- if (options.trigger) this.loadUrl(fragment);
1530
+ if (options.trigger) return this.loadUrl(fragment);
1496
1531
  },
1497
1532
 
1498
1533
  // Update the hash location, either replacing the current entry, or adding
@@ -1560,7 +1595,7 @@
1560
1595
  };
1561
1596
 
1562
1597
  // Wrap an optional error callback with a fallback error event.
1563
- var wrapError = function (model, options) {
1598
+ var wrapError = function(model, options) {
1564
1599
  var error = options.error;
1565
1600
  options.error = function(resp) {
1566
1601
  if (error) error(model, resp, options);
@@ -1568,4 +1603,6 @@
1568
1603
  };
1569
1604
  };
1570
1605
 
1571
- }).call(this);
1606
+ return Backbone;
1607
+
1608
+ }));