rails-backbone 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Backbone-Rails [![Build Status](https://secure.travis-ci.org/codebrew/backbone-rails.png)](http://travis-ci.org/codebrew/backbone-rails)
2
2
 
3
- Easily setup and use backbone.js (0.9.2) with rails 3.1 and greater
3
+ Easily setup and use backbone.js (0.9.9) with rails 3.1 and greater
4
4
 
5
5
  Follow [@TheRyanFitz on Twitter](http://twitter.com/#!/TheRyanFitz). Tweet any questions or suggestions you have about the project.
6
6
 
@@ -98,4 +98,4 @@ If you prefer haml, this is equivalent to inserting the following code into `app
98
98
 
99
99
 
100
100
  Now start your server `rails s` and browse to [localhost:3000/posts](http://localhost:3000/posts)
101
- You should now have a fully functioning single page crud app for Post models.
101
+ You should now have a fully functioning single page crud app for Post models.
@@ -4,5 +4,5 @@ class <%= view_namespace %>.<%= @action.camelize %>View extends Backbone.View
4
4
  template: JST["<%= jst @action %>"]
5
5
 
6
6
  render: ->
7
- $(@el).html(@template())
7
+ @$el.html(@template())
8
8
  return this
@@ -14,4 +14,4 @@
14
14
 
15
15
  </form>
16
16
 
17
- <a href="#/index">Back</a>
17
+ <a href="#/index">Back</a>
@@ -1,23 +1,23 @@
1
1
  <%= view_namespace %> ||= {}
2
2
 
3
3
  class <%= view_namespace %>.EditView extends Backbone.View
4
- template : JST["<%= jst 'edit' %>"]
4
+ template: JST["<%= jst 'edit' %>"]
5
5
 
6
- events :
7
- "submit #edit-<%= singular_name %>" : "update"
6
+ events:
7
+ "submit #edit-<%= singular_name %>": "update"
8
8
 
9
- update : (e) ->
9
+ update: (e) ->
10
10
  e.preventDefault()
11
11
  e.stopPropagation()
12
12
 
13
13
  @model.save(null,
14
- success : (<%= singular_name %>) =>
14
+ success: (<%= singular_name %>) =>
15
15
  @model = <%= singular_name %>
16
16
  window.location.hash = "/#{@model.id}"
17
17
  )
18
18
 
19
- render : ->
20
- $(@el).html(@template(@model.toJSON() ))
19
+ render: ->
20
+ @$el.html(@template(@model.toJSON() ))
21
21
 
22
22
  this.$("form").backboneLink(@model)
23
23
 
@@ -14,7 +14,7 @@ class <%= view_namespace %>.IndexView extends Backbone.View
14
14
  @$("tbody").append(view.render().el)
15
15
 
16
16
  render: =>
17
- $(@el).html(@template(<%= plural_model_name %>: @options.<%= plural_model_name %>.toJSON() ))
17
+ @$el.html(@template(<%= plural_model_name %>: @options.<%= plural_model_name %>.toJSON() ))
18
18
  @addAll()
19
19
 
20
20
  return this
@@ -15,5 +15,5 @@ class <%= view_namespace %>.<%= singular_name.camelize %>View extends Backbone.V
15
15
  return false
16
16
 
17
17
  render: ->
18
- $(@el).html(@template(@model.toJSON() ))
18
+ @$el.html(@template(@model.toJSON() ))
19
19
  return this
@@ -30,7 +30,7 @@ class <%= view_namespace %>.NewView extends Backbone.View
30
30
  )
31
31
 
32
32
  render: ->
33
- $(@el).html(@template(@model.toJSON() ))
33
+ @$el.html(@template(@model.toJSON() ))
34
34
 
35
35
  this.$("form").backboneLink(@model)
36
36
 
@@ -4,5 +4,5 @@ class <%= view_namespace %>.ShowView extends Backbone.View
4
4
  template: JST["<%= jst 'show' %>"]
5
5
 
6
6
  render: ->
7
- $(@el).html(@template(@model.toJSON() ))
7
+ @$el.html(@template(@model.toJSON() ))
8
8
  return this
@@ -1,4 +1,4 @@
1
- // Backbone.js 0.9.2
1
+ // Backbone.js 0.9.9
2
2
 
3
3
  // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
4
4
  // Backbone may be freely distributed under the MIT license.
@@ -10,7 +10,7 @@
10
10
  // Initial Setup
11
11
  // -------------
12
12
 
13
- // Save a reference to the global object (`window` in the browser, `global`
13
+ // Save a reference to the global object (`window` in the browser, `exports`
14
14
  // on the server).
15
15
  var root = this;
16
16
 
@@ -18,9 +18,11 @@
18
18
  // restored later on, if `noConflict` is used.
19
19
  var previousBackbone = root.Backbone;
20
20
 
21
- // Create a local reference to slice/splice.
22
- var slice = Array.prototype.slice;
23
- var splice = Array.prototype.splice;
21
+ // Create a local reference to array methods.
22
+ var array = [];
23
+ var push = array.push;
24
+ var slice = array.slice;
25
+ var splice = array.splice;
24
26
 
25
27
  // The top-level namespace. All public Backbone classes and modules will
26
28
  // be attached to this. Exported for both CommonJS and the browser.
@@ -32,23 +34,14 @@
32
34
  }
33
35
 
34
36
  // Current version of the library. Keep in sync with `package.json`.
35
- Backbone.VERSION = '0.9.2';
37
+ Backbone.VERSION = '0.9.9';
36
38
 
37
39
  // Require Underscore, if we're on the server, and it's not already present.
38
40
  var _ = root._;
39
41
  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
40
42
 
41
43
  // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
42
- var $ = root.jQuery || root.Zepto || root.ender;
43
-
44
- // Set the JavaScript library that will be used for DOM manipulation and
45
- // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
46
- // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
47
- // alternate JavaScript library (or a mock library for testing your views
48
- // outside of a browser).
49
- Backbone.setDomLibrary = function(lib) {
50
- $ = lib;
51
- };
44
+ Backbone.$ = root.jQuery || root.Zepto || root.ender;
52
45
 
53
46
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
54
47
  // to its previous owner. Returns a reference to this Backbone object.
@@ -69,14 +62,51 @@
69
62
  Backbone.emulateJSON = false;
70
63
 
71
64
  // Backbone.Events
72
- // -----------------
65
+ // ---------------
73
66
 
74
- // Regular expression used to split event strings
67
+ // Regular expression used to split event strings.
75
68
  var eventSplitter = /\s+/;
76
69
 
70
+ // Implement fancy features of the Events API such as multiple event
71
+ // names `"change blur"` and jQuery-style event maps `{change: action}`
72
+ // in terms of the existing API.
73
+ var eventsApi = function(obj, action, name, rest) {
74
+ if (!name) return true;
75
+ if (typeof name === 'object') {
76
+ for (var key in name) {
77
+ obj[action].apply(obj, [key, name[key]].concat(rest));
78
+ }
79
+ } else if (eventSplitter.test(name)) {
80
+ var names = name.split(eventSplitter);
81
+ for (var i = 0, l = names.length; i < l; i++) {
82
+ obj[action].apply(obj, [names[i]].concat(rest));
83
+ }
84
+ } else {
85
+ return true;
86
+ }
87
+ };
88
+
89
+ // Optimized internal dispatch function for triggering events. Tries to
90
+ // keep the usual cases speedy (most Backbone events have 3 arguments).
91
+ var triggerEvents = function(obj, events, args) {
92
+ var ev, i = -1, l = events.length;
93
+ switch (args.length) {
94
+ case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
95
+ return;
96
+ case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
97
+ return;
98
+ case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
99
+ return;
100
+ case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
101
+ return;
102
+ default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
103
+ }
104
+ };
105
+
77
106
  // A module that can be mixed in to *any object* in order to provide it with
78
- // custom events. You may bind with `on` or remove with `off` callback functions
79
- // to an event; trigger`-ing an event fires all callbacks in succession.
107
+ // custom events. You may bind with `on` or remove with `off` callback
108
+ // functions to an event; `trigger`-ing an event fires all callbacks in
109
+ // succession.
80
110
  //
81
111
  // var object = {};
82
112
  // _.extend(object, Backbone.Events);
@@ -85,58 +115,58 @@
85
115
  //
86
116
  var Events = Backbone.Events = {
87
117
 
88
- // Bind one or more space separated events, `events`, to a `callback`
89
- // function. Passing `"all"` will bind the callback to all events fired.
90
- on: function(events, callback, context) {
91
-
92
- var calls, event, node, tail, list;
93
- if (!callback) return this;
94
- events = events.split(eventSplitter);
95
- calls = this._callbacks || (this._callbacks = {});
96
-
97
- // Create an immutable callback list, allowing traversal during
98
- // modification. The tail is an empty object that will always be used
99
- // as the next node.
100
- while (event = events.shift()) {
101
- list = calls[event];
102
- node = list ? list.tail : {};
103
- node.next = tail = {};
104
- node.context = context;
105
- node.callback = callback;
106
- calls[event] = {tail: tail, next: list ? list.next : node};
107
- }
108
-
118
+ // Bind one or more space separated events, or an events map,
119
+ // to a `callback` function. Passing `"all"` will bind the callback to
120
+ // all events fired.
121
+ on: function(name, callback, context) {
122
+ if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
123
+ this._events || (this._events = {});
124
+ var list = this._events[name] || (this._events[name] = []);
125
+ list.push({callback: callback, context: context, ctx: context || this});
109
126
  return this;
110
127
  },
111
128
 
112
- // Remove one or many callbacks. If `context` is null, removes all callbacks
113
- // with that function. If `callback` is null, removes all callbacks for the
114
- // event. If `events` is null, removes all bound callbacks for all events.
115
- off: function(events, callback, context) {
116
- var event, calls, node, tail, cb, ctx;
129
+ // Bind events to only be triggered a single time. After the first time
130
+ // the callback is invoked, it will be removed.
131
+ once: function(name, callback, context) {
132
+ if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
133
+ var self = this;
134
+ var once = _.once(function() {
135
+ self.off(name, once);
136
+ callback.apply(this, arguments);
137
+ });
138
+ once._callback = callback;
139
+ this.on(name, once, context);
140
+ return this;
141
+ },
117
142
 
118
- // No events, or removing *all* events.
119
- if (!(calls = this._callbacks)) return;
120
- if (!(events || callback || context)) {
121
- delete this._callbacks;
143
+ // Remove one or many callbacks. If `context` is null, removes all
144
+ // callbacks with that function. If `callback` is null, removes all
145
+ // callbacks for the event. If `events` is null, removes all bound
146
+ // callbacks for all events.
147
+ off: function(name, callback, context) {
148
+ var list, ev, events, names, i, l, j, k;
149
+ if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
150
+ if (!name && !callback && !context) {
151
+ this._events = {};
122
152
  return this;
123
153
  }
124
154
 
125
- // Loop through the listed events and contexts, splicing them out of the
126
- // linked list of callbacks if appropriate.
127
- events = events ? events.split(eventSplitter) : _.keys(calls);
128
- while (event = events.shift()) {
129
- node = calls[event];
130
- delete calls[event];
131
- if (!node || !(callback || context)) continue;
132
- // Create a new list, omitting the indicated callbacks.
133
- tail = node.tail;
134
- while ((node = node.next) !== tail) {
135
- cb = node.callback;
136
- ctx = node.context;
137
- if ((callback && cb !== callback) || (context && ctx !== context)) {
138
- this.on(event, cb, ctx);
155
+ names = name ? [name] : _.keys(this._events);
156
+ for (i = 0, l = names.length; i < l; i++) {
157
+ name = names[i];
158
+ if (list = this._events[name]) {
159
+ events = [];
160
+ if (callback || context) {
161
+ for (j = 0, k = list.length; j < k; j++) {
162
+ ev = list[j];
163
+ if ((callback && callback !== (ev.callback._callback || ev.callback)) ||
164
+ (context && context !== ev.context)) {
165
+ events.push(ev);
166
+ }
167
+ }
139
168
  }
169
+ this._events[name] = events;
140
170
  }
141
171
  }
142
172
 
@@ -147,40 +177,53 @@
147
177
  // passed the same arguments as `trigger` is, apart from the event name
148
178
  // (unless you're listening on `"all"`, which will cause your callback to
149
179
  // receive the true name of the event as the first argument).
150
- trigger: function(events) {
151
- var event, node, calls, tail, args, all, rest;
152
- if (!(calls = this._callbacks)) return this;
153
- all = calls.all;
154
- events = events.split(eventSplitter);
155
- rest = slice.call(arguments, 1);
156
-
157
- // For each event, walk through the linked list of callbacks twice,
158
- // first to trigger the event, then to trigger any `"all"` callbacks.
159
- while (event = events.shift()) {
160
- if (node = calls[event]) {
161
- tail = node.tail;
162
- while ((node = node.next) !== tail) {
163
- node.callback.apply(node.context || this, rest);
164
- }
165
- }
166
- if (node = all) {
167
- tail = node.tail;
168
- args = [event].concat(rest);
169
- while ((node = node.next) !== tail) {
170
- node.callback.apply(node.context || this, args);
171
- }
180
+ trigger: function(name) {
181
+ if (!this._events) return this;
182
+ var args = slice.call(arguments, 1);
183
+ if (!eventsApi(this, 'trigger', name, args)) return this;
184
+ var events = this._events[name];
185
+ var allEvents = this._events.all;
186
+ if (events) triggerEvents(this, events, args);
187
+ if (allEvents) triggerEvents(this, allEvents, arguments);
188
+ return this;
189
+ },
190
+
191
+ // An inversion-of-control version of `on`. Tell *this* object to listen to
192
+ // an event in another object ... keeping track of what it's listening to.
193
+ listenTo: function(object, events, callback) {
194
+ var listeners = this._listeners || (this._listeners = {});
195
+ var id = object._listenerId || (object._listenerId = _.uniqueId('l'));
196
+ listeners[id] = object;
197
+ object.on(events, callback || this, this);
198
+ return this;
199
+ },
200
+
201
+ // Tell this object to stop listening to either specific events ... or
202
+ // to every object it's currently listening to.
203
+ stopListening: function(object, events, callback) {
204
+ var listeners = this._listeners;
205
+ if (!listeners) return;
206
+ if (object) {
207
+ object.off(events, callback, this);
208
+ if (!events && !callback) delete listeners[object._listenerId];
209
+ } else {
210
+ for (var id in listeners) {
211
+ listeners[id].off(null, null, this);
172
212
  }
213
+ this._listeners = {};
173
214
  }
174
-
175
215
  return this;
176
216
  }
177
-
178
217
  };
179
218
 
180
219
  // Aliases for backwards compatibility.
181
220
  Events.bind = Events.on;
182
221
  Events.unbind = Events.off;
183
222
 
223
+ // Allow the `Backbone` object to serve as a global event bus, for folks who
224
+ // want global "pubsub" in a convenient place.
225
+ _.extend(Backbone, Events);
226
+
184
227
  // Backbone.Model
185
228
  // --------------
186
229
 
@@ -188,23 +231,16 @@
188
231
  // is automatically generated and assigned for you.
189
232
  var Model = Backbone.Model = function(attributes, options) {
190
233
  var defaults;
191
- attributes || (attributes = {});
192
- if (options && options.parse) attributes = this.parse(attributes);
193
- if (defaults = getValue(this, 'defaults')) {
194
- attributes = _.extend({}, defaults, attributes);
195
- }
196
- if (options && options.collection) this.collection = options.collection;
197
- this.attributes = {};
198
- this._escapedAttributes = {};
234
+ var attrs = attributes || {};
199
235
  this.cid = _.uniqueId('c');
200
236
  this.changed = {};
201
- this._silent = {};
202
- this._pending = {};
203
- this.set(attributes, {silent: true});
204
- // Reset change tracking.
205
- this.changed = {};
206
- this._silent = {};
207
- this._pending = {};
237
+ this.attributes = {};
238
+ this._changes = [];
239
+ if (options && options.collection) this.collection = options.collection;
240
+ if (options && options.parse) attrs = this.parse(attrs);
241
+ if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults);
242
+ this.set(attrs, {silent: true});
243
+ this._currentAttributes = _.clone(this.attributes);
208
244
  this._previousAttributes = _.clone(this.attributes);
209
245
  this.initialize.apply(this, arguments);
210
246
  };
@@ -215,14 +251,6 @@
215
251
  // A hash of attributes whose current and previous value differ.
216
252
  changed: null,
217
253
 
218
- // A hash of attributes that have silently changed since the last time
219
- // `change` was called. Will become pending attributes on the next call.
220
- _silent: null,
221
-
222
- // A hash of attributes that have changed since the last `'change'` event
223
- // began.
224
- _pending: null,
225
-
226
254
  // The default name for the JSON `id` attribute is `"id"`. MongoDB and
227
255
  // CouchDB users may want to set this to `"_id"`.
228
256
  idAttribute: 'id',
@@ -236,6 +264,11 @@
236
264
  return _.clone(this.attributes);
237
265
  },
238
266
 
267
+ // Proxy `Backbone.sync` by default.
268
+ sync: function() {
269
+ return Backbone.sync.apply(this, arguments);
270
+ },
271
+
239
272
  // Get the value of an attribute.
240
273
  get: function(attr) {
241
274
  return this.attributes[attr];
@@ -243,10 +276,7 @@
243
276
 
244
277
  // Get the HTML-escaped value of an attribute.
245
278
  escape: function(attr) {
246
- var html;
247
- if (html = this._escapedAttributes[attr]) return html;
248
- var val = this.get(attr);
249
- return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
279
+ return _.escape(this.get(attr));
250
280
  },
251
281
 
252
282
  // Returns `true` if the attribute contains a value that is not null
@@ -257,23 +287,21 @@
257
287
 
258
288
  // Set a hash of model attributes on the object, firing `"change"` unless
259
289
  // you choose to silence it.
260
- set: function(key, value, options) {
261
- var attrs, attr, val;
290
+ set: function(key, val, options) {
291
+ var attr, attrs;
292
+ if (key == null) return this;
262
293
 
263
294
  // Handle both `"key", value` and `{key: value}` -style arguments.
264
- if (_.isObject(key) || key == null) {
295
+ if (_.isObject(key)) {
265
296
  attrs = key;
266
- options = value;
297
+ options = val;
267
298
  } else {
268
- attrs = {};
269
- attrs[key] = value;
299
+ (attrs = {})[key] = val;
270
300
  }
271
301
 
272
302
  // Extract attributes and options.
273
- options || (options = {});
274
- if (!attrs) return this;
275
- if (attrs instanceof Model) attrs = attrs.attributes;
276
- if (options.unset) for (attr in attrs) attrs[attr] = void 0;
303
+ var silent = options && options.silent;
304
+ var unset = options && options.unset;
277
305
 
278
306
  // Run validation.
279
307
  if (!this._validate(attrs, options)) return false;
@@ -281,52 +309,38 @@
281
309
  // Check for changes of `id`.
282
310
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
283
311
 
284
- var changes = options.changes = {};
285
312
  var now = this.attributes;
286
- var escaped = this._escapedAttributes;
287
- var prev = this._previousAttributes || {};
288
313
 
289
314
  // For each `set` attribute...
290
315
  for (attr in attrs) {
291
316
  val = attrs[attr];
292
317
 
293
- // If the new and current value differ, record the change.
294
- if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
295
- delete escaped[attr];
296
- (options.silent ? this._silent : changes)[attr] = true;
297
- }
298
-
299
- // Update or delete the current value.
300
- options.unset ? delete now[attr] : now[attr] = val;
301
-
302
- // If the new and previous value differ, record the change. If not,
303
- // then remove changes for this attribute.
304
- if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
305
- this.changed[attr] = val;
306
- if (!options.silent) this._pending[attr] = true;
307
- } else {
308
- delete this.changed[attr];
309
- delete this._pending[attr];
310
- }
318
+ // Update or delete the current value, and track the change.
319
+ unset ? delete now[attr] : now[attr] = val;
320
+ this._changes.push(attr, val);
311
321
  }
312
322
 
323
+ // Signal that the model's state has potentially changed, and we need
324
+ // to recompute the actual changes.
325
+ this._hasComputed = false;
326
+
313
327
  // Fire the `"change"` events.
314
- if (!options.silent) this.change(options);
328
+ if (!silent) this.change(options);
315
329
  return this;
316
330
  },
317
331
 
318
332
  // Remove an attribute from the model, firing `"change"` unless you choose
319
333
  // to silence it. `unset` is a noop if the attribute doesn't exist.
320
334
  unset: function(attr, options) {
321
- (options || (options = {})).unset = true;
322
- return this.set(attr, null, options);
335
+ return this.set(attr, void 0, _.extend({}, options, {unset: true}));
323
336
  },
324
337
 
325
338
  // Clear all attributes on the model, firing `"change"` unless you choose
326
339
  // to silence it.
327
340
  clear: function(options) {
328
- (options || (options = {})).unset = true;
329
- return this.set(_.clone(this.attributes), options);
341
+ var attrs = {};
342
+ for (var key in this.attributes) attrs[key] = void 0;
343
+ return this.set(attrs, _.extend({}, options, {unset: true}));
330
344
  },
331
345
 
332
346
  // Fetch the model from the server. If the server's representation of the
@@ -334,35 +348,34 @@
334
348
  // triggering a `"change"` event.
335
349
  fetch: function(options) {
336
350
  options = options ? _.clone(options) : {};
351
+ if (options.parse === void 0) options.parse = true;
337
352
  var model = this;
338
353
  var success = options.success;
339
354
  options.success = function(resp, status, xhr) {
340
- if (!model.set(model.parse(resp, xhr), options)) return false;
341
- if (success) success(model, resp);
355
+ if (!model.set(model.parse(resp), options)) return false;
356
+ if (success) success(model, resp, options);
342
357
  };
343
- options.error = Backbone.wrapError(options.error, model, options);
344
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
358
+ return this.sync('read', this, options);
345
359
  },
346
360
 
347
361
  // Set a hash of model attributes, and sync the model to the server.
348
362
  // If the server returns an attributes hash that differs, the model's
349
363
  // state will be `set` again.
350
- save: function(key, value, options) {
351
- var attrs, current;
364
+ save: function(key, val, options) {
365
+ var attrs, current, done;
352
366
 
353
- // Handle both `("key", value)` and `({key: value})` -style calls.
354
- if (_.isObject(key) || key == null) {
367
+ // Handle both `"key", value` and `{key: value}` -style arguments.
368
+ if (key == null || _.isObject(key)) {
355
369
  attrs = key;
356
- options = value;
357
- } else {
358
- attrs = {};
359
- attrs[key] = value;
370
+ options = val;
371
+ } else if (key != null) {
372
+ (attrs = {})[key] = val;
360
373
  }
361
374
  options = options ? _.clone(options) : {};
362
375
 
363
376
  // If we're "wait"-ing to set changed attributes, validate early.
364
377
  if (options.wait) {
365
- if (!this._validate(attrs, options)) return false;
378
+ if (attrs && !this._validate(attrs, options)) return false;
366
379
  current = _.clone(this.attributes);
367
380
  }
368
381
 
@@ -372,29 +385,33 @@
372
385
  return false;
373
386
  }
374
387
 
388
+ // Do not persist invalid models.
389
+ if (!attrs && !this._validate(null, options)) return false;
390
+
375
391
  // After a successful server-side save, the client is (optionally)
376
392
  // updated with the server-side state.
377
393
  var model = this;
378
394
  var success = options.success;
379
395
  options.success = function(resp, status, xhr) {
380
- var serverAttrs = model.parse(resp, xhr);
381
- if (options.wait) {
382
- delete options.wait;
383
- serverAttrs = _.extend(attrs || {}, serverAttrs);
384
- }
396
+ done = true;
397
+ var serverAttrs = model.parse(resp);
398
+ if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
385
399
  if (!model.set(serverAttrs, options)) return false;
386
- if (success) {
387
- success(model, resp);
388
- } else {
389
- model.trigger('sync', model, resp, options);
390
- }
400
+ if (success) success(model, resp, options);
391
401
  };
392
402
 
393
403
  // Finish configuring and sending the Ajax request.
394
- options.error = Backbone.wrapError(options.error, model, options);
395
- var method = this.isNew() ? 'create' : 'update';
396
- var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
397
- if (options.wait) this.set(current, silentOptions);
404
+ var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
405
+ if (method == 'patch') options.attrs = attrs;
406
+ var xhr = this.sync(method, this, options);
407
+
408
+ // When using `wait`, reset attributes to original values unless
409
+ // `success` has been called already.
410
+ if (!done && options.wait) {
411
+ this.clear(silentOptions);
412
+ this.set(current, silentOptions);
413
+ }
414
+
398
415
  return xhr;
399
416
  },
400
417
 
@@ -406,27 +423,22 @@
406
423
  var model = this;
407
424
  var success = options.success;
408
425
 
409
- var triggerDestroy = function() {
426
+ var destroy = function() {
410
427
  model.trigger('destroy', model, model.collection, options);
411
428
  };
412
429
 
430
+ options.success = function(resp) {
431
+ if (options.wait || model.isNew()) destroy();
432
+ if (success) success(model, resp, options);
433
+ };
434
+
413
435
  if (this.isNew()) {
414
- triggerDestroy();
436
+ options.success();
415
437
  return false;
416
438
  }
417
439
 
418
- options.success = function(resp) {
419
- if (options.wait) triggerDestroy();
420
- if (success) {
421
- success(model, resp);
422
- } else {
423
- model.trigger('sync', model, resp, options);
424
- }
425
- };
426
-
427
- options.error = Backbone.wrapError(options.error, model, options);
428
- var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
429
- if (!options.wait) triggerDestroy();
440
+ var xhr = this.sync('delete', this, options);
441
+ if (!options.wait) destroy();
430
442
  return xhr;
431
443
  },
432
444
 
@@ -434,14 +446,14 @@
434
446
  // using Backbone's restful methods, override this to change the endpoint
435
447
  // that will be called.
436
448
  url: function() {
437
- var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
449
+ var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
438
450
  if (this.isNew()) return base;
439
- return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
451
+ return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
440
452
  },
441
453
 
442
454
  // **parse** converts a response into the hash of attributes to be `set` on
443
455
  // the model. The default implementation is just to pass the response along.
444
- parse: function(resp, xhr) {
456
+ parse: function(resp) {
445
457
  return resp;
446
458
  },
447
459
 
@@ -459,30 +471,24 @@
459
471
  // a `"change:attribute"` event for each changed attribute.
460
472
  // Calling this will cause all objects observing the model to update.
461
473
  change: function(options) {
462
- options || (options = {});
463
474
  var changing = this._changing;
464
475
  this._changing = true;
465
476
 
466
- // Silent changes become pending changes.
467
- for (var attr in this._silent) this._pending[attr] = true;
477
+ // Generate the changes to be triggered on the model.
478
+ var triggers = this._computeChanges(true);
468
479
 
469
- // Silent changes are triggered.
470
- var changes = _.extend({}, options.changes, this._silent);
471
- this._silent = {};
472
- for (var attr in changes) {
473
- this.trigger('change:' + attr, this, this.get(attr), options);
480
+ this._pending = !!triggers.length;
481
+
482
+ for (var i = triggers.length - 2; i >= 0; i -= 2) {
483
+ this.trigger('change:' + triggers[i], this, triggers[i + 1], options);
474
484
  }
485
+
475
486
  if (changing) return this;
476
487
 
477
- // Continue firing `"change"` events while there are pending changes.
478
- while (!_.isEmpty(this._pending)) {
479
- this._pending = {};
488
+ // Trigger a `change` while there have been changes.
489
+ while (this._pending) {
490
+ this._pending = false;
480
491
  this.trigger('change', this, options);
481
- // Pending and silent changes still remain.
482
- for (var attr in this.changed) {
483
- if (this._pending[attr] || this._silent[attr]) continue;
484
- delete this.changed[attr];
485
- }
486
492
  this._previousAttributes = _.clone(this.attributes);
487
493
  }
488
494
 
@@ -493,7 +499,8 @@
493
499
  // Determine if the model has changed since the last `"change"` event.
494
500
  // If you specify an attribute name, determine if that attribute has changed.
495
501
  hasChanged: function(attr) {
496
- if (!arguments.length) return !_.isEmpty(this.changed);
502
+ if (!this._hasComputed) this._computeChanges();
503
+ if (attr == null) return !_.isEmpty(this.changed);
497
504
  return _.has(this.changed, attr);
498
505
  },
499
506
 
@@ -513,10 +520,43 @@
513
520
  return changed;
514
521
  },
515
522
 
523
+ // Looking at the built up list of `set` attribute changes, compute how
524
+ // many of the attributes have actually changed. If `loud`, return a
525
+ // boiled-down list of only the real changes.
526
+ _computeChanges: function(loud) {
527
+ this.changed = {};
528
+ var already = {};
529
+ var triggers = [];
530
+ var current = this._currentAttributes;
531
+ var changes = this._changes;
532
+
533
+ // Loop through the current queue of potential model changes.
534
+ for (var i = changes.length - 2; i >= 0; i -= 2) {
535
+ var key = changes[i], val = changes[i + 1];
536
+ if (already[key]) continue;
537
+ already[key] = true;
538
+
539
+ // Check if the attribute has been modified since the last change,
540
+ // and update `this.changed` accordingly. If we're inside of a `change`
541
+ // call, also add a trigger to the list.
542
+ if (current[key] !== val) {
543
+ this.changed[key] = val;
544
+ if (!loud) continue;
545
+ triggers.push(key, val);
546
+ current[key] = val;
547
+ }
548
+ }
549
+ if (loud) this._changes = [];
550
+
551
+ // Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`.
552
+ this._hasComputed = true;
553
+ return triggers;
554
+ },
555
+
516
556
  // Get the previous value of an attribute, recorded at the time the last
517
557
  // `"change"` event was fired.
518
558
  previous: function(attr) {
519
- if (!arguments.length || !this._previousAttributes) return null;
559
+ if (attr == null || !this._previousAttributes) return null;
520
560
  return this._previousAttributes[attr];
521
561
  },
522
562
 
@@ -526,25 +566,16 @@
526
566
  return _.clone(this._previousAttributes);
527
567
  },
528
568
 
529
- // Check if the model is currently in a valid state. It's only possible to
530
- // get into an *invalid* state if you're using silent changes.
531
- isValid: function() {
532
- return !this.validate(this.attributes);
533
- },
534
-
535
569
  // Run validation against the next complete set of model attributes,
536
570
  // returning `true` if all is well. If a specific `error` callback has
537
571
  // been passed, call that instead of firing the general `"error"` event.
538
572
  _validate: function(attrs, options) {
539
- if (options.silent || !this.validate) return true;
573
+ if (!this.validate) return true;
540
574
  attrs = _.extend({}, this.attributes, attrs);
541
575
  var error = this.validate(attrs, options);
542
576
  if (!error) return true;
543
- if (options && options.error) {
544
- options.error(this, error, options);
545
- } else {
546
- this.trigger('error', this, error, options);
547
- }
577
+ if (options && options.error) options.error(this, error, options);
578
+ this.trigger('error', this, error, options);
548
579
  return false;
549
580
  }
550
581
 
@@ -559,10 +590,10 @@
559
590
  var Collection = Backbone.Collection = function(models, options) {
560
591
  options || (options = {});
561
592
  if (options.model) this.model = options.model;
562
- if (options.comparator) this.comparator = options.comparator;
593
+ if (options.comparator !== void 0) this.comparator = options.comparator;
563
594
  this._reset();
564
595
  this.initialize.apply(this, arguments);
565
- if (models) this.reset(models, {silent: true, parse: options.parse});
596
+ if (models) this.reset(models, _.extend({silent: true}, options));
566
597
  };
567
598
 
568
599
  // Define the Collection's inheritable methods.
@@ -582,54 +613,65 @@
582
613
  return this.map(function(model){ return model.toJSON(options); });
583
614
  },
584
615
 
616
+ // Proxy `Backbone.sync` by default.
617
+ sync: function() {
618
+ return Backbone.sync.apply(this, arguments);
619
+ },
620
+
585
621
  // Add a model, or list of models to the set. Pass **silent** to avoid
586
622
  // firing the `add` event for every new model.
587
623
  add: function(models, options) {
588
- var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
589
- options || (options = {});
624
+ var i, args, length, model, existing, needsSort;
625
+ var at = options && options.at;
626
+ var sort = ((options && options.sort) == null ? true : options.sort);
590
627
  models = _.isArray(models) ? models.slice() : [models];
591
628
 
592
- // Begin by turning bare objects into model references, and preventing
593
- // invalid models or duplicate models from being added.
594
- for (i = 0, length = models.length; i < length; i++) {
595
- if (!(model = models[i] = this._prepareModel(models[i], options))) {
596
- throw new Error("Can't add an invalid model to a collection");
629
+ // Turn bare objects into model references, and prevent invalid models
630
+ // from being added.
631
+ for (i = models.length - 1; i >= 0; i--) {
632
+ if(!(model = this._prepareModel(models[i], options))) {
633
+ this.trigger("error", this, models[i], options);
634
+ models.splice(i, 1);
635
+ continue;
597
636
  }
598
- cid = model.cid;
599
- id = model.id;
600
- if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
601
- dups.push(i);
637
+ models[i] = model;
638
+
639
+ existing = model.id != null && this._byId[model.id];
640
+ // If a duplicate is found, prevent it from being added and
641
+ // optionally merge it into the existing model.
642
+ if (existing || this._byCid[model.cid]) {
643
+ if (options && options.merge && existing) {
644
+ existing.set(model.attributes, options);
645
+ needsSort = sort;
646
+ }
647
+ models.splice(i, 1);
602
648
  continue;
603
649
  }
604
- cids[cid] = ids[id] = model;
605
- }
606
650
 
607
- // Remove duplicates.
608
- i = dups.length;
609
- while (i--) {
610
- models.splice(dups[i], 1);
611
- }
612
-
613
- // Listen to added models' events, and index models for lookup by
614
- // `id` and by `cid`.
615
- for (i = 0, length = models.length; i < length; i++) {
616
- (model = models[i]).on('all', this._onModelEvent, this);
651
+ // Listen to added models' events, and index models for lookup by
652
+ // `id` and by `cid`.
653
+ model.on('all', this._onModelEvent, this);
617
654
  this._byCid[model.cid] = model;
618
655
  if (model.id != null) this._byId[model.id] = model;
619
656
  }
620
657
 
621
- // Insert models into the collection, re-sorting if needed, and triggering
622
- // `add` events unless silenced.
623
- this.length += length;
624
- index = options.at != null ? options.at : this.models.length;
625
- splice.apply(this.models, [index, 0].concat(models));
626
- if (this.comparator) this.sort({silent: true});
627
- if (options.silent) return this;
628
- for (i = 0, length = this.models.length; i < length; i++) {
629
- if (!cids[(model = this.models[i]).cid]) continue;
630
- options.index = i;
658
+ // See if sorting is needed, update `length` and splice in new models.
659
+ if (models.length) needsSort = sort;
660
+ this.length += models.length;
661
+ args = [at != null ? at : this.models.length, 0];
662
+ push.apply(args, models);
663
+ splice.apply(this.models, args);
664
+
665
+ // Sort the collection if appropriate.
666
+ if (needsSort && this.comparator && at == null) this.sort({silent: true});
667
+
668
+ if (options && options.silent) return this;
669
+
670
+ // Trigger `add` events.
671
+ while (model = models.shift()) {
631
672
  model.trigger('add', model, this, options);
632
673
  }
674
+
633
675
  return this;
634
676
  },
635
677
 
@@ -640,7 +682,7 @@
640
682
  options || (options = {});
641
683
  models = _.isArray(models) ? models.slice() : [models];
642
684
  for (i = 0, l = models.length; i < l; i++) {
643
- model = this.getByCid(models[i]) || this.get(models[i]);
685
+ model = this.get(models[i]);
644
686
  if (!model) continue;
645
687
  delete this._byId[model.id];
646
688
  delete this._byCid[model.cid];
@@ -659,7 +701,7 @@
659
701
  // Add a model to the end of the collection.
660
702
  push: function(model, options) {
661
703
  model = this._prepareModel(model, options);
662
- this.add(model, options);
704
+ this.add(model, _.extend({at: this.length}, options));
663
705
  return model;
664
706
  },
665
707
 
@@ -684,15 +726,15 @@
684
726
  return model;
685
727
  },
686
728
 
687
- // Get a model from the set by id.
688
- get: function(id) {
689
- if (id == null) return void 0;
690
- return this._byId[id.id != null ? id.id : id];
729
+ // Slice out a sub-array of models from the collection.
730
+ slice: function(begin, end) {
731
+ return this.models.slice(begin, end);
691
732
  },
692
733
 
693
- // Get a model from the set by client id.
694
- getByCid: function(cid) {
695
- return cid && this._byCid[cid.cid || cid];
734
+ // Get a model from the set by id.
735
+ get: function(obj) {
736
+ if (obj == null) return void 0;
737
+ return this._byId[obj.id != null ? obj.id : obj] || this._byCid[obj.cid || obj];
696
738
  },
697
739
 
698
740
  // Get the model at the given index.
@@ -715,34 +757,74 @@
715
757
  // normal circumstances, as the set will maintain sort order as each item
716
758
  // is added.
717
759
  sort: function(options) {
718
- options || (options = {});
719
- if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
720
- var boundComparator = _.bind(this.comparator, this);
721
- if (this.comparator.length == 1) {
722
- this.models = this.sortBy(boundComparator);
760
+ if (!this.comparator) {
761
+ throw new Error('Cannot sort a set without a comparator');
762
+ }
763
+
764
+ if (_.isString(this.comparator) || this.comparator.length === 1) {
765
+ this.models = this.sortBy(this.comparator, this);
723
766
  } else {
724
- this.models.sort(boundComparator);
767
+ this.models.sort(_.bind(this.comparator, this));
725
768
  }
726
- if (!options.silent) this.trigger('reset', this, options);
769
+
770
+ if (!options || !options.silent) this.trigger('sort', this, options);
727
771
  return this;
728
772
  },
729
773
 
730
774
  // Pluck an attribute from each model in the collection.
731
775
  pluck: function(attr) {
732
- return _.map(this.models, function(model){ return model.get(attr); });
776
+ return _.invoke(this.models, 'get', attr);
777
+ },
778
+
779
+ // Smartly update a collection with a change set of models, adding,
780
+ // removing, and merging as necessary.
781
+ update: function(models, options) {
782
+ var model, i, l, existing;
783
+ var add = [], remove = [], modelMap = {};
784
+ var idAttr = this.model.prototype.idAttribute;
785
+ options = _.extend({add: true, merge: true, remove: true}, options);
786
+ if (options.parse) models = this.parse(models);
787
+
788
+ // Allow a single model (or no argument) to be passed.
789
+ if (!_.isArray(models)) models = models ? [models] : [];
790
+
791
+ // Proxy to `add` for this case, no need to iterate...
792
+ if (options.add && !options.remove) return this.add(models, options);
793
+
794
+ // Determine which models to add and merge, and which to remove.
795
+ for (i = 0, l = models.length; i < l; i++) {
796
+ model = models[i];
797
+ existing = this.get(model.id || model.cid || model[idAttr]);
798
+ if (options.remove && existing) modelMap[existing.cid] = true;
799
+ if ((options.add && !existing) || (options.merge && existing)) {
800
+ add.push(model);
801
+ }
802
+ }
803
+ if (options.remove) {
804
+ for (i = 0, l = this.models.length; i < l; i++) {
805
+ model = this.models[i];
806
+ if (!modelMap[model.cid]) remove.push(model);
807
+ }
808
+ }
809
+
810
+ // Remove models (if applicable) before we add and merge the rest.
811
+ if (remove.length) this.remove(remove, options);
812
+ if (add.length) this.add(add, options);
813
+ return this;
733
814
  },
734
815
 
735
816
  // When you have more items than you want to add or remove individually,
736
817
  // you can reset the entire set with a new list of models, without firing
737
818
  // any `add` or `remove` events. Fires `reset` when finished.
738
819
  reset: function(models, options) {
739
- models || (models = []);
740
820
  options || (options = {});
821
+ if (options.parse) models = this.parse(models);
741
822
  for (var i = 0, l = this.models.length; i < l; i++) {
742
823
  this._removeReference(this.models[i]);
743
824
  }
825
+ options.previousModels = this.models;
744
826
  this._reset();
745
- this.add(models, _.extend({silent: true}, options));
827
+ if (models) this.add(models, _.extend({silent: true}, options));
746
828
  if (!options.silent) this.trigger('reset', this, options);
747
829
  return this;
748
830
  },
@@ -752,34 +834,30 @@
752
834
  // models to the collection instead of resetting.
753
835
  fetch: function(options) {
754
836
  options = options ? _.clone(options) : {};
755
- if (options.parse === undefined) options.parse = true;
837
+ if (options.parse === void 0) options.parse = true;
756
838
  var collection = this;
757
839
  var success = options.success;
758
840
  options.success = function(resp, status, xhr) {
759
- collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
760
- if (success) success(collection, resp);
841
+ var method = options.update ? 'update' : 'reset';
842
+ collection[method](resp, options);
843
+ if (success) success(collection, resp, options);
761
844
  };
762
- options.error = Backbone.wrapError(options.error, collection, options);
763
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
845
+ return this.sync('read', this, options);
764
846
  },
765
847
 
766
848
  // Create a new instance of a model in this collection. Add the model to the
767
849
  // collection immediately, unless `wait: true` is passed, in which case we
768
850
  // wait for the server to agree.
769
851
  create: function(model, options) {
770
- var coll = this;
852
+ var collection = this;
771
853
  options = options ? _.clone(options) : {};
772
854
  model = this._prepareModel(model, options);
773
855
  if (!model) return false;
774
- if (!options.wait) coll.add(model, options);
856
+ if (!options.wait) collection.add(model, options);
775
857
  var success = options.success;
776
- options.success = function(nextModel, resp, xhr) {
777
- if (options.wait) coll.add(nextModel, options);
778
- if (success) {
779
- success(nextModel, resp);
780
- } else {
781
- nextModel.trigger('sync', model, resp, options);
782
- }
858
+ options.success = function(model, resp, options) {
859
+ if (options.wait) collection.add(model, options);
860
+ if (success) success(model, resp, options);
783
861
  };
784
862
  model.save(null, options);
785
863
  return model;
@@ -787,19 +865,24 @@
787
865
 
788
866
  // **parse** converts a response into a list of models to be added to the
789
867
  // collection. The default implementation is just to pass it through.
790
- parse: function(resp, xhr) {
868
+ parse: function(resp) {
791
869
  return resp;
792
870
  },
793
871
 
872
+ // Create a new collection with an identical list of models as this one.
873
+ clone: function() {
874
+ return new this.constructor(this.models);
875
+ },
876
+
794
877
  // Proxy to _'s chain. Can't be proxied the same way the rest of the
795
878
  // underscore methods are proxied because it relies on the underscore
796
879
  // constructor.
797
- chain: function () {
880
+ chain: function() {
798
881
  return _(this.models).chain();
799
882
  },
800
883
 
801
884
  // Reset all internal state. Called when the collection is reset.
802
- _reset: function(options) {
885
+ _reset: function() {
803
886
  this.length = 0;
804
887
  this.models = [];
805
888
  this._byId = {};
@@ -807,24 +890,21 @@
807
890
  },
808
891
 
809
892
  // Prepare a model or hash of attributes to be added to this collection.
810
- _prepareModel: function(model, options) {
811
- options || (options = {});
812
- if (!(model instanceof Model)) {
813
- var attrs = model;
814
- options.collection = this;
815
- model = new this.model(attrs, options);
816
- if (!model._validate(model.attributes, options)) model = false;
817
- } else if (!model.collection) {
818
- model.collection = this;
893
+ _prepareModel: function(attrs, options) {
894
+ if (attrs instanceof Model) {
895
+ if (!attrs.collection) attrs.collection = this;
896
+ return attrs;
819
897
  }
898
+ options || (options = {});
899
+ options.collection = this;
900
+ var model = new this.model(attrs, options);
901
+ if (!model._validate(attrs, options)) return false;
820
902
  return model;
821
903
  },
822
904
 
823
905
  // Internal method to remove a model's ties to a collection.
824
906
  _removeReference: function(model) {
825
- if (this == model.collection) {
826
- delete model.collection;
827
- }
907
+ if (this === model.collection) delete model.collection;
828
908
  model.off('all', this._onModelEvent, this);
829
909
  },
830
910
 
@@ -833,13 +913,11 @@
833
913
  // events simply proxy through. "add" and "remove" events that originate
834
914
  // in other collections are ignored.
835
915
  _onModelEvent: function(event, model, collection, options) {
836
- if ((event == 'add' || event == 'remove') && collection != this) return;
837
- if (event == 'destroy') {
838
- this.remove(model, options);
839
- }
916
+ if ((event === 'add' || event === 'remove') && collection !== this) return;
917
+ if (event === 'destroy') this.remove(model, options);
840
918
  if (model && event === 'change:' + model.idAttribute) {
841
919
  delete this._byId[model.previous(model.idAttribute)];
842
- this._byId[model.id] = model;
920
+ if (model.id != null) this._byId[model.id] = model;
843
921
  }
844
922
  this.trigger.apply(this, arguments);
845
923
  }
@@ -847,21 +925,37 @@
847
925
  });
848
926
 
849
927
  // Underscore methods that we want to implement on the Collection.
850
- var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
851
- 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
852
- 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
853
- 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
854
- 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
928
+ var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
929
+ 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
930
+ 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
931
+ 'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take',
932
+ 'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle',
933
+ 'lastIndexOf', 'isEmpty'];
855
934
 
856
935
  // Mix in each Underscore method as a proxy to `Collection#models`.
857
936
  _.each(methods, function(method) {
858
937
  Collection.prototype[method] = function() {
859
- return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
938
+ var args = slice.call(arguments);
939
+ args.unshift(this.models);
940
+ return _[method].apply(_, args);
941
+ };
942
+ });
943
+
944
+ // Underscore methods that take a property name as an argument.
945
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
946
+
947
+ // Use attributes instead of properties.
948
+ _.each(attributeMethods, function(method) {
949
+ Collection.prototype[method] = function(value, context) {
950
+ var iterator = _.isFunction(value) ? value : function(model) {
951
+ return model.get(value);
952
+ };
953
+ return _[method](this.models, iterator, context);
860
954
  };
861
955
  });
862
956
 
863
957
  // Backbone.Router
864
- // -------------------
958
+ // ---------------
865
959
 
866
960
  // Routers map faux-URLs to actions, and fire events when routes are
867
961
  // matched. Creating a new one sets its `routes` hash, if not set statically.
@@ -874,9 +968,10 @@
874
968
 
875
969
  // Cached regular expressions for matching named param parts and splatted
876
970
  // parts of route strings.
971
+ var optionalParam = /\((.*?)\)/g;
877
972
  var namedParam = /:\w+/g;
878
973
  var splatParam = /\*\w+/g;
879
- var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
974
+ var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
880
975
 
881
976
  // Set up all inheritable **Backbone.Router** properties and methods.
882
977
  _.extend(Router.prototype, Events, {
@@ -892,7 +987,6 @@
892
987
  // });
893
988
  //
894
989
  route: function(route, name, callback) {
895
- Backbone.history || (Backbone.history = new History);
896
990
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
897
991
  if (!callback) callback = this[name];
898
992
  Backbone.history.route(route, _.bind(function(fragment) {
@@ -907,6 +1001,7 @@
907
1001
  // Simple proxy to `Backbone.history` to save a fragment into the history.
908
1002
  navigate: function(fragment, options) {
909
1003
  Backbone.history.navigate(fragment, options);
1004
+ return this;
910
1005
  },
911
1006
 
912
1007
  // Bind all defined routes to `Backbone.history`. We have to reverse the
@@ -914,12 +1009,9 @@
914
1009
  // routes can be defined at the bottom of the route map.
915
1010
  _bindRoutes: function() {
916
1011
  if (!this.routes) return;
917
- var routes = [];
918
- for (var route in this.routes) {
919
- routes.unshift([route, this.routes[route]]);
920
- }
921
- for (var i = 0, l = routes.length; i < l; i++) {
922
- this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
1012
+ var route, routes = _.keys(this.routes);
1013
+ while ((route = routes.pop()) != null) {
1014
+ this.route(route, this.routes[route]);
923
1015
  }
924
1016
  },
925
1017
 
@@ -927,6 +1019,7 @@
927
1019
  // against the current location hash.
928
1020
  _routeToRegExp: function(route) {
929
1021
  route = route.replace(escapeRegExp, '\\$&')
1022
+ .replace(optionalParam, '(?:$1)?')
930
1023
  .replace(namedParam, '([^\/]+)')
931
1024
  .replace(splatParam, '(.*?)');
932
1025
  return new RegExp('^' + route + '$');
@@ -948,14 +1041,26 @@
948
1041
  var History = Backbone.History = function() {
949
1042
  this.handlers = [];
950
1043
  _.bindAll(this, 'checkUrl');
1044
+
1045
+ // #1653 - Ensure that `History` can be used outside of the browser.
1046
+ if (typeof window !== 'undefined') {
1047
+ this.location = window.location;
1048
+ this.history = window.history;
1049
+ }
951
1050
  };
952
1051
 
953
- // Cached regex for cleaning leading hashes and slashes .
954
- var routeStripper = /^[#\/]/;
1052
+ // Cached regex for stripping a leading hash/slash and trailing space.
1053
+ var routeStripper = /^[#\/]|\s+$/g;
1054
+
1055
+ // Cached regex for stripping leading and trailing slashes.
1056
+ var rootStripper = /^\/+|\/+$/g;
955
1057
 
956
1058
  // Cached regex for detecting MSIE.
957
1059
  var isExplorer = /msie [\w.]+/;
958
1060
 
1061
+ // Cached regex for removing a trailing slash.
1062
+ var trailingSlash = /\/$/;
1063
+
959
1064
  // Has the history handling already been started?
960
1065
  History.started = false;
961
1066
 
@@ -968,9 +1073,8 @@
968
1073
 
969
1074
  // Gets the true hash value. Cannot use location.hash directly due to bug
970
1075
  // in Firefox where location.hash will always be decoded.
971
- getHash: function(windowOverride) {
972
- var loc = windowOverride ? windowOverride.location : window.location;
973
- var match = loc.href.match(/#(.*)$/);
1076
+ getHash: function(window) {
1077
+ var match = (window || this).location.href.match(/#(.*)$/);
974
1078
  return match ? match[1] : '';
975
1079
  },
976
1080
 
@@ -978,15 +1082,14 @@
978
1082
  // the hash, or the override.
979
1083
  getFragment: function(fragment, forcePushState) {
980
1084
  if (fragment == null) {
981
- if (this._hasPushState || forcePushState) {
982
- fragment = window.location.pathname;
983
- var search = window.location.search;
984
- if (search) fragment += search;
1085
+ if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1086
+ fragment = this.location.pathname;
1087
+ var root = this.root.replace(trailingSlash, '');
1088
+ if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
985
1089
  } else {
986
1090
  fragment = this.getHash();
987
1091
  }
988
1092
  }
989
- if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
990
1093
  return fragment.replace(routeStripper, '');
991
1094
  },
992
1095
 
@@ -999,24 +1102,28 @@
999
1102
  // Figure out the initial configuration. Do we need an iframe?
1000
1103
  // Is pushState desired ... is it available?
1001
1104
  this.options = _.extend({}, {root: '/'}, this.options, options);
1105
+ this.root = this.options.root;
1002
1106
  this._wantsHashChange = this.options.hashChange !== false;
1003
1107
  this._wantsPushState = !!this.options.pushState;
1004
- this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
1108
+ this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1005
1109
  var fragment = this.getFragment();
1006
1110
  var docMode = document.documentMode;
1007
1111
  var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1008
1112
 
1009
- if (oldIE) {
1010
- this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1113
+ // Normalize root to always include a leading and trailing slash.
1114
+ this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1115
+
1116
+ if (oldIE && this._wantsHashChange) {
1117
+ this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1011
1118
  this.navigate(fragment);
1012
1119
  }
1013
1120
 
1014
1121
  // Depending on whether we're using pushState or hashes, and whether
1015
1122
  // 'onhashchange' is supported, determine how we check the URL state.
1016
1123
  if (this._hasPushState) {
1017
- $(window).bind('popstate', this.checkUrl);
1124
+ Backbone.$(window).bind('popstate', this.checkUrl);
1018
1125
  } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1019
- $(window).bind('hashchange', this.checkUrl);
1126
+ Backbone.$(window).bind('hashchange', this.checkUrl);
1020
1127
  } else if (this._wantsHashChange) {
1021
1128
  this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1022
1129
  }
@@ -1024,14 +1131,14 @@
1024
1131
  // Determine if we need to change the base url, for a pushState link
1025
1132
  // opened by a non-pushState browser.
1026
1133
  this.fragment = fragment;
1027
- var loc = window.location;
1028
- var atRoot = loc.pathname == this.options.root;
1134
+ var loc = this.location;
1135
+ var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1029
1136
 
1030
1137
  // If we've started off with a route from a `pushState`-enabled browser,
1031
1138
  // but we're currently in a browser that doesn't support it...
1032
1139
  if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1033
1140
  this.fragment = this.getFragment(null, true);
1034
- window.location.replace(this.options.root + '#' + this.fragment);
1141
+ this.location.replace(this.root + this.location.search + '#' + this.fragment);
1035
1142
  // Return immediately as browser will do redirect to new url
1036
1143
  return true;
1037
1144
 
@@ -1039,18 +1146,16 @@
1039
1146
  // in a browser where it could be `pushState`-based instead...
1040
1147
  } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1041
1148
  this.fragment = this.getHash().replace(routeStripper, '');
1042
- window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
1149
+ this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1043
1150
  }
1044
1151
 
1045
- if (!this.options.silent) {
1046
- return this.loadUrl();
1047
- }
1152
+ if (!this.options.silent) return this.loadUrl();
1048
1153
  },
1049
1154
 
1050
1155
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1051
1156
  // but possibly useful for unit testing Routers.
1052
1157
  stop: function() {
1053
- $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
1158
+ Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
1054
1159
  clearInterval(this._checkUrlInterval);
1055
1160
  History.started = false;
1056
1161
  },
@@ -1065,8 +1170,10 @@
1065
1170
  // calls `loadUrl`, normalizing across the hidden iframe.
1066
1171
  checkUrl: function(e) {
1067
1172
  var current = this.getFragment();
1068
- if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
1069
- if (current == this.fragment) return false;
1173
+ if (current === this.fragment && this.iframe) {
1174
+ current = this.getFragment(this.getHash(this.iframe));
1175
+ }
1176
+ if (current === this.fragment) return false;
1070
1177
  if (this.iframe) this.navigate(current);
1071
1178
  this.loadUrl() || this.loadUrl(this.getHash());
1072
1179
  },
@@ -1095,31 +1202,31 @@
1095
1202
  navigate: function(fragment, options) {
1096
1203
  if (!History.started) return false;
1097
1204
  if (!options || options === true) options = {trigger: options};
1098
- var frag = (fragment || '').replace(routeStripper, '');
1099
- if (this.fragment == frag) return;
1205
+ fragment = this.getFragment(fragment || '');
1206
+ if (this.fragment === fragment) return;
1207
+ this.fragment = fragment;
1208
+ var url = this.root + fragment;
1100
1209
 
1101
1210
  // If pushState is available, we use it to set the fragment as a real URL.
1102
1211
  if (this._hasPushState) {
1103
- if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
1104
- this.fragment = frag;
1105
- window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
1212
+ this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1106
1213
 
1107
1214
  // If hash changes haven't been explicitly disabled, update the hash
1108
1215
  // fragment to store history.
1109
1216
  } else if (this._wantsHashChange) {
1110
- this.fragment = frag;
1111
- this._updateHash(window.location, frag, options.replace);
1112
- if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
1113
- // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
1114
- // When replace is true, we don't want this.
1217
+ this._updateHash(this.location, fragment, options.replace);
1218
+ if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1219
+ // Opening and closing the iframe tricks IE7 and earlier to push a
1220
+ // history entry on hash-tag change. When replace is true, we don't
1221
+ // want this.
1115
1222
  if(!options.replace) this.iframe.document.open().close();
1116
- this._updateHash(this.iframe.location, frag, options.replace);
1223
+ this._updateHash(this.iframe.location, fragment, options.replace);
1117
1224
  }
1118
1225
 
1119
1226
  // If you've told us that you explicitly don't want fallback hashchange-
1120
1227
  // based history, then `navigate` becomes a page refresh.
1121
1228
  } else {
1122
- window.location.assign(this.options.root + fragment);
1229
+ return this.location.assign(url);
1123
1230
  }
1124
1231
  if (options.trigger) this.loadUrl(fragment);
1125
1232
  },
@@ -1128,13 +1235,19 @@
1128
1235
  // a new one to the browser history.
1129
1236
  _updateHash: function(location, fragment, replace) {
1130
1237
  if (replace) {
1131
- location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
1238
+ var href = location.href.replace(/(javascript:|#).*$/, '');
1239
+ location.replace(href + '#' + fragment);
1132
1240
  } else {
1133
- location.hash = fragment;
1241
+ // #1649 - Some browsers require that `hash` contains a leading #.
1242
+ location.hash = '#' + fragment;
1134
1243
  }
1135
1244
  }
1245
+
1136
1246
  });
1137
1247
 
1248
+ // Create the default Backbone.history.
1249
+ Backbone.history = new History;
1250
+
1138
1251
  // Backbone.View
1139
1252
  // -------------
1140
1253
 
@@ -1152,7 +1265,7 @@
1152
1265
  var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1153
1266
 
1154
1267
  // List of view options to be merged as properties.
1155
- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
1268
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1156
1269
 
1157
1270
  // Set up all inheritable **Backbone.View** properties and methods.
1158
1271
  _.extend(View.prototype, Events, {
@@ -1177,10 +1290,11 @@
1177
1290
  return this;
1178
1291
  },
1179
1292
 
1180
- // Remove this view from the DOM. Note that the view isn't present in the
1181
- // DOM by default, so calling this method may be a no-op.
1293
+ // Remove this view by taking the element out of the DOM, and removing any
1294
+ // applicable Backbone.Events listeners.
1182
1295
  remove: function() {
1183
1296
  this.$el.remove();
1297
+ this.stopListening();
1184
1298
  return this;
1185
1299
  },
1186
1300
 
@@ -1191,8 +1305,8 @@
1191
1305
  //
1192
1306
  make: function(tagName, attributes, content) {
1193
1307
  var el = document.createElement(tagName);
1194
- if (attributes) $(el).attr(attributes);
1195
- if (content) $(el).html(content);
1308
+ if (attributes) Backbone.$(el).attr(attributes);
1309
+ if (content != null) Backbone.$(el).html(content);
1196
1310
  return el;
1197
1311
  },
1198
1312
 
@@ -1200,7 +1314,7 @@
1200
1314
  // re-delegation.
1201
1315
  setElement: function(element, delegate) {
1202
1316
  if (this.$el) this.undelegateEvents();
1203
- this.$el = (element instanceof $) ? element : $(element);
1317
+ this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1204
1318
  this.el = this.$el[0];
1205
1319
  if (delegate !== false) this.delegateEvents();
1206
1320
  return this;
@@ -1222,7 +1336,7 @@
1222
1336
  // This only works for delegate-able events: not `focus`, `blur`, and
1223
1337
  // not `change`, `submit`, and `reset` in Internet Explorer.
1224
1338
  delegateEvents: function(events) {
1225
- if (!(events || (events = getValue(this, 'events')))) return;
1339
+ if (!(events || (events = _.result(this, 'events')))) return;
1226
1340
  this.undelegateEvents();
1227
1341
  for (var key in events) {
1228
1342
  var method = events[key];
@@ -1251,11 +1365,8 @@
1251
1365
  // Keys with special meaning *(model, collection, id, className)*, are
1252
1366
  // attached directly to the view.
1253
1367
  _configure: function(options) {
1254
- if (this.options) options = _.extend({}, this.options, options);
1255
- for (var i = 0, l = viewOptions.length; i < l; i++) {
1256
- var attr = viewOptions[i];
1257
- if (options[attr]) this[attr] = options[attr];
1258
- }
1368
+ if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1369
+ _.extend(this, _.pick(options, viewOptions));
1259
1370
  this.options = options;
1260
1371
  },
1261
1372
 
@@ -1265,27 +1376,17 @@
1265
1376
  // an element from the `id`, `className` and `tagName` properties.
1266
1377
  _ensureElement: function() {
1267
1378
  if (!this.el) {
1268
- var attrs = getValue(this, 'attributes') || {};
1269
- if (this.id) attrs.id = this.id;
1270
- if (this.className) attrs['class'] = this.className;
1271
- this.setElement(this.make(this.tagName, attrs), false);
1379
+ var attrs = _.extend({}, _.result(this, 'attributes'));
1380
+ if (this.id) attrs.id = _.result(this, 'id');
1381
+ if (this.className) attrs['class'] = _.result(this, 'className');
1382
+ this.setElement(this.make(_.result(this, 'tagName'), attrs), false);
1272
1383
  } else {
1273
- this.setElement(this.el, false);
1384
+ this.setElement(_.result(this, 'el'), false);
1274
1385
  }
1275
1386
  }
1276
1387
 
1277
1388
  });
1278
1389
 
1279
- // The self-propagating extend function that Backbone classes use.
1280
- var extend = function (protoProps, classProps) {
1281
- var child = inherits(this, protoProps, classProps);
1282
- child.extend = this.extend;
1283
- return child;
1284
- };
1285
-
1286
- // Set up inheritance for the model, collection, and view.
1287
- Model.extend = Collection.extend = Router.extend = View.extend = extend;
1288
-
1289
1390
  // Backbone.sync
1290
1391
  // -------------
1291
1392
 
@@ -1293,6 +1394,7 @@
1293
1394
  var methodMap = {
1294
1395
  'create': 'POST',
1295
1396
  'update': 'PUT',
1397
+ 'patch': 'PATCH',
1296
1398
  'delete': 'DELETE',
1297
1399
  'read': 'GET'
1298
1400
  };
@@ -1316,112 +1418,112 @@
1316
1418
  var type = methodMap[method];
1317
1419
 
1318
1420
  // Default options, unless specified.
1319
- options || (options = {});
1421
+ _.defaults(options || (options = {}), {
1422
+ emulateHTTP: Backbone.emulateHTTP,
1423
+ emulateJSON: Backbone.emulateJSON
1424
+ });
1320
1425
 
1321
1426
  // Default JSON-request options.
1322
1427
  var params = {type: type, dataType: 'json'};
1323
1428
 
1324
1429
  // Ensure that we have a URL.
1325
1430
  if (!options.url) {
1326
- params.url = getValue(model, 'url') || urlError();
1431
+ params.url = _.result(model, 'url') || urlError();
1327
1432
  }
1328
1433
 
1329
1434
  // Ensure that we have the appropriate request data.
1330
- if (!options.data && model && (method == 'create' || method == 'update')) {
1435
+ if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1331
1436
  params.contentType = 'application/json';
1332
- params.data = JSON.stringify(model.toJSON());
1437
+ params.data = JSON.stringify(options.attrs || model.toJSON(options));
1333
1438
  }
1334
1439
 
1335
1440
  // For older servers, emulate JSON by encoding the request into an HTML-form.
1336
- if (Backbone.emulateJSON) {
1441
+ if (options.emulateJSON) {
1337
1442
  params.contentType = 'application/x-www-form-urlencoded';
1338
1443
  params.data = params.data ? {model: params.data} : {};
1339
1444
  }
1340
1445
 
1341
1446
  // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1342
1447
  // And an `X-HTTP-Method-Override` header.
1343
- if (Backbone.emulateHTTP) {
1344
- if (type === 'PUT' || type === 'DELETE') {
1345
- if (Backbone.emulateJSON) params.data._method = type;
1346
- params.type = 'POST';
1347
- params.beforeSend = function(xhr) {
1348
- xhr.setRequestHeader('X-HTTP-Method-Override', type);
1349
- };
1350
- }
1448
+ if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1449
+ params.type = 'POST';
1450
+ if (options.emulateJSON) params.data._method = type;
1451
+ var beforeSend = options.beforeSend;
1452
+ options.beforeSend = function(xhr) {
1453
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
1454
+ if (beforeSend) return beforeSend.apply(this, arguments);
1455
+ };
1351
1456
  }
1352
1457
 
1353
1458
  // Don't process data on a non-GET request.
1354
- if (params.type !== 'GET' && !Backbone.emulateJSON) {
1459
+ if (params.type !== 'GET' && !options.emulateJSON) {
1355
1460
  params.processData = false;
1356
1461
  }
1357
1462
 
1463
+ var success = options.success;
1464
+ options.success = function(resp, status, xhr) {
1465
+ if (success) success(resp, status, xhr);
1466
+ model.trigger('sync', model, resp, options);
1467
+ };
1468
+
1469
+ var error = options.error;
1470
+ options.error = function(xhr, status, thrown) {
1471
+ if (error) error(model, xhr, options);
1472
+ model.trigger('error', model, xhr, options);
1473
+ };
1474
+
1358
1475
  // Make the request, allowing the user to override any Ajax options.
1359
- return $.ajax(_.extend(params, options));
1476
+ var xhr = Backbone.ajax(_.extend(params, options));
1477
+ model.trigger('request', model, xhr, options);
1478
+ return xhr;
1360
1479
  };
1361
1480
 
1362
- // Wrap an optional error callback with a fallback error event.
1363
- Backbone.wrapError = function(onError, originalModel, options) {
1364
- return function(model, resp) {
1365
- resp = model === originalModel ? resp : model;
1366
- if (onError) {
1367
- onError(originalModel, resp, options);
1368
- } else {
1369
- originalModel.trigger('error', originalModel, resp, options);
1370
- }
1371
- };
1481
+ // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1482
+ Backbone.ajax = function() {
1483
+ return Backbone.$.ajax.apply(Backbone.$, arguments);
1372
1484
  };
1373
1485
 
1374
1486
  // Helpers
1375
1487
  // -------
1376
1488
 
1377
- // Shared empty constructor function to aid in prototype-chain creation.
1378
- var ctor = function(){};
1379
-
1380
1489
  // Helper function to correctly set up the prototype chain, for subclasses.
1381
1490
  // Similar to `goog.inherits`, but uses a hash of prototype properties and
1382
1491
  // class properties to be extended.
1383
- var inherits = function(parent, protoProps, staticProps) {
1492
+ var extend = function(protoProps, staticProps) {
1493
+ var parent = this;
1384
1494
  var child;
1385
1495
 
1386
1496
  // The constructor function for the new subclass is either defined by you
1387
1497
  // (the "constructor" property in your `extend` definition), or defaulted
1388
1498
  // by us to simply call the parent's constructor.
1389
- if (protoProps && protoProps.hasOwnProperty('constructor')) {
1499
+ if (protoProps && _.has(protoProps, 'constructor')) {
1390
1500
  child = protoProps.constructor;
1391
1501
  } else {
1392
1502
  child = function(){ parent.apply(this, arguments); };
1393
1503
  }
1394
1504
 
1395
- // Inherit class (static) properties from parent.
1396
- _.extend(child, parent);
1505
+ // Add static properties to the constructor function, if supplied.
1506
+ _.extend(child, parent, staticProps);
1397
1507
 
1398
1508
  // Set the prototype chain to inherit from `parent`, without calling
1399
1509
  // `parent`'s constructor function.
1400
- ctor.prototype = parent.prototype;
1401
- child.prototype = new ctor();
1510
+ var Surrogate = function(){ this.constructor = child; };
1511
+ Surrogate.prototype = parent.prototype;
1512
+ child.prototype = new Surrogate;
1402
1513
 
1403
1514
  // Add prototype properties (instance properties) to the subclass,
1404
1515
  // if supplied.
1405
1516
  if (protoProps) _.extend(child.prototype, protoProps);
1406
1517
 
1407
- // Add static properties to the constructor function, if supplied.
1408
- if (staticProps) _.extend(child, staticProps);
1409
-
1410
- // Correctly set child's `prototype.constructor`.
1411
- child.prototype.constructor = child;
1412
-
1413
- // Set a convenience property in case the parent's prototype is needed later.
1518
+ // Set a convenience property in case the parent's prototype is needed
1519
+ // later.
1414
1520
  child.__super__ = parent.prototype;
1415
1521
 
1416
1522
  return child;
1417
1523
  };
1418
1524
 
1419
- // Helper function to get a value from a Backbone object as a property
1420
- // or as a function.
1421
- var getValue = function(object, prop) {
1422
- if (!(object && object[prop])) return null;
1423
- return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1424
- };
1525
+ // Set up inheritance for the model, collection, router, view and history.
1526
+ Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
1425
1527
 
1426
1528
  // Throw an error when a URL is needed, and none is supplied.
1427
1529
  var urlError = function() {