appetizer-ui 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.
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
10
10
  gem.test_files = `git ls-files -- test/*`.split("\n")
11
11
  gem.name = "appetizer-ui"
12
12
  gem.require_paths = ["lib"]
13
- gem.version = "0.8.0"
13
+ gem.version = "0.9.0"
14
14
 
15
15
  gem.required_ruby_version = ">= 1.9.2"
16
16
 
@@ -1,20 +1,27 @@
1
- // Backbone.js 0.5.3
2
- // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
1
+ // Backbone.js 0.9.1
2
+
3
+ // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
3
4
  // Backbone may be freely distributed under the MIT license.
4
5
  // For all details and documentation:
5
- // http://documentcloud.github.com/backbone
6
+ // http://backbonejs.org
6
7
 
7
8
  (function(){
8
9
 
9
10
  // Initial Setup
10
11
  // -------------
11
12
 
12
- // Save a reference to the global object.
13
+ // Save a reference to the global object (`window` in the browser, `global`
14
+ // on the server).
13
15
  var root = this;
14
16
 
15
- // Save the previous value of the `Backbone` variable.
17
+ // Save the previous value of the `Backbone` variable, so that it can be
18
+ // restored later on, if `noConflict` is used.
16
19
  var previousBackbone = root.Backbone;
17
20
 
21
+ // Create a local reference to slice/splice.
22
+ var slice = Array.prototype.slice;
23
+ var splice = Array.prototype.splice;
24
+
18
25
  // The top-level namespace. All public Backbone classes and modules will
19
26
  // be attached to this. Exported for both CommonJS and the browser.
20
27
  var Backbone;
@@ -25,14 +32,23 @@
25
32
  }
26
33
 
27
34
  // Current version of the library. Keep in sync with `package.json`.
28
- Backbone.VERSION = '0.5.3';
35
+ Backbone.VERSION = '0.9.1';
29
36
 
30
37
  // Require Underscore, if we're on the server, and it's not already present.
31
38
  var _ = root._;
32
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
33
-
34
- // For Backbone's purposes, jQuery or Zepto owns the `$` variable.
35
- var $ = root.jQuery || root.Zepto;
39
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
40
+
41
+ // 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
+ };
36
52
 
37
53
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
38
54
  // to its previous owner. Returns a reference to this Backbone object.
@@ -41,9 +57,9 @@
41
57
  return this;
42
58
  };
43
59
 
44
- // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
45
- // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
46
- // `X-Http-Method-Override` header.
60
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
61
+ // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
62
+ // set a `X-Http-Method-Override` header.
47
63
  Backbone.emulateHTTP = false;
48
64
 
49
65
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
@@ -56,43 +72,53 @@
56
72
  // -----------------
57
73
 
58
74
  // A module that can be mixed in to *any object* in order to provide it with
59
- // custom events. You may `bind` or `unbind` a callback function to an event;
60
- // `trigger`-ing an event fires all callbacks in succession.
75
+ // custom events. You may bind with `on` or remove with `off` callback functions
76
+ // to an event; trigger`-ing an event fires all callbacks in succession.
61
77
  //
62
78
  // var object = {};
63
79
  // _.extend(object, Backbone.Events);
64
- // object.bind('expand', function(){ alert('expanded'); });
80
+ // object.on('expand', function(){ alert('expanded'); });
65
81
  // object.trigger('expand');
66
82
  //
67
83
  Backbone.Events = {
68
84
 
69
- // Bind an event, specified by a string name, `ev`, to a `callback` function.
70
- // Passing `"all"` will bind the callback to all events fired.
71
- bind : function(ev, callback, context) {
85
+ // Bind an event, specified by a string name, `ev`, to a `callback`
86
+ // function. Passing `"all"` will bind the callback to all events fired.
87
+ on: function(events, callback, context) {
88
+ var ev;
89
+ events = events.split(/\s+/);
72
90
  var calls = this._callbacks || (this._callbacks = {});
73
- var list = calls[ev] || (calls[ev] = []);
74
- list.push([callback, context]);
91
+ while (ev = events.shift()) {
92
+ // Create an immutable callback list, allowing traversal during
93
+ // modification. The tail is an empty object that will always be used
94
+ // as the next node.
95
+ var list = calls[ev] || (calls[ev] = {});
96
+ var tail = list.tail || (list.tail = list.next = {});
97
+ tail.callback = callback;
98
+ tail.context = context;
99
+ list.tail = tail.next = {};
100
+ }
75
101
  return this;
76
102
  },
77
103
 
78
- // Remove one or many callbacks. If `callback` is null, removes all
79
- // callbacks for the event. If `ev` is null, removes all bound callbacks
80
- // for all events.
81
- unbind : function(ev, callback) {
82
- var calls;
83
- if (!ev) {
84
- this._callbacks = {};
104
+ // Remove one or many callbacks. If `context` is null, removes all callbacks
105
+ // with that function. If `callback` is null, removes all callbacks for the
106
+ // event. If `ev` is null, removes all bound callbacks for all events.
107
+ off: function(events, callback, context) {
108
+ var ev, calls, node;
109
+ if (!events) {
110
+ delete this._callbacks;
85
111
  } else if (calls = this._callbacks) {
86
- if (!callback) {
87
- calls[ev] = [];
88
- } else {
89
- var list = calls[ev];
90
- if (!list) return this;
91
- for (var i = 0, l = list.length; i < l; i++) {
92
- if (list[i] && callback === list[i][0]) {
93
- list[i] = null;
94
- break;
95
- }
112
+ events = events.split(/\s+/);
113
+ while (ev = events.shift()) {
114
+ node = calls[ev];
115
+ delete calls[ev];
116
+ if (!callback || !node) continue;
117
+ // Create a new list, omitting the indicated event/context pairs.
118
+ while ((node = node.next) && node.next) {
119
+ if (node.callback === callback &&
120
+ (!context || node.context === context)) continue;
121
+ this.on(ev, node.callback, node.context);
96
122
  }
97
123
  }
98
124
  }
@@ -102,21 +128,24 @@
102
128
  // Trigger an event, firing all bound callbacks. Callbacks are passed the
103
129
  // same arguments as `trigger` is, apart from the event name.
104
130
  // Listening for `"all"` passes the true event name as the first argument.
105
- trigger : function(eventName) {
106
- var list, calls, ev, callback, args;
107
- var both = 2;
131
+ trigger: function(events) {
132
+ var event, node, calls, tail, args, all, rest;
108
133
  if (!(calls = this._callbacks)) return this;
109
- while (both--) {
110
- ev = both ? eventName : 'all';
111
- if (list = calls[ev]) {
112
- for (var i = 0, l = list.length; i < l; i++) {
113
- if (!(callback = list[i])) {
114
- list.splice(i, 1); i--; l--;
115
- } else {
116
- args = both ? Array.prototype.slice.call(arguments, 1) : arguments;
117
- callback[0].apply(callback[1] || this, args);
118
- }
119
- }
134
+ all = calls['all'];
135
+ (events = events.split(/\s+/)).push(null);
136
+ // Save references to the current heads & tails.
137
+ while (event = events.shift()) {
138
+ if (all) events.push({next: all.next, tail: all.tail, event: event});
139
+ if (!(node = calls[event])) continue;
140
+ events.push({next: node.next, tail: node.tail});
141
+ }
142
+ // Traverse each list, stopping when the saved tail is reached.
143
+ rest = slice.call(arguments, 1);
144
+ while (node = events.pop()) {
145
+ tail = node.tail;
146
+ args = node.event ? [node.event].concat(rest) : rest;
147
+ while ((node = node.next) !== tail) {
148
+ node.callback.apply(node.context || this, args);
120
149
  }
121
150
  }
122
151
  return this;
@@ -124,6 +153,10 @@
124
153
 
125
154
  };
126
155
 
156
+ // Aliases for backwards compatibility.
157
+ Backbone.Events.bind = Backbone.Events.on;
158
+ Backbone.Events.unbind = Backbone.Events.off;
159
+
127
160
  // Backbone.Model
128
161
  // --------------
129
162
 
@@ -132,278 +165,306 @@
132
165
  Backbone.Model = function(attributes, options) {
133
166
  var defaults;
134
167
  attributes || (attributes = {});
135
- if (defaults = this.defaults) {
136
- if (_.isFunction(defaults)) defaults = defaults.call(this);
168
+ if (options && options.parse) attributes = this.parse(attributes);
169
+ if (defaults = getValue(this, 'defaults')) {
137
170
  attributes = _.extend({}, defaults, attributes);
138
171
  }
172
+ if (options && options.collection) this.collection = options.collection;
139
173
  this.attributes = {};
140
174
  this._escapedAttributes = {};
141
175
  this.cid = _.uniqueId('c');
142
- this.set(attributes, {silent : true});
143
- this._changed = false;
176
+ if (!this.set(attributes, {silent: true})) {
177
+ throw new Error("Can't create an invalid model");
178
+ }
179
+ delete this._changed;
144
180
  this._previousAttributes = _.clone(this.attributes);
145
- if (options && options.collection) this.collection = options.collection;
146
- this.initialize(attributes, options);
181
+ this.initialize.apply(this, arguments);
147
182
  };
148
183
 
149
184
  // Attach all inheritable methods to the Model prototype.
150
185
  _.extend(Backbone.Model.prototype, Backbone.Events, {
151
186
 
152
- // A snapshot of the model's previous attributes, taken immediately
153
- // after the last `"change"` event was fired.
154
- _previousAttributes : null,
155
-
156
- // Has the item been changed since the last `"change"` event?
157
- _changed : false,
158
-
159
187
  // The default name for the JSON `id` attribute is `"id"`. MongoDB and
160
188
  // CouchDB users may want to set this to `"_id"`.
161
- idAttribute : 'id',
189
+ idAttribute: 'id',
162
190
 
163
191
  // Initialize is an empty function by default. Override it with your own
164
192
  // initialization logic.
165
- initialize : function(){},
193
+ initialize: function(){},
166
194
 
167
195
  // Return a copy of the model's `attributes` object.
168
- toJSON : function() {
196
+ toJSON: function() {
169
197
  return _.clone(this.attributes);
170
198
  },
171
199
 
172
200
  // Get the value of an attribute.
173
- get : function(attr) {
201
+ get: function(attr) {
174
202
  return this.attributes[attr];
175
203
  },
176
204
 
177
205
  // Get the HTML-escaped value of an attribute.
178
- escape : function(attr) {
206
+ escape: function(attr) {
179
207
  var html;
180
208
  if (html = this._escapedAttributes[attr]) return html;
181
209
  var val = this.attributes[attr];
182
- return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
210
+ return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
183
211
  },
184
212
 
185
213
  // Returns `true` if the attribute contains a value that is not null
186
214
  // or undefined.
187
- has : function(attr) {
215
+ has: function(attr) {
188
216
  return this.attributes[attr] != null;
189
217
  },
190
218
 
191
- // Set a hash of model attributes on the object, firing `"change"` unless you
192
- // choose to silence it.
193
- set : function(attrs, options) {
219
+ // Set a hash of model attributes on the object, firing `"change"` unless
220
+ // you choose to silence it.
221
+ set: function(key, value, options) {
222
+ var attrs, attr, val;
223
+ if (_.isObject(key) || key == null) {
224
+ attrs = key;
225
+ options = value;
226
+ } else {
227
+ attrs = {};
228
+ attrs[key] = value;
229
+ }
194
230
 
195
231
  // Extract attributes and options.
196
232
  options || (options = {});
197
233
  if (!attrs) return this;
198
- if (attrs.attributes) attrs = attrs.attributes;
199
- var now = this.attributes, escaped = this._escapedAttributes;
234
+ if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
235
+ if (options.unset) for (attr in attrs) attrs[attr] = void 0;
200
236
 
201
237
  // Run validation.
202
- if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
238
+ if (!this._validate(attrs, options)) return false;
203
239
 
204
240
  // Check for changes of `id`.
205
241
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
206
242
 
207
- // We're about to start triggering change events.
208
- var alreadyChanging = this._changing;
209
- this._changing = true;
243
+ var now = this.attributes;
244
+ var escaped = this._escapedAttributes;
245
+ var prev = this._previousAttributes || {};
246
+ var alreadySetting = this._setting;
247
+ this._changed || (this._changed = {});
248
+ this._setting = true;
210
249
 
211
250
  // Update attributes.
212
- for (var attr in attrs) {
213
- var val = attrs[attr];
214
- if (!_.isEqual(now[attr], val)) {
215
- now[attr] = val;
216
- delete escaped[attr];
217
- this._changed = true;
218
- if (!options.silent) this.trigger('change:' + attr, this, val, options);
251
+ for (attr in attrs) {
252
+ val = attrs[attr];
253
+ if (!_.isEqual(now[attr], val)) delete escaped[attr];
254
+ options.unset ? delete now[attr] : now[attr] = val;
255
+ if (this._changing && !_.isEqual(this._changed[attr], val)) {
256
+ this.trigger('change:' + attr, this, val, options);
257
+ this._moreChanges = true;
258
+ }
259
+ delete this._changed[attr];
260
+ if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
261
+ this._changed[attr] = val;
219
262
  }
220
263
  }
221
264
 
222
- // Fire the `"change"` event, if the model has been changed.
223
- if (!alreadyChanging && !options.silent && this._changed) this.change(options);
224
- this._changing = false;
265
+ // Fire the `"change"` events, if the model has been changed.
266
+ if (!alreadySetting) {
267
+ if (!options.silent && this.hasChanged()) this.change(options);
268
+ this._setting = false;
269
+ }
225
270
  return this;
226
271
  },
227
272
 
228
273
  // Remove an attribute from the model, firing `"change"` unless you choose
229
274
  // to silence it. `unset` is a noop if the attribute doesn't exist.
230
- unset : function(attr, options) {
231
- if (!(attr in this.attributes)) return this;
232
- options || (options = {});
233
- var value = this.attributes[attr];
234
-
235
- // Run validation.
236
- var validObj = {};
237
- validObj[attr] = void 0;
238
- if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
239
-
240
- // Remove the attribute.
241
- delete this.attributes[attr];
242
- delete this._escapedAttributes[attr];
243
- if (attr == this.idAttribute) delete this.id;
244
- this._changed = true;
245
- if (!options.silent) {
246
- this.trigger('change:' + attr, this, void 0, options);
247
- this.change(options);
248
- }
249
- return this;
275
+ unset: function(attr, options) {
276
+ (options || (options = {})).unset = true;
277
+ return this.set(attr, null, options);
250
278
  },
251
279
 
252
280
  // Clear all attributes on the model, firing `"change"` unless you choose
253
281
  // to silence it.
254
- clear : function(options) {
255
- options || (options = {});
256
- var attr;
257
- var old = this.attributes;
258
-
259
- // Run validation.
260
- var validObj = {};
261
- for (attr in old) validObj[attr] = void 0;
262
- if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
263
-
264
- this.attributes = {};
265
- this._escapedAttributes = {};
266
- this._changed = true;
267
- if (!options.silent) {
268
- for (attr in old) {
269
- this.trigger('change:' + attr, this, void 0, options);
270
- }
271
- this.change(options);
272
- }
273
- return this;
282
+ clear: function(options) {
283
+ (options || (options = {})).unset = true;
284
+ return this.set(_.clone(this.attributes), options);
274
285
  },
275
286
 
276
287
  // Fetch the model from the server. If the server's representation of the
277
288
  // model differs from its current attributes, they will be overriden,
278
289
  // triggering a `"change"` event.
279
- fetch : function(options) {
280
- options || (options = {});
290
+ fetch: function(options) {
291
+ options = options ? _.clone(options) : {};
281
292
  var model = this;
282
293
  var success = options.success;
283
294
  options.success = function(resp, status, xhr) {
284
295
  if (!model.set(model.parse(resp, xhr), options)) return false;
285
296
  if (success) success(model, resp);
286
297
  };
287
- options.error = wrapError(options.error, model, options);
298
+ options.error = Backbone.wrapError(options.error, model, options);
288
299
  return (this.sync || Backbone.sync).call(this, 'read', this, options);
289
300
  },
290
301
 
291
302
  // Set a hash of model attributes, and sync the model to the server.
292
303
  // If the server returns an attributes hash that differs, the model's
293
304
  // state will be `set` again.
294
- save : function(attrs, options) {
295
- options || (options = {});
296
- if (attrs && !this.set(attrs, options)) return false;
305
+ save: function(key, value, options) {
306
+ var attrs, current;
307
+ if (_.isObject(key) || key == null) {
308
+ attrs = key;
309
+ options = value;
310
+ } else {
311
+ attrs = {};
312
+ attrs[key] = value;
313
+ }
314
+
315
+ options = options ? _.clone(options) : {};
316
+ if (options.wait) current = _.clone(this.attributes);
317
+ var silentOptions = _.extend({}, options, {silent: true});
318
+ if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
319
+ return false;
320
+ }
297
321
  var model = this;
298
322
  var success = options.success;
299
323
  options.success = function(resp, status, xhr) {
300
- if (!model.set(model.parse(resp, xhr), options)) return false;
301
- if (success) success(model, resp, xhr);
324
+ var serverAttrs = model.parse(resp, xhr);
325
+ if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
326
+ if (!model.set(serverAttrs, options)) return false;
327
+ if (success) {
328
+ success(model, resp);
329
+ } else {
330
+ model.trigger('sync', model, resp, options);
331
+ }
302
332
  };
303
- options.error = wrapError(options.error, model, options);
333
+ options.error = Backbone.wrapError(options.error, model, options);
304
334
  var method = this.isNew() ? 'create' : 'update';
305
- return (this.sync || Backbone.sync).call(this, method, this, options);
335
+ var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
336
+ if (options.wait) this.set(current, silentOptions);
337
+ return xhr;
306
338
  },
307
339
 
308
- // Destroy this model on the server if it was already persisted. Upon success, the model is removed
309
- // from its collection, if it has one.
310
- destroy : function(options) {
311
- options || (options = {});
312
- if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
340
+ // Destroy this model on the server if it was already persisted.
341
+ // Optimistically removes the model from its collection, if it has one.
342
+ // If `wait: true` is passed, waits for the server to respond before removal.
343
+ destroy: function(options) {
344
+ options = options ? _.clone(options) : {};
313
345
  var model = this;
314
346
  var success = options.success;
315
- options.success = function(resp) {
347
+
348
+ var triggerDestroy = function() {
316
349
  model.trigger('destroy', model, model.collection, options);
317
- if (success) success(model, resp);
318
350
  };
319
- options.error = wrapError(options.error, model, options);
320
- return (this.sync || Backbone.sync).call(this, 'delete', this, options);
351
+
352
+ if (this.isNew()) return triggerDestroy();
353
+ options.success = function(resp) {
354
+ if (options.wait) triggerDestroy();
355
+ if (success) {
356
+ success(model, resp);
357
+ } else {
358
+ model.trigger('sync', model, resp, options);
359
+ }
360
+ };
361
+ options.error = Backbone.wrapError(options.error, model, options);
362
+ var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
363
+ if (!options.wait) triggerDestroy();
364
+ return xhr;
321
365
  },
322
366
 
323
367
  // Default URL for the model's representation on the server -- if you're
324
368
  // using Backbone's restful methods, override this to change the endpoint
325
369
  // that will be called.
326
- url : function() {
327
- var base = getUrl(this.collection) || this.urlRoot || urlError();
370
+ url: function() {
371
+ var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();
328
372
  if (this.isNew()) return base;
329
373
  return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
330
374
  },
331
375
 
332
376
  // **parse** converts a response into the hash of attributes to be `set` on
333
377
  // the model. The default implementation is just to pass the response along.
334
- parse : function(resp, xhr) {
378
+ parse: function(resp, xhr) {
335
379
  return resp;
336
380
  },
337
381
 
338
382
  // Create a new model with identical attributes to this one.
339
- clone : function() {
340
- return new this.constructor(this);
383
+ clone: function() {
384
+ return new this.constructor(this.attributes);
341
385
  },
342
386
 
343
387
  // A model is new if it has never been saved to the server, and lacks an id.
344
- isNew : function() {
388
+ isNew: function() {
345
389
  return this.id == null;
346
390
  },
347
391
 
348
- // Call this method to manually fire a `change` event for this model.
392
+ // Call this method to manually fire a `"change"` event for this model and
393
+ // a `"change:attribute"` event for each changed attribute.
349
394
  // Calling this will cause all objects observing the model to update.
350
- change : function(options) {
351
- this.trigger('change', this, options);
395
+ change: function(options) {
396
+ if (this._changing || !this.hasChanged()) return this;
397
+ this._changing = true;
398
+ this._moreChanges = true;
399
+ for (var attr in this._changed) {
400
+ this.trigger('change:' + attr, this, this._changed[attr], options);
401
+ }
402
+ while (this._moreChanges) {
403
+ this._moreChanges = false;
404
+ this.trigger('change', this, options);
405
+ }
352
406
  this._previousAttributes = _.clone(this.attributes);
353
- this._changed = false;
407
+ delete this._changed;
408
+ this._changing = false;
409
+ return this;
354
410
  },
355
411
 
356
412
  // Determine if the model has changed since the last `"change"` event.
357
413
  // If you specify an attribute name, determine if that attribute has changed.
358
- hasChanged : function(attr) {
359
- if (attr) return this._previousAttributes[attr] != this.attributes[attr];
360
- return this._changed;
414
+ hasChanged: function(attr) {
415
+ if (!arguments.length) return !_.isEmpty(this._changed);
416
+ return this._changed && _.has(this._changed, attr);
361
417
  },
362
418
 
363
- // Return an object containing all the attributes that have changed, or false
364
- // if there are no changed attributes. Useful for determining what parts of a
365
- // view need to be updated and/or what attributes need to be persisted to
366
- // the server.
367
- changedAttributes : function(now) {
368
- now || (now = this.attributes);
369
- var old = this._previousAttributes;
370
- var changed = false;
371
- for (var attr in now) {
372
- if (!_.isEqual(old[attr], now[attr])) {
373
- changed = changed || {};
374
- changed[attr] = now[attr];
375
- }
419
+ // Return an object containing all the attributes that have changed, or
420
+ // false if there are no changed attributes. Useful for determining what
421
+ // parts of a view need to be updated and/or what attributes need to be
422
+ // persisted to the server. Unset attributes will be set to undefined.
423
+ // You can also pass an attributes object to diff against the model,
424
+ // determining if there *would be* a change.
425
+ changedAttributes: function(diff) {
426
+ if (!diff) return this.hasChanged() ? _.clone(this._changed) : false;
427
+ var val, changed = false, old = this._previousAttributes;
428
+ for (var attr in diff) {
429
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
430
+ (changed || (changed = {}))[attr] = val;
376
431
  }
377
432
  return changed;
378
433
  },
379
434
 
380
435
  // Get the previous value of an attribute, recorded at the time the last
381
436
  // `"change"` event was fired.
382
- previous : function(attr) {
383
- if (!attr || !this._previousAttributes) return null;
437
+ previous: function(attr) {
438
+ if (!arguments.length || !this._previousAttributes) return null;
384
439
  return this._previousAttributes[attr];
385
440
  },
386
441
 
387
442
  // Get all of the attributes of the model at the time of the previous
388
443
  // `"change"` event.
389
- previousAttributes : function() {
444
+ previousAttributes: function() {
390
445
  return _.clone(this._previousAttributes);
391
446
  },
392
447
 
448
+ // Check if the model is currently in a valid state. It's only possible to
449
+ // get into an *invalid* state if you're using silent changes.
450
+ isValid: function() {
451
+ return !this.validate(this.attributes);
452
+ },
453
+
393
454
  // Run validation against a set of incoming attributes, returning `true`
394
455
  // if all is well. If a specific `error` callback has been passed,
395
456
  // call that instead of firing the general `"error"` event.
396
- _performValidation : function(attrs, options) {
397
- var error = this.validate(attrs);
398
- if (error) {
399
- if (options.error) {
400
- options.error(this, error, options);
401
- } else {
402
- this.trigger('error', this, error, options);
403
- }
404
- return false;
457
+ _validate: function(attrs, options) {
458
+ if (options.silent || !this.validate) return true;
459
+ attrs = _.extend({}, this.attributes, attrs);
460
+ var error = this.validate(attrs, options);
461
+ if (!error) return true;
462
+ if (options && options.error) {
463
+ options.error(this, error, options);
464
+ } else {
465
+ this.trigger('error', this, error, options);
405
466
  }
406
- return true;
467
+ return false;
407
468
  }
408
469
 
409
470
  });
@@ -417,10 +478,9 @@
417
478
  Backbone.Collection = function(models, options) {
418
479
  options || (options = {});
419
480
  if (options.comparator) this.comparator = options.comparator;
420
- _.bindAll(this, '_onModelEvent', '_removeReference');
421
481
  this._reset();
422
- if (models) this.reset(models, {silent: true});
423
482
  this.initialize.apply(this, arguments);
483
+ if (models) this.reset(models, {silent: true, parse: options.parse});
424
484
  };
425
485
 
426
486
  // Define the Collection's inheritable methods.
@@ -428,52 +488,92 @@
428
488
 
429
489
  // The default model for a collection is just a **Backbone.Model**.
430
490
  // This should be overridden in most cases.
431
- model : Backbone.Model,
491
+ model: Backbone.Model,
432
492
 
433
493
  // Initialize is an empty function by default. Override it with your own
434
494
  // initialization logic.
435
- initialize : function(){},
495
+ initialize: function(){},
436
496
 
437
497
  // The JSON representation of a Collection is an array of the
438
498
  // models' attributes.
439
- toJSON : function() {
499
+ toJSON: function() {
440
500
  return this.map(function(model){ return model.toJSON(); });
441
501
  },
442
502
 
443
503
  // Add a model, or list of models to the set. Pass **silent** to avoid
444
- // firing the `added` event for every new model.
445
- add : function(models, options) {
446
- if (_.isArray(models)) {
447
- for (var i = 0, l = models.length; i < l; i++) {
448
- this._add(models[i], options);
504
+ // firing the `add` event for every new model.
505
+ add: function(models, options) {
506
+ var i, index, length, model, cid, id, cids = {}, ids = {};
507
+ options || (options = {});
508
+ models = _.isArray(models) ? models.slice() : [models];
509
+
510
+ // Begin by turning bare objects into model references, and preventing
511
+ // invalid models or duplicate models from being added.
512
+ for (i = 0, length = models.length; i < length; i++) {
513
+ if (!(model = models[i] = this._prepareModel(models[i], options))) {
514
+ throw new Error("Can't add an invalid model to a collection");
449
515
  }
450
- } else {
451
- this._add(models, options);
516
+ if (cids[cid = model.cid] || this._byCid[cid] ||
517
+ (((id = model.id) != null) && (ids[id] || this._byId[id]))) {
518
+ throw new Error("Can't add the same model to a collection twice");
519
+ }
520
+ cids[cid] = ids[id] = model;
521
+ }
522
+
523
+ // Listen to added models' events, and index models for lookup by
524
+ // `id` and by `cid`.
525
+ for (i = 0; i < length; i++) {
526
+ (model = models[i]).on('all', this._onModelEvent, this);
527
+ this._byCid[model.cid] = model;
528
+ if (model.id != null) this._byId[model.id] = model;
529
+ }
530
+
531
+ // Insert models into the collection, re-sorting if needed, and triggering
532
+ // `add` events unless silenced.
533
+ this.length += length;
534
+ index = options.at != null ? options.at : this.models.length;
535
+ splice.apply(this.models, [index, 0].concat(models));
536
+ if (this.comparator) this.sort({silent: true});
537
+ if (options.silent) return this;
538
+ for (i = 0, length = this.models.length; i < length; i++) {
539
+ if (!cids[(model = this.models[i]).cid]) continue;
540
+ options.index = i;
541
+ model.trigger('add', model, this, options);
452
542
  }
453
543
  return this;
454
544
  },
455
545
 
456
546
  // Remove a model, or a list of models from the set. Pass silent to avoid
457
- // firing the `removed` event for every model removed.
458
- remove : function(models, options) {
459
- if (_.isArray(models)) {
460
- for (var i = 0, l = models.length; i < l; i++) {
461
- this._remove(models[i], options);
547
+ // firing the `remove` event for every model removed.
548
+ remove: function(models, options) {
549
+ var i, l, index, model;
550
+ options || (options = {});
551
+ models = _.isArray(models) ? models.slice() : [models];
552
+ for (i = 0, l = models.length; i < l; i++) {
553
+ model = this.getByCid(models[i]) || this.get(models[i]);
554
+ if (!model) continue;
555
+ delete this._byId[model.id];
556
+ delete this._byCid[model.cid];
557
+ index = this.indexOf(model);
558
+ this.models.splice(index, 1);
559
+ this.length--;
560
+ if (!options.silent) {
561
+ options.index = index;
562
+ model.trigger('remove', model, this, options);
462
563
  }
463
- } else {
464
- this._remove(models, options);
564
+ this._removeReference(model);
465
565
  }
466
566
  return this;
467
567
  },
468
568
 
469
569
  // Get a model from the set by id.
470
- get : function(id) {
570
+ get: function(id) {
471
571
  if (id == null) return null;
472
572
  return this._byId[id.id != null ? id.id : id];
473
573
  },
474
574
 
475
575
  // Get a model from the set by client id.
476
- getByCid : function(cid) {
576
+ getByCid: function(cid) {
477
577
  return cid && this._byCid[cid.cid || cid];
478
578
  },
479
579
 
@@ -482,30 +582,38 @@
482
582
  return this.models[index];
483
583
  },
484
584
 
485
- // Force the collection to re-sort itself. You don't need to call this under normal
486
- // circumstances, as the set will maintain sort order as each item is added.
487
- sort : function(options) {
585
+ // Force the collection to re-sort itself. You don't need to call this under
586
+ // normal circumstances, as the set will maintain sort order as each item
587
+ // is added.
588
+ sort: function(options) {
488
589
  options || (options = {});
489
590
  if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
490
- this.models = this.sortBy(this.comparator);
591
+ var boundComparator = _.bind(this.comparator, this);
592
+ if (this.comparator.length == 1) {
593
+ this.models = this.sortBy(boundComparator);
594
+ } else {
595
+ this.models.sort(boundComparator);
596
+ }
491
597
  if (!options.silent) this.trigger('reset', this, options);
492
598
  return this;
493
599
  },
494
600
 
495
601
  // Pluck an attribute from each model in the collection.
496
- pluck : function(attr) {
602
+ pluck: function(attr) {
497
603
  return _.map(this.models, function(model){ return model.get(attr); });
498
604
  },
499
605
 
500
606
  // When you have more items than you want to add or remove individually,
501
607
  // you can reset the entire set with a new list of models, without firing
502
- // any `added` or `removed` events. Fires `reset` when finished.
503
- reset : function(models, options) {
608
+ // any `add` or `remove` events. Fires `reset` when finished.
609
+ reset: function(models, options) {
504
610
  models || (models = []);
505
611
  options || (options = {});
506
- this.each(this._removeReference);
612
+ for (var i = 0, l = this.models.length; i < l; i++) {
613
+ this._removeReference(this.models[i]);
614
+ }
507
615
  this._reset();
508
- this.add(models, {silent: true});
616
+ this.add(models, {silent: true, parse: options.parse});
509
617
  if (!options.silent) this.trigger('reset', this, options);
510
618
  return this;
511
619
  },
@@ -513,30 +621,36 @@
513
621
  // Fetch the default set of models for this collection, resetting the
514
622
  // collection when they arrive. If `add: true` is passed, appends the
515
623
  // models to the collection instead of resetting.
516
- fetch : function(options) {
517
- options || (options = {});
624
+ fetch: function(options) {
625
+ options = options ? _.clone(options) : {};
626
+ if (options.parse === undefined) options.parse = true;
518
627
  var collection = this;
519
628
  var success = options.success;
520
629
  options.success = function(resp, status, xhr) {
521
630
  collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
522
631
  if (success) success(collection, resp);
523
632
  };
524
- options.error = wrapError(options.error, collection, options);
633
+ options.error = Backbone.wrapError(options.error, collection, options);
525
634
  return (this.sync || Backbone.sync).call(this, 'read', this, options);
526
635
  },
527
636
 
528
- // Create a new instance of a model in this collection. After the model
529
- // has been created on the server, it will be added to the collection.
530
- // Returns the model, or 'false' if validation on a new model fails.
531
- create : function(model, options) {
637
+ // Create a new instance of a model in this collection. Add the model to the
638
+ // collection immediately, unless `wait: true` is passed, in which case we
639
+ // wait for the server to agree.
640
+ create: function(model, options) {
532
641
  var coll = this;
533
- options || (options = {});
642
+ options = options ? _.clone(options) : {};
534
643
  model = this._prepareModel(model, options);
535
644
  if (!model) return false;
645
+ if (!options.wait) coll.add(model, options);
536
646
  var success = options.success;
537
647
  options.success = function(nextModel, resp, xhr) {
538
- coll.add(nextModel, options);
539
- if (success) success(nextModel, resp, xhr);
648
+ if (options.wait) coll.add(nextModel, options);
649
+ if (success) {
650
+ success(nextModel, resp);
651
+ } else {
652
+ nextModel.trigger('sync', model, resp, options);
653
+ }
540
654
  };
541
655
  model.save(null, options);
542
656
  return model;
@@ -544,7 +658,7 @@
544
658
 
545
659
  // **parse** converts a response into a list of models to be added to the
546
660
  // collection. The default implementation is just to pass it through.
547
- parse : function(resp, xhr) {
661
+ parse: function(resp, xhr) {
548
662
  return resp;
549
663
  },
550
664
 
@@ -556,77 +670,42 @@
556
670
  },
557
671
 
558
672
  // Reset all internal state. Called when the collection is reset.
559
- _reset : function(options) {
673
+ _reset: function(options) {
560
674
  this.length = 0;
561
675
  this.models = [];
562
676
  this._byId = {};
563
677
  this._byCid = {};
564
678
  },
565
679
 
566
- // Prepare a model to be added to this collection
680
+ // Prepare a model or hash of attributes to be added to this collection.
567
681
  _prepareModel: function(model, options) {
568
682
  if (!(model instanceof Backbone.Model)) {
569
683
  var attrs = model;
570
- model = new this.model(attrs, {collection: this});
571
- if (model.validate && !model._performValidation(attrs, options)) model = false;
684
+ options.collection = this;
685
+ model = new this.model(attrs, options);
686
+ if (!model._validate(model.attributes, options)) model = false;
572
687
  } else if (!model.collection) {
573
688
  model.collection = this;
574
689
  }
575
690
  return model;
576
691
  },
577
692
 
578
- // Internal implementation of adding a single model to the set, updating
579
- // hash indexes for `id` and `cid` lookups.
580
- // Returns the model, or 'false' if validation on a new model fails.
581
- _add : function(model, options) {
582
- options || (options = {});
583
- model = this._prepareModel(model, options);
584
- if (!model) return false;
585
- var already = this.getByCid(model);
586
- if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
587
- this._byId[model.id] = model;
588
- this._byCid[model.cid] = model;
589
- var index = options.at != null ? options.at :
590
- this.comparator ? this.sortedIndex(model, this.comparator) :
591
- this.length;
592
- this.models.splice(index, 0, model);
593
- model.bind('all', this._onModelEvent);
594
- this.length++;
595
- if (!options.silent) model.trigger('add', model, this, options);
596
- return model;
597
- },
598
-
599
- // Internal implementation of removing a single model from the set, updating
600
- // hash indexes for `id` and `cid` lookups.
601
- _remove : function(model, options) {
602
- options || (options = {});
603
- model = this.getByCid(model) || this.get(model);
604
- if (!model) return null;
605
- delete this._byId[model.id];
606
- delete this._byCid[model.cid];
607
- this.models.splice(this.indexOf(model), 1);
608
- this.length--;
609
- if (!options.silent) model.trigger('remove', model, this, options);
610
- this._removeReference(model);
611
- return model;
612
- },
613
-
614
693
  // Internal method to remove a model's ties to a collection.
615
- _removeReference : function(model) {
694
+ _removeReference: function(model) {
616
695
  if (this == model.collection) {
617
696
  delete model.collection;
618
697
  }
619
- model.unbind('all', this._onModelEvent);
698
+ model.off('all', this._onModelEvent, this);
620
699
  },
621
700
 
622
701
  // Internal method called every time a model in the set fires an event.
623
702
  // Sets need to update their indexes when models change ids. All other
624
703
  // events simply proxy through. "add" and "remove" events that originate
625
704
  // in other collections are ignored.
626
- _onModelEvent : function(ev, model, collection, options) {
705
+ _onModelEvent: function(ev, model, collection, options) {
627
706
  if ((ev == 'add' || ev == 'remove') && collection != this) return;
628
707
  if (ev == 'destroy') {
629
- this._remove(model, options);
708
+ this.remove(model, options);
630
709
  }
631
710
  if (model && ev === 'change:' + model.idAttribute) {
632
711
  delete this._byId[model.previous(model.idAttribute)];
@@ -638,10 +717,11 @@
638
717
  });
639
718
 
640
719
  // Underscore methods that we want to implement on the Collection.
641
- var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
642
- 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
643
- 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
644
- 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
720
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
721
+ 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
722
+ 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
723
+ 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
724
+ 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
645
725
 
646
726
  // Mix in each Underscore method as a proxy to `Collection#models`.
647
727
  _.each(methods, function(method) {
@@ -664,8 +744,8 @@
664
744
 
665
745
  // Cached regular expressions for matching named param parts and splatted
666
746
  // parts of route strings.
667
- var namedParam = /:([\w\d]+)/g;
668
- var splatParam = /\*([\w\d]+)/g;
747
+ var namedParam = /:\w+/g;
748
+ var splatParam = /\*\w+/g;
669
749
  var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
670
750
 
671
751
  // Set up all inheritable **Backbone.Router** properties and methods.
@@ -673,7 +753,7 @@
673
753
 
674
754
  // Initialize is an empty function by default. Override it with your own
675
755
  // initialization logic.
676
- initialize : function(){},
756
+ initialize: function(){},
677
757
 
678
758
  // Manually bind a single named route to a callback. For example:
679
759
  //
@@ -681,25 +761,28 @@
681
761
  // ...
682
762
  // });
683
763
  //
684
- route : function(route, name, callback) {
764
+ route: function(route, name, callback) {
685
765
  Backbone.history || (Backbone.history = new Backbone.History);
686
766
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
767
+ if (!callback) callback = this[name];
687
768
  Backbone.history.route(route, _.bind(function(fragment) {
688
769
  var args = this._extractParameters(route, fragment);
689
- callback.apply(this, args);
770
+ callback && callback.apply(this, args);
690
771
  this.trigger.apply(this, ['route:' + name].concat(args));
772
+ Backbone.history.trigger('route', this, name, args);
691
773
  }, this));
774
+ return this;
692
775
  },
693
776
 
694
777
  // Simple proxy to `Backbone.history` to save a fragment into the history.
695
- navigate : function(fragment, triggerRoute) {
696
- Backbone.history.navigate(fragment, triggerRoute);
778
+ navigate: function(fragment, options) {
779
+ Backbone.history.navigate(fragment, options);
697
780
  },
698
781
 
699
782
  // Bind all defined routes to `Backbone.history`. We have to reverse the
700
783
  // order of the routes here to support behavior where the most general
701
784
  // routes can be defined at the bottom of the route map.
702
- _bindRoutes : function() {
785
+ _bindRoutes: function() {
703
786
  if (!this.routes) return;
704
787
  var routes = [];
705
788
  for (var route in this.routes) {
@@ -712,16 +795,16 @@
712
795
 
713
796
  // Convert a route string into a regular expression, suitable for matching
714
797
  // against the current location hash.
715
- _routeToRegExp : function(route) {
716
- route = route.replace(escapeRegExp, "\\$&")
717
- .replace(namedParam, "([^\/]*)")
718
- .replace(splatParam, "(.*?)");
798
+ _routeToRegExp: function(route) {
799
+ route = route.replace(escapeRegExp, '\\$&')
800
+ .replace(namedParam, '([^\/]+)')
801
+ .replace(splatParam, '(.*?)');
719
802
  return new RegExp('^' + route + '$');
720
803
  },
721
804
 
722
805
  // Given a route, and a URL fragment that it matches, return the array of
723
806
  // extracted parameters.
724
- _extractParameters : function(route, fragment) {
807
+ _extractParameters: function(route, fragment) {
725
808
  return route.exec(fragment).slice(1);
726
809
  }
727
810
 
@@ -737,8 +820,8 @@
737
820
  _.bindAll(this, 'checkUrl');
738
821
  };
739
822
 
740
- // Cached regex for cleaning hashes.
741
- var hashStrip = /^#*/;
823
+ // Cached regex for cleaning leading hashes and slashes .
824
+ var routeStripper = /^[#\/]/;
742
825
 
743
826
  // Cached regex for detecting MSIE.
744
827
  var isExplorer = /msie [\w.]+/;
@@ -747,7 +830,7 @@
747
830
  var historyStarted = false;
748
831
 
749
832
  // Set up all inheritable **Backbone.History** properties and methods.
750
- _.extend(Backbone.History.prototype, {
833
+ _.extend(Backbone.History.prototype, Backbone.Events, {
751
834
 
752
835
  // The default interval to poll for hash changes, if necessary, is
753
836
  // twenty times a second.
@@ -755,28 +838,30 @@
755
838
 
756
839
  // Get the cross-browser normalized URL fragment, either from the URL,
757
840
  // the hash, or the override.
758
- getFragment : function(fragment, forcePushState) {
841
+ getFragment: function(fragment, forcePushState) {
759
842
  if (fragment == null) {
760
843
  if (this._hasPushState || forcePushState) {
761
844
  fragment = window.location.pathname;
762
845
  var search = window.location.search;
763
846
  if (search) fragment += search;
764
- if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
765
847
  } else {
766
848
  fragment = window.location.hash;
767
849
  }
768
850
  }
769
- return decodeURIComponent(fragment.replace(hashStrip, ''));
851
+ fragment = decodeURIComponent(fragment);
852
+ if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
853
+ return fragment.replace(routeStripper, '');
770
854
  },
771
855
 
772
856
  // Start the hash change handling, returning `true` if the current URL matches
773
857
  // an existing route, and `false` otherwise.
774
- start : function(options) {
858
+ start: function(options) {
775
859
 
776
860
  // Figure out the initial configuration. Do we need an iframe?
777
861
  // Is pushState desired ... is it available?
778
862
  if (historyStarted) throw new Error("Backbone.history has already been started");
779
863
  this.options = _.extend({}, {root: '/'}, this.options, options);
864
+ this._wantsHashChange = this.options.hashChange !== false;
780
865
  this._wantsPushState = !!this.options.pushState;
781
866
  this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
782
867
  var fragment = this.getFragment();
@@ -791,10 +876,10 @@
791
876
  // 'onhashchange' is supported, determine how we check the URL state.
792
877
  if (this._hasPushState) {
793
878
  $(window).bind('popstate', this.checkUrl);
794
- } else if ('onhashchange' in window && !oldIE) {
879
+ } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
795
880
  $(window).bind('hashchange', this.checkUrl);
796
- } else {
797
- setInterval(this.checkUrl, this.interval);
881
+ } else if (this._wantsHashChange) {
882
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
798
883
  }
799
884
 
800
885
  // Determine if we need to change the base url, for a pushState link
@@ -803,13 +888,19 @@
803
888
  historyStarted = true;
804
889
  var loc = window.location;
805
890
  var atRoot = loc.pathname == this.options.root;
806
- if (this._wantsPushState && !this._hasPushState && !atRoot) {
891
+
892
+ // If we've started off with a route from a `pushState`-enabled browser,
893
+ // but we're currently in a browser that doesn't support it...
894
+ if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
807
895
  this.fragment = this.getFragment(null, true);
808
896
  window.location.replace(this.options.root + '#' + this.fragment);
809
897
  // Return immediately as browser will do redirect to new url
810
898
  return true;
899
+
900
+ // Or if we've started out with a hash-based route, but we're currently
901
+ // in a browser where it could be `pushState`-based instead...
811
902
  } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
812
- this.fragment = loc.hash.replace(hashStrip, '');
903
+ this.fragment = loc.hash.replace(routeStripper, '');
813
904
  window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
814
905
  }
815
906
 
@@ -818,15 +909,23 @@
818
909
  }
819
910
  },
820
911
 
821
- // Add a route to be tested when the fragment changes. Routes added later may
822
- // override previous routes.
823
- route : function(route, callback) {
824
- this.handlers.unshift({route : route, callback : callback});
912
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
913
+ // but possibly useful for unit testing Routers.
914
+ stop: function() {
915
+ $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
916
+ clearInterval(this._checkUrlInterval);
917
+ historyStarted = false;
918
+ },
919
+
920
+ // Add a route to be tested when the fragment changes. Routes added later
921
+ // may override previous routes.
922
+ route: function(route, callback) {
923
+ this.handlers.unshift({route: route, callback: callback});
825
924
  },
826
925
 
827
926
  // Checks the current URL to see if it has changed, and if it has,
828
927
  // calls `loadUrl`, normalizing across the hidden iframe.
829
- checkUrl : function(e) {
928
+ checkUrl: function(e) {
830
929
  var current = this.getFragment();
831
930
  if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
832
931
  if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
@@ -837,7 +936,7 @@
837
936
  // Attempt to load the current URL fragment. If a route succeeds with a
838
937
  // match, returns `true`. If no defined routes matches the fragment,
839
938
  // returns `false`.
840
- loadUrl : function(fragmentOverride) {
939
+ loadUrl: function(fragmentOverride) {
841
940
  var fragment = this.fragment = this.getFragment(fragmentOverride);
842
941
  var matched = _.any(this.handlers, function(handler) {
843
942
  if (handler.route.test(fragment)) {
@@ -848,27 +947,54 @@
848
947
  return matched;
849
948
  },
850
949
 
851
- // Save a fragment into the hash history. You are responsible for properly
852
- // URL-encoding the fragment in advance. This does not trigger
853
- // a `hashchange` event.
854
- navigate : function(fragment, triggerRoute) {
855
- var frag = (fragment || '').replace(hashStrip, '');
950
+ // Save a fragment into the hash history, or replace the URL state if the
951
+ // 'replace' option is passed. You are responsible for properly URL-encoding
952
+ // the fragment in advance.
953
+ //
954
+ // The options object can contain `trigger: true` if you wish to have the
955
+ // route callback be fired (not usually desirable), or `replace: true`, if
956
+ // you which to modify the current URL without adding an entry to the history.
957
+ navigate: function(fragment, options) {
958
+ if (!historyStarted) return false;
959
+ if (!options || options === true) options = {trigger: options};
960
+ var frag = (fragment || '').replace(routeStripper, '');
856
961
  if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
962
+
963
+ // If pushState is available, we use it to set the fragment as a real URL.
857
964
  if (this._hasPushState) {
858
- var loc = window.location;
859
965
  if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
860
966
  this.fragment = frag;
861
- window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
862
- } else {
863
- window.location.hash = this.fragment = frag;
967
+ window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
968
+
969
+ // If hash changes haven't been explicitly disabled, update the hash
970
+ // fragment to store history.
971
+ } else if (this._wantsHashChange) {
972
+ this.fragment = frag;
973
+ this._updateHash(window.location, frag, options.replace);
864
974
  if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
865
- this.iframe.document.open().close();
866
- this.iframe.location.hash = frag;
975
+ // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
976
+ // When replace is true, we don't want this.
977
+ if(!options.replace) this.iframe.document.open().close();
978
+ this._updateHash(this.iframe.location, frag, options.replace);
867
979
  }
980
+
981
+ // If you've told us that you explicitly don't want fallback hashchange-
982
+ // based history, then `navigate` becomes a page refresh.
983
+ } else {
984
+ window.location.assign(this.options.root + fragment);
868
985
  }
869
- if (triggerRoute) this.loadUrl(fragment);
870
- }
986
+ if (options.trigger) this.loadUrl(fragment);
987
+ },
871
988
 
989
+ // Update the hash location, either replacing the current entry, or adding
990
+ // a new one to the browser history.
991
+ _updateHash: function(location, fragment, replace) {
992
+ if (replace) {
993
+ location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
994
+ } else {
995
+ location.hash = fragment;
996
+ }
997
+ }
872
998
  });
873
999
 
874
1000
  // Backbone.View
@@ -880,15 +1006,8 @@
880
1006
  this.cid = _.uniqueId('view');
881
1007
  this._configure(options || {});
882
1008
  this._ensureElement();
883
- this.delegateEvents();
884
1009
  this.initialize.apply(this, arguments);
885
- };
886
-
887
- // Element lookup, scoped to DOM elements within the current view.
888
- // This should be prefered to global lookups, if you're dealing with
889
- // a specific view.
890
- var selectorDelegate = function(selector) {
891
- return $(selector, this.el);
1010
+ this.delegateEvents();
892
1011
  };
893
1012
 
894
1013
  // Cached regex to split keys for `delegate`.
@@ -901,26 +1020,29 @@
901
1020
  _.extend(Backbone.View.prototype, Backbone.Events, {
902
1021
 
903
1022
  // The default `tagName` of a View's element is `"div"`.
904
- tagName : 'div',
1023
+ tagName: 'div',
905
1024
 
906
- // Attach the `selectorDelegate` function as the `$` property.
907
- $ : selectorDelegate,
1025
+ // jQuery delegate for element lookup, scoped to DOM elements within the
1026
+ // current view. This should be prefered to global lookups where possible.
1027
+ $: function(selector) {
1028
+ return this.$el.find(selector);
1029
+ },
908
1030
 
909
1031
  // Initialize is an empty function by default. Override it with your own
910
1032
  // initialization logic.
911
- initialize : function(){},
1033
+ initialize: function(){},
912
1034
 
913
1035
  // **render** is the core function that your view should override, in order
914
1036
  // to populate its element (`this.el`), with the appropriate HTML. The
915
1037
  // convention is for **render** to always return `this`.
916
- render : function() {
1038
+ render: function() {
917
1039
  return this;
918
1040
  },
919
1041
 
920
1042
  // Remove this view from the DOM. Note that the view isn't present in the
921
1043
  // DOM by default, so calling this method may be a no-op.
922
- remove : function() {
923
- $(this.el).remove();
1044
+ remove: function() {
1045
+ this.$el.remove();
924
1046
  return this;
925
1047
  },
926
1048
 
@@ -929,20 +1051,30 @@
929
1051
  //
930
1052
  // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
931
1053
  //
932
- make : function(tagName, attributes, content) {
1054
+ make: function(tagName, attributes, content) {
933
1055
  var el = document.createElement(tagName);
934
1056
  if (attributes) $(el).attr(attributes);
935
1057
  if (content) $(el).html(content);
936
1058
  return el;
937
1059
  },
938
1060
 
939
- // Set callbacks, where `this.callbacks` is a hash of
1061
+ // Change the view's element (`this.el` property), including event
1062
+ // re-delegation.
1063
+ setElement: function(element, delegate) {
1064
+ this.$el = $(element);
1065
+ this.el = this.$el[0];
1066
+ if (delegate !== false) this.delegateEvents();
1067
+ return this;
1068
+ },
1069
+
1070
+ // Set callbacks, where `this.events` is a hash of
940
1071
  //
941
1072
  // *{"event selector": "callback"}*
942
1073
  //
943
1074
  // {
944
1075
  // 'mousedown .title': 'edit',
945
1076
  // 'click .button': 'save'
1077
+ // 'click .open': function(e) { ... }
946
1078
  // }
947
1079
  //
948
1080
  // pairs. Callbacks will be bound to the view, with `this` set properly.
@@ -950,29 +1082,36 @@
950
1082
  // Omitting the selector binds the event to `this.el`.
951
1083
  // This only works for delegate-able events: not `focus`, `blur`, and
952
1084
  // not `change`, `submit`, and `reset` in Internet Explorer.
953
- delegateEvents : function(events) {
954
- if (!(events || (events = this.events))) return;
955
- if (_.isFunction(events)) events = events.call(this);
956
- $(this.el).unbind('.delegateEvents' + this.cid);
1085
+ delegateEvents: function(events) {
1086
+ if (!(events || (events = getValue(this, 'events')))) return;
1087
+ this.undelegateEvents();
957
1088
  for (var key in events) {
958
- var method = this[events[key]];
1089
+ var method = events[key];
1090
+ if (!_.isFunction(method)) method = this[events[key]];
959
1091
  if (!method) throw new Error('Event "' + events[key] + '" does not exist');
960
1092
  var match = key.match(eventSplitter);
961
1093
  var eventName = match[1], selector = match[2];
962
1094
  method = _.bind(method, this);
963
1095
  eventName += '.delegateEvents' + this.cid;
964
1096
  if (selector === '') {
965
- $(this.el).bind(eventName, method);
1097
+ this.$el.bind(eventName, method);
966
1098
  } else {
967
- $(this.el).delegate(selector, eventName, method);
1099
+ this.$el.delegate(selector, eventName, method);
968
1100
  }
969
1101
  }
970
1102
  },
971
1103
 
1104
+ // Clears all callbacks previously bound to the view with `delegateEvents`.
1105
+ // You usually don't need to use this, but may wish to if you have multiple
1106
+ // Backbone views attached to the same DOM element.
1107
+ undelegateEvents: function() {
1108
+ this.$el.unbind('.delegateEvents' + this.cid);
1109
+ },
1110
+
972
1111
  // Performs the initial configuration of a View with a set of options.
973
1112
  // Keys with special meaning *(model, collection, id, className)*, are
974
1113
  // attached directly to the view.
975
- _configure : function(options) {
1114
+ _configure: function(options) {
976
1115
  if (this.options) options = _.extend({}, this.options, options);
977
1116
  for (var i = 0, l = viewOptions.length; i < l; i++) {
978
1117
  var attr = viewOptions[i];
@@ -984,15 +1123,15 @@
984
1123
  // Ensure that the View has a DOM element to render into.
985
1124
  // If `this.el` is a string, pass it through `$()`, take the first
986
1125
  // matching element, and re-assign it to `el`. Otherwise, create
987
- // an element from the `id`, `className` and `tagName` proeprties.
988
- _ensureElement : function() {
1126
+ // an element from the `id`, `className` and `tagName` properties.
1127
+ _ensureElement: function() {
989
1128
  if (!this.el) {
990
- var attrs = this.attributes || {};
1129
+ var attrs = getValue(this, 'attributes') || {};
991
1130
  if (this.id) attrs.id = this.id;
992
1131
  if (this.className) attrs['class'] = this.className;
993
- this.el = this.make(this.tagName, attrs);
994
- } else if (_.isString(this.el)) {
995
- this.el = $(this.el).get(0);
1132
+ this.setElement(this.make(this.tagName, attrs), false);
1133
+ } else {
1134
+ this.setElement(this.el, false);
996
1135
  }
997
1136
  }
998
1137
 
@@ -1009,20 +1148,20 @@
1009
1148
  Backbone.Model.extend = Backbone.Collection.extend =
1010
1149
  Backbone.Router.extend = Backbone.View.extend = extend;
1011
1150
 
1151
+ // Backbone.sync
1152
+ // -------------
1153
+
1012
1154
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1013
1155
  var methodMap = {
1014
1156
  'create': 'POST',
1015
1157
  'update': 'PUT',
1016
1158
  'delete': 'DELETE',
1017
- 'read' : 'GET'
1159
+ 'read': 'GET'
1018
1160
  };
1019
1161
 
1020
- // Backbone.sync
1021
- // -------------
1022
-
1023
1162
  // Override this function to change the manner in which Backbone persists
1024
1163
  // models to the server. You will be passed the type of request, and the
1025
- // model in question. By default, uses makes a RESTful Ajax request
1164
+ // model in question. By default, makes a RESTful Ajax request
1026
1165
  // to the model's `url()`. Some possible customizations could be:
1027
1166
  //
1028
1167
  // * Use `setTimeout` to batch rapid-fire updates into a single request.
@@ -1031,26 +1170,23 @@
1031
1170
  //
1032
1171
  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1033
1172
  // as `POST`, with a `_method` parameter containing the true HTTP method,
1034
- // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
1035
- // `application/json` with the model in a param named `model`.
1173
+ // as well as all requests with the body as `application/x-www-form-urlencoded`
1174
+ // instead of `application/json` with the model in a param named `model`.
1036
1175
  // Useful when interfacing with server-side languages like **PHP** that make
1037
1176
  // it difficult to read the body of `PUT` requests.
1038
1177
  Backbone.sync = function(method, model, options) {
1039
1178
  var type = methodMap[method];
1040
1179
 
1041
1180
  // Default JSON-request options.
1042
- var params = _.extend({
1043
- type: type,
1044
- dataType: 'json'
1045
- }, options);
1181
+ var params = {type: type, dataType: 'json'};
1046
1182
 
1047
1183
  // Ensure that we have a URL.
1048
- if (!params.url) {
1049
- params.url = getUrl(model) || urlError();
1184
+ if (!options.url) {
1185
+ params.url = getValue(model, 'url') || urlError();
1050
1186
  }
1051
1187
 
1052
1188
  // Ensure that we have the appropriate request data.
1053
- if (!params.data && model && (method == 'create' || method == 'update')) {
1189
+ if (!options.data && model && (method == 'create' || method == 'update')) {
1054
1190
  params.contentType = 'application/json';
1055
1191
  params.data = JSON.stringify(model.toJSON());
1056
1192
  }
@@ -1058,7 +1194,7 @@
1058
1194
  // For older servers, emulate JSON by encoding the request into an HTML-form.
1059
1195
  if (Backbone.emulateJSON) {
1060
1196
  params.contentType = 'application/x-www-form-urlencoded';
1061
- params.data = params.data ? {model : params.data} : {};
1197
+ params.data = params.data ? {model: params.data} : {};
1062
1198
  }
1063
1199
 
1064
1200
  // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
@@ -1078,8 +1214,20 @@
1078
1214
  params.processData = false;
1079
1215
  }
1080
1216
 
1081
- // Make the request.
1082
- return $.ajax(params);
1217
+ // Make the request, allowing the user to override any Ajax options.
1218
+ return $.ajax(_.extend(params, options));
1219
+ };
1220
+
1221
+ // Wrap an optional error callback with a fallback error event.
1222
+ Backbone.wrapError = function(onError, originalModel, options) {
1223
+ return function(model, resp) {
1224
+ resp = model === originalModel ? resp : model;
1225
+ if (onError) {
1226
+ onError(originalModel, resp, options);
1227
+ } else {
1228
+ originalModel.trigger('error', originalModel, resp, options);
1229
+ }
1230
+ };
1083
1231
  };
1084
1232
 
1085
1233
  // Helpers
@@ -1096,11 +1244,11 @@
1096
1244
 
1097
1245
  // The constructor function for the new subclass is either defined by you
1098
1246
  // (the "constructor" property in your `extend` definition), or defaulted
1099
- // by us to simply call `super()`.
1247
+ // by us to simply call the parent's constructor.
1100
1248
  if (protoProps && protoProps.hasOwnProperty('constructor')) {
1101
1249
  child = protoProps.constructor;
1102
1250
  } else {
1103
- child = function(){ return parent.apply(this, arguments); };
1251
+ child = function(){ parent.apply(this, arguments); };
1104
1252
  }
1105
1253
 
1106
1254
  // Inherit class (static) properties from parent.
@@ -1127,11 +1275,11 @@
1127
1275
  return child;
1128
1276
  };
1129
1277
 
1130
- // Helper function to get a URL from a Model or Collection as a property
1278
+ // Helper function to get a value from a Backbone object as a property
1131
1279
  // or as a function.
1132
- var getUrl = function(object) {
1133
- if (!(object && object.url)) return null;
1134
- return _.isFunction(object.url) ? object.url() : object.url;
1280
+ var getValue = function(object, prop) {
1281
+ if (!(object && object[prop])) return null;
1282
+ return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1135
1283
  };
1136
1284
 
1137
1285
  // Throw an error when a URL is needed, and none is supplied.
@@ -1139,20 +1287,4 @@
1139
1287
  throw new Error('A "url" property or function must be specified');
1140
1288
  };
1141
1289
 
1142
- // Wrap an optional error callback with a fallback error event.
1143
- var wrapError = function(onError, model, options) {
1144
- return function(resp) {
1145
- if (onError) {
1146
- onError(model, resp, options);
1147
- } else {
1148
- model.trigger('error', model, resp, options);
1149
- }
1150
- };
1151
- };
1152
-
1153
- // Helper function to escape a string for HTML rendering.
1154
- var escapeHTML = function(string) {
1155
- return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
1156
- };
1157
-
1158
1290
  }).call(this);