backbonejs-rails 0.0.3 → 0.0.4
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/.gitignore +2 -0
- data/.rvmrc +1 -1
- data/README.md +1 -1
- data/backbonejs-rails.gemspec +0 -1
- data/lib/backbonejs-rails/version.rb +1 -1
- data/lib/backbonejs-rails.rb +3 -0
- data/lib/generators/backbonejs/install/install_generator.rb +94 -54
- data/lib/generators/backbonejs/uninstall/uninstall_generator.rb +31 -0
- data/vendor/assets/javascripts/backbone.js +340 -193
- data/vendor/assets/javascripts/backbone.min.js +29 -23
- data/vendor/assets/javascripts/underscore.js +49 -17
- data/vendor/assets/javascripts/underscore.min.js +19 -18
- metadata +58 -78
- data/.DS_Store +0 -0
- data/vendor/.DS_Store +0 -0
- data/vendor/assets/.DS_Store +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
// Backbone.js 0.
|
1
|
+
// Backbone.js 0.5.3
|
2
2
|
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
3
3
|
// Backbone may be freely distributed under the MIT license.
|
4
4
|
// For all details and documentation:
|
@@ -9,26 +9,39 @@
|
|
9
9
|
// Initial Setup
|
10
10
|
// -------------
|
11
11
|
|
12
|
+
// Save a reference to the global object.
|
13
|
+
var root = this;
|
14
|
+
|
15
|
+
// Save the previous value of the `Backbone` variable.
|
16
|
+
var previousBackbone = root.Backbone;
|
17
|
+
|
12
18
|
// The top-level namespace. All public Backbone classes and modules will
|
13
19
|
// be attached to this. Exported for both CommonJS and the browser.
|
14
20
|
var Backbone;
|
15
21
|
if (typeof exports !== 'undefined') {
|
16
22
|
Backbone = exports;
|
17
23
|
} else {
|
18
|
-
Backbone =
|
24
|
+
Backbone = root.Backbone = {};
|
19
25
|
}
|
20
26
|
|
21
27
|
// Current version of the library. Keep in sync with `package.json`.
|
22
|
-
Backbone.VERSION = '0.
|
28
|
+
Backbone.VERSION = '0.5.3';
|
23
29
|
|
24
30
|
// Require Underscore, if we're on the server, and it's not already present.
|
25
|
-
var _ =
|
26
|
-
if (!_ && (typeof require !== 'undefined')) _ = require(
|
31
|
+
var _ = root._;
|
32
|
+
if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
|
27
33
|
|
28
|
-
// For Backbone's purposes,
|
29
|
-
var $ =
|
34
|
+
// For Backbone's purposes, jQuery or Zepto owns the `$` variable.
|
35
|
+
var $ = root.jQuery || root.Zepto;
|
36
|
+
|
37
|
+
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
|
38
|
+
// to its previous owner. Returns a reference to this Backbone object.
|
39
|
+
Backbone.noConflict = function() {
|
40
|
+
root.Backbone = previousBackbone;
|
41
|
+
return this;
|
42
|
+
};
|
30
43
|
|
31
|
-
// Turn on `emulateHTTP` to
|
44
|
+
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
|
32
45
|
// fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
|
33
46
|
// `X-Http-Method-Override` header.
|
34
47
|
Backbone.emulateHTTP = false;
|
@@ -55,10 +68,10 @@
|
|
55
68
|
|
56
69
|
// Bind an event, specified by a string name, `ev`, to a `callback` function.
|
57
70
|
// Passing `"all"` will bind the callback to all events fired.
|
58
|
-
bind : function(ev, callback) {
|
71
|
+
bind : function(ev, callback, context) {
|
59
72
|
var calls = this._callbacks || (this._callbacks = {});
|
60
|
-
var list =
|
61
|
-
list.push(callback);
|
73
|
+
var list = calls[ev] || (calls[ev] = []);
|
74
|
+
list.push([callback, context]);
|
62
75
|
return this;
|
63
76
|
},
|
64
77
|
|
@@ -76,8 +89,8 @@
|
|
76
89
|
var list = calls[ev];
|
77
90
|
if (!list) return this;
|
78
91
|
for (var i = 0, l = list.length; i < l; i++) {
|
79
|
-
if (callback === list[i]) {
|
80
|
-
list
|
92
|
+
if (list[i] && callback === list[i][0]) {
|
93
|
+
list[i] = null;
|
81
94
|
break;
|
82
95
|
}
|
83
96
|
}
|
@@ -89,17 +102,21 @@
|
|
89
102
|
// Trigger an event, firing all bound callbacks. Callbacks are passed the
|
90
103
|
// same arguments as `trigger` is, apart from the event name.
|
91
104
|
// Listening for `"all"` passes the true event name as the first argument.
|
92
|
-
trigger : function(
|
93
|
-
var list, calls,
|
105
|
+
trigger : function(eventName) {
|
106
|
+
var list, calls, ev, callback, args;
|
107
|
+
var both = 2;
|
94
108
|
if (!(calls = this._callbacks)) return this;
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
+
}
|
103
120
|
}
|
104
121
|
}
|
105
122
|
return this;
|
@@ -113,12 +130,17 @@
|
|
113
130
|
// Create a new model, with defined attributes. A client id (`cid`)
|
114
131
|
// is automatically generated and assigned for you.
|
115
132
|
Backbone.Model = function(attributes, options) {
|
133
|
+
var defaults;
|
116
134
|
attributes || (attributes = {});
|
117
|
-
if (
|
135
|
+
if (defaults = this.defaults) {
|
136
|
+
if (_.isFunction(defaults)) defaults = defaults.call(this);
|
137
|
+
attributes = _.extend({}, defaults, attributes);
|
138
|
+
}
|
118
139
|
this.attributes = {};
|
119
140
|
this._escapedAttributes = {};
|
120
141
|
this.cid = _.uniqueId('c');
|
121
142
|
this.set(attributes, {silent : true});
|
143
|
+
this._changed = false;
|
122
144
|
this._previousAttributes = _.clone(this.attributes);
|
123
145
|
if (options && options.collection) this.collection = options.collection;
|
124
146
|
this.initialize(attributes, options);
|
@@ -134,6 +156,10 @@
|
|
134
156
|
// Has the item been changed since the last `"change"` event?
|
135
157
|
_changed : false,
|
136
158
|
|
159
|
+
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
160
|
+
// CouchDB users may want to set this to `"_id"`.
|
161
|
+
idAttribute : 'id',
|
162
|
+
|
137
163
|
// Initialize is an empty function by default. Override it with your own
|
138
164
|
// initialization logic.
|
139
165
|
initialize : function(){},
|
@@ -153,7 +179,13 @@
|
|
153
179
|
var html;
|
154
180
|
if (html = this._escapedAttributes[attr]) return html;
|
155
181
|
var val = this.attributes[attr];
|
156
|
-
return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val);
|
182
|
+
return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
|
183
|
+
},
|
184
|
+
|
185
|
+
// Returns `true` if the attribute contains a value that is not null
|
186
|
+
// or undefined.
|
187
|
+
has : function(attr) {
|
188
|
+
return this.attributes[attr] != null;
|
157
189
|
},
|
158
190
|
|
159
191
|
// Set a hash of model attributes on the object, firing `"change"` unless you
|
@@ -170,7 +202,11 @@
|
|
170
202
|
if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
|
171
203
|
|
172
204
|
// Check for changes of `id`.
|
173
|
-
if (
|
205
|
+
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
206
|
+
|
207
|
+
// We're about to start triggering change events.
|
208
|
+
var alreadyChanging = this._changing;
|
209
|
+
this._changing = true;
|
174
210
|
|
175
211
|
// Update attributes.
|
176
212
|
for (var attr in attrs) {
|
@@ -178,21 +214,21 @@
|
|
178
214
|
if (!_.isEqual(now[attr], val)) {
|
179
215
|
now[attr] = val;
|
180
216
|
delete escaped[attr];
|
181
|
-
|
182
|
-
|
183
|
-
this.trigger('change:' + attr, this, val, options);
|
184
|
-
}
|
217
|
+
this._changed = true;
|
218
|
+
if (!options.silent) this.trigger('change:' + attr, this, val, options);
|
185
219
|
}
|
186
220
|
}
|
187
221
|
|
188
222
|
// Fire the `"change"` event, if the model has been changed.
|
189
|
-
if (!options.silent && this._changed) this.change(options);
|
223
|
+
if (!alreadyChanging && !options.silent && this._changed) this.change(options);
|
224
|
+
this._changing = false;
|
190
225
|
return this;
|
191
226
|
},
|
192
227
|
|
193
228
|
// Remove an attribute from the model, firing `"change"` unless you choose
|
194
|
-
// to silence it.
|
229
|
+
// to silence it. `unset` is a noop if the attribute doesn't exist.
|
195
230
|
unset : function(attr, options) {
|
231
|
+
if (!(attr in this.attributes)) return this;
|
196
232
|
options || (options = {});
|
197
233
|
var value = this.attributes[attr];
|
198
234
|
|
@@ -204,8 +240,9 @@
|
|
204
240
|
// Remove the attribute.
|
205
241
|
delete this.attributes[attr];
|
206
242
|
delete this._escapedAttributes[attr];
|
243
|
+
if (attr == this.idAttribute) delete this.id;
|
244
|
+
this._changed = true;
|
207
245
|
if (!options.silent) {
|
208
|
-
this._changed = true;
|
209
246
|
this.trigger('change:' + attr, this, void 0, options);
|
210
247
|
this.change(options);
|
211
248
|
}
|
@@ -216,6 +253,7 @@
|
|
216
253
|
// to silence it.
|
217
254
|
clear : function(options) {
|
218
255
|
options || (options = {});
|
256
|
+
var attr;
|
219
257
|
var old = this.attributes;
|
220
258
|
|
221
259
|
// Run validation.
|
@@ -225,8 +263,8 @@
|
|
225
263
|
|
226
264
|
this.attributes = {};
|
227
265
|
this._escapedAttributes = {};
|
266
|
+
this._changed = true;
|
228
267
|
if (!options.silent) {
|
229
|
-
this._changed = true;
|
230
268
|
for (attr in old) {
|
231
269
|
this.trigger('change:' + attr, this, void 0, options);
|
232
270
|
}
|
@@ -241,13 +279,13 @@
|
|
241
279
|
fetch : function(options) {
|
242
280
|
options || (options = {});
|
243
281
|
var model = this;
|
244
|
-
var success =
|
245
|
-
|
246
|
-
if (
|
282
|
+
var success = options.success;
|
283
|
+
options.success = function(resp, status, xhr) {
|
284
|
+
if (!model.set(model.parse(resp, xhr), options)) return false;
|
285
|
+
if (success) success(model, resp);
|
247
286
|
};
|
248
|
-
|
249
|
-
(this.sync || Backbone.sync)('read', this,
|
250
|
-
return this;
|
287
|
+
options.error = wrapError(options.error, model, options);
|
288
|
+
return (this.sync || Backbone.sync).call(this, 'read', this, options);
|
251
289
|
},
|
252
290
|
|
253
291
|
// Set a hash of model attributes, and sync the model to the server.
|
@@ -257,42 +295,43 @@
|
|
257
295
|
options || (options = {});
|
258
296
|
if (attrs && !this.set(attrs, options)) return false;
|
259
297
|
var model = this;
|
260
|
-
var success =
|
261
|
-
|
262
|
-
if (
|
298
|
+
var success = options.success;
|
299
|
+
options.success = function(resp, status, xhr) {
|
300
|
+
if (!model.set(model.parse(resp, xhr), options)) return false;
|
301
|
+
if (success) success(model, resp, xhr);
|
263
302
|
};
|
264
|
-
|
303
|
+
options.error = wrapError(options.error, model, options);
|
265
304
|
var method = this.isNew() ? 'create' : 'update';
|
266
|
-
(this.sync || Backbone.sync)(method, this,
|
267
|
-
return this;
|
305
|
+
return (this.sync || Backbone.sync).call(this, method, this, options);
|
268
306
|
},
|
269
307
|
|
270
|
-
// Destroy this model on the server. Upon success, the model is removed
|
308
|
+
// Destroy this model on the server if it was already persisted. Upon success, the model is removed
|
271
309
|
// from its collection, if it has one.
|
272
310
|
destroy : function(options) {
|
273
311
|
options || (options = {});
|
312
|
+
if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
|
274
313
|
var model = this;
|
275
|
-
var success =
|
276
|
-
|
277
|
-
|
314
|
+
var success = options.success;
|
315
|
+
options.success = function(resp) {
|
316
|
+
model.trigger('destroy', model, model.collection, options);
|
317
|
+
if (success) success(model, resp);
|
278
318
|
};
|
279
|
-
|
280
|
-
(this.sync || Backbone.sync)('delete', this,
|
281
|
-
return this;
|
319
|
+
options.error = wrapError(options.error, model, options);
|
320
|
+
return (this.sync || Backbone.sync).call(this, 'delete', this, options);
|
282
321
|
},
|
283
322
|
|
284
323
|
// Default URL for the model's representation on the server -- if you're
|
285
324
|
// using Backbone's restful methods, override this to change the endpoint
|
286
325
|
// that will be called.
|
287
326
|
url : function() {
|
288
|
-
var base = getUrl(this.collection);
|
327
|
+
var base = getUrl(this.collection) || this.urlRoot || urlError();
|
289
328
|
if (this.isNew()) return base;
|
290
|
-
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
|
329
|
+
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
|
291
330
|
},
|
292
331
|
|
293
332
|
// **parse** converts a response into the hash of attributes to be `set` on
|
294
333
|
// the model. The default implementation is just to pass the response along.
|
295
|
-
parse : function(resp) {
|
334
|
+
parse : function(resp, xhr) {
|
296
335
|
return resp;
|
297
336
|
},
|
298
337
|
|
@@ -301,10 +340,9 @@
|
|
301
340
|
return new this.constructor(this);
|
302
341
|
},
|
303
342
|
|
304
|
-
// A model is new if it has never been saved to the server, and
|
305
|
-
// ID.
|
343
|
+
// A model is new if it has never been saved to the server, and lacks an id.
|
306
344
|
isNew : function() {
|
307
|
-
return
|
345
|
+
return this.id == null;
|
308
346
|
},
|
309
347
|
|
310
348
|
// Call this method to manually fire a `change` event for this model.
|
@@ -359,7 +397,7 @@
|
|
359
397
|
var error = this.validate(attrs);
|
360
398
|
if (error) {
|
361
399
|
if (options.error) {
|
362
|
-
options.error(this, error);
|
400
|
+
options.error(this, error, options);
|
363
401
|
} else {
|
364
402
|
this.trigger('error', this, error, options);
|
365
403
|
}
|
@@ -378,14 +416,11 @@
|
|
378
416
|
// its models in sort order, as they're added and removed.
|
379
417
|
Backbone.Collection = function(models, options) {
|
380
418
|
options || (options = {});
|
381
|
-
if (options.comparator)
|
382
|
-
|
383
|
-
delete options.comparator;
|
384
|
-
}
|
385
|
-
this._boundOnModelEvent = _.bind(this._onModelEvent, this);
|
419
|
+
if (options.comparator) this.comparator = options.comparator;
|
420
|
+
_.bindAll(this, '_onModelEvent', '_removeReference');
|
386
421
|
this._reset();
|
387
|
-
if (models) this.
|
388
|
-
this.initialize(
|
422
|
+
if (models) this.reset(models, {silent: true});
|
423
|
+
this.initialize.apply(this, arguments);
|
389
424
|
};
|
390
425
|
|
391
426
|
// Define the Collection's inheritable methods.
|
@@ -453,7 +488,7 @@
|
|
453
488
|
options || (options = {});
|
454
489
|
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
|
455
490
|
this.models = this.sortBy(this.comparator);
|
456
|
-
if (!options.silent) this.trigger('
|
491
|
+
if (!options.silent) this.trigger('reset', this, options);
|
457
492
|
return this;
|
458
493
|
},
|
459
494
|
|
@@ -463,51 +498,53 @@
|
|
463
498
|
},
|
464
499
|
|
465
500
|
// When you have more items than you want to add or remove individually,
|
466
|
-
// you can
|
467
|
-
// any `added` or `removed` events. Fires `
|
468
|
-
|
501
|
+
// you can reset the entire set with a new list of models, without firing
|
502
|
+
// any `added` or `removed` events. Fires `reset` when finished.
|
503
|
+
reset : function(models, options) {
|
469
504
|
models || (models = []);
|
470
505
|
options || (options = {});
|
506
|
+
this.each(this._removeReference);
|
471
507
|
this._reset();
|
472
508
|
this.add(models, {silent: true});
|
473
|
-
if (!options.silent) this.trigger('
|
509
|
+
if (!options.silent) this.trigger('reset', this, options);
|
474
510
|
return this;
|
475
511
|
},
|
476
512
|
|
477
|
-
// Fetch the default set of models for this collection,
|
478
|
-
// collection when they arrive.
|
513
|
+
// Fetch the default set of models for this collection, resetting the
|
514
|
+
// collection when they arrive. If `add: true` is passed, appends the
|
515
|
+
// models to the collection instead of resetting.
|
479
516
|
fetch : function(options) {
|
480
517
|
options || (options = {});
|
481
518
|
var collection = this;
|
482
|
-
var success =
|
483
|
-
|
484
|
-
|
519
|
+
var success = options.success;
|
520
|
+
options.success = function(resp, status, xhr) {
|
521
|
+
collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
|
522
|
+
if (success) success(collection, resp);
|
485
523
|
};
|
486
|
-
|
487
|
-
(this.sync || Backbone.sync)('read', this,
|
488
|
-
return this;
|
524
|
+
options.error = wrapError(options.error, collection, options);
|
525
|
+
return (this.sync || Backbone.sync).call(this, 'read', this, options);
|
489
526
|
},
|
490
527
|
|
491
528
|
// Create a new instance of a model in this collection. After the model
|
492
529
|
// has been created on the server, it will be added to the collection.
|
530
|
+
// Returns the model, or 'false' if validation on a new model fails.
|
493
531
|
create : function(model, options) {
|
494
532
|
var coll = this;
|
495
533
|
options || (options = {});
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
coll.add(nextModel);
|
503
|
-
if (options.success) options.success(nextModel, resp);
|
534
|
+
model = this._prepareModel(model, options);
|
535
|
+
if (!model) return false;
|
536
|
+
var success = options.success;
|
537
|
+
options.success = function(nextModel, resp, xhr) {
|
538
|
+
coll.add(nextModel, options);
|
539
|
+
if (success) success(nextModel, resp, xhr);
|
504
540
|
};
|
505
|
-
|
541
|
+
model.save(null, options);
|
542
|
+
return model;
|
506
543
|
},
|
507
544
|
|
508
545
|
// **parse** converts a response into a list of models to be added to the
|
509
546
|
// collection. The default implementation is just to pass it through.
|
510
|
-
parse : function(resp) {
|
547
|
+
parse : function(resp, xhr) {
|
511
548
|
return resp;
|
512
549
|
},
|
513
550
|
|
@@ -518,7 +555,7 @@
|
|
518
555
|
return _(this.models).chain();
|
519
556
|
},
|
520
557
|
|
521
|
-
// Reset all internal state. Called when the collection is
|
558
|
+
// Reset all internal state. Called when the collection is reset.
|
522
559
|
_reset : function(options) {
|
523
560
|
this.length = 0;
|
524
561
|
this.models = [];
|
@@ -526,21 +563,34 @@
|
|
526
563
|
this._byCid = {};
|
527
564
|
},
|
528
565
|
|
566
|
+
// Prepare a model to be added to this collection
|
567
|
+
_prepareModel: function(model, options) {
|
568
|
+
if (!(model instanceof Backbone.Model)) {
|
569
|
+
var attrs = model;
|
570
|
+
model = new this.model(attrs, {collection: this});
|
571
|
+
if (model.validate && !model._performValidation(attrs, options)) model = false;
|
572
|
+
} else if (!model.collection) {
|
573
|
+
model.collection = this;
|
574
|
+
}
|
575
|
+
return model;
|
576
|
+
},
|
577
|
+
|
529
578
|
// Internal implementation of adding a single model to the set, updating
|
530
579
|
// hash indexes for `id` and `cid` lookups.
|
580
|
+
// Returns the model, or 'false' if validation on a new model fails.
|
531
581
|
_add : function(model, options) {
|
532
582
|
options || (options = {});
|
533
|
-
|
534
|
-
|
535
|
-
}
|
583
|
+
model = this._prepareModel(model, options);
|
584
|
+
if (!model) return false;
|
536
585
|
var already = this.getByCid(model);
|
537
586
|
if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
|
538
587
|
this._byId[model.id] = model;
|
539
588
|
this._byCid[model.cid] = model;
|
540
|
-
|
541
|
-
|
589
|
+
var index = options.at != null ? options.at :
|
590
|
+
this.comparator ? this.sortedIndex(model, this.comparator) :
|
591
|
+
this.length;
|
542
592
|
this.models.splice(index, 0, model);
|
543
|
-
model.bind('all', this.
|
593
|
+
model.bind('all', this._onModelEvent);
|
544
594
|
this.length++;
|
545
595
|
if (!options.silent) model.trigger('add', model, this, options);
|
546
596
|
return model;
|
@@ -554,20 +604,32 @@
|
|
554
604
|
if (!model) return null;
|
555
605
|
delete this._byId[model.id];
|
556
606
|
delete this._byCid[model.cid];
|
557
|
-
delete model.collection;
|
558
607
|
this.models.splice(this.indexOf(model), 1);
|
559
608
|
this.length--;
|
560
609
|
if (!options.silent) model.trigger('remove', model, this, options);
|
561
|
-
|
610
|
+
this._removeReference(model);
|
562
611
|
return model;
|
563
612
|
},
|
564
613
|
|
614
|
+
// Internal method to remove a model's ties to a collection.
|
615
|
+
_removeReference : function(model) {
|
616
|
+
if (this == model.collection) {
|
617
|
+
delete model.collection;
|
618
|
+
}
|
619
|
+
model.unbind('all', this._onModelEvent);
|
620
|
+
},
|
621
|
+
|
565
622
|
// Internal method called every time a model in the set fires an event.
|
566
623
|
// Sets need to update their indexes when models change ids. All other
|
567
|
-
// events simply proxy through.
|
568
|
-
|
569
|
-
|
570
|
-
|
624
|
+
// events simply proxy through. "add" and "remove" events that originate
|
625
|
+
// in other collections are ignored.
|
626
|
+
_onModelEvent : function(ev, model, collection, options) {
|
627
|
+
if ((ev == 'add' || ev == 'remove') && collection != this) return;
|
628
|
+
if (ev == 'destroy') {
|
629
|
+
this._remove(model, options);
|
630
|
+
}
|
631
|
+
if (model && ev === 'change:' + model.idAttribute) {
|
632
|
+
delete this._byId[model.previous(model.idAttribute)];
|
571
633
|
this._byId[model.id] = model;
|
572
634
|
}
|
573
635
|
this.trigger.apply(this, arguments);
|
@@ -578,8 +640,8 @@
|
|
578
640
|
// Underscore methods that we want to implement on the Collection.
|
579
641
|
var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
|
580
642
|
'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
|
581
|
-
'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
|
582
|
-
'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];
|
643
|
+
'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
|
644
|
+
'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
|
583
645
|
|
584
646
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
585
647
|
_.each(methods, function(method) {
|
@@ -588,25 +650,26 @@
|
|
588
650
|
};
|
589
651
|
});
|
590
652
|
|
591
|
-
// Backbone.
|
653
|
+
// Backbone.Router
|
592
654
|
// -------------------
|
593
655
|
|
594
|
-
//
|
656
|
+
// Routers map faux-URLs to actions, and fire events when routes are
|
595
657
|
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
596
|
-
Backbone.
|
658
|
+
Backbone.Router = function(options) {
|
597
659
|
options || (options = {});
|
598
660
|
if (options.routes) this.routes = options.routes;
|
599
661
|
this._bindRoutes();
|
600
|
-
this.initialize(
|
662
|
+
this.initialize.apply(this, arguments);
|
601
663
|
};
|
602
664
|
|
603
665
|
// Cached regular expressions for matching named param parts and splatted
|
604
666
|
// parts of route strings.
|
605
|
-
var namedParam
|
606
|
-
var splatParam
|
667
|
+
var namedParam = /:([\w\d]+)/g;
|
668
|
+
var splatParam = /\*([\w\d]+)/g;
|
669
|
+
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
|
607
670
|
|
608
|
-
// Set up all inheritable **Backbone.
|
609
|
-
_.extend(Backbone.
|
671
|
+
// Set up all inheritable **Backbone.Router** properties and methods.
|
672
|
+
_.extend(Backbone.Router.prototype, Backbone.Events, {
|
610
673
|
|
611
674
|
// Initialize is an empty function by default. Override it with your own
|
612
675
|
// initialization logic.
|
@@ -628,25 +691,31 @@
|
|
628
691
|
}, this));
|
629
692
|
},
|
630
693
|
|
631
|
-
// Simple proxy to `Backbone.history` to save a fragment into the history
|
632
|
-
|
633
|
-
|
634
|
-
Backbone.history.saveLocation(fragment);
|
694
|
+
// Simple proxy to `Backbone.history` to save a fragment into the history.
|
695
|
+
navigate : function(fragment, triggerRoute) {
|
696
|
+
Backbone.history.navigate(fragment, triggerRoute);
|
635
697
|
},
|
636
698
|
|
637
|
-
// Bind all defined routes to `Backbone.history`.
|
699
|
+
// Bind all defined routes to `Backbone.history`. We have to reverse the
|
700
|
+
// order of the routes here to support behavior where the most general
|
701
|
+
// routes can be defined at the bottom of the route map.
|
638
702
|
_bindRoutes : function() {
|
639
703
|
if (!this.routes) return;
|
704
|
+
var routes = [];
|
640
705
|
for (var route in this.routes) {
|
641
|
-
|
642
|
-
|
706
|
+
routes.unshift([route, this.routes[route]]);
|
707
|
+
}
|
708
|
+
for (var i = 0, l = routes.length; i < l; i++) {
|
709
|
+
this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
|
643
710
|
}
|
644
711
|
},
|
645
712
|
|
646
713
|
// Convert a route string into a regular expression, suitable for matching
|
647
|
-
// against the current location
|
714
|
+
// against the current location hash.
|
648
715
|
_routeToRegExp : function(route) {
|
649
|
-
route = route.replace(
|
716
|
+
route = route.replace(escapeRegExp, "\\$&")
|
717
|
+
.replace(namedParam, "([^\/]*)")
|
718
|
+
.replace(splatParam, "(.*?)");
|
650
719
|
return new RegExp('^' + route + '$');
|
651
720
|
},
|
652
721
|
|
@@ -661,17 +730,22 @@
|
|
661
730
|
// Backbone.History
|
662
731
|
// ----------------
|
663
732
|
|
664
|
-
// Handles cross-browser history management, based on URL
|
733
|
+
// Handles cross-browser history management, based on URL fragments. If the
|
665
734
|
// browser does not support `onhashchange`, falls back to polling.
|
666
735
|
Backbone.History = function() {
|
667
736
|
this.handlers = [];
|
668
|
-
this.fragment = this.getFragment();
|
669
737
|
_.bindAll(this, 'checkUrl');
|
670
738
|
};
|
671
739
|
|
672
740
|
// Cached regex for cleaning hashes.
|
673
741
|
var hashStrip = /^#*/;
|
674
742
|
|
743
|
+
// Cached regex for detecting MSIE.
|
744
|
+
var isExplorer = /msie [\w.]+/;
|
745
|
+
|
746
|
+
// Has the history handling already been started?
|
747
|
+
var historyStarted = false;
|
748
|
+
|
675
749
|
// Set up all inheritable **Backbone.History** properties and methods.
|
676
750
|
_.extend(Backbone.History.prototype, {
|
677
751
|
|
@@ -679,53 +753,92 @@
|
|
679
753
|
// twenty times a second.
|
680
754
|
interval: 50,
|
681
755
|
|
682
|
-
// Get the cross-browser normalized URL fragment
|
683
|
-
|
684
|
-
|
756
|
+
// Get the cross-browser normalized URL fragment, either from the URL,
|
757
|
+
// the hash, or the override.
|
758
|
+
getFragment : function(fragment, forcePushState) {
|
759
|
+
if (fragment == null) {
|
760
|
+
if (this._hasPushState || forcePushState) {
|
761
|
+
fragment = window.location.pathname;
|
762
|
+
var search = window.location.search;
|
763
|
+
if (search) fragment += search;
|
764
|
+
if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
|
765
|
+
} else {
|
766
|
+
fragment = window.location.hash;
|
767
|
+
}
|
768
|
+
}
|
769
|
+
return decodeURIComponent(fragment.replace(hashStrip, ''));
|
685
770
|
},
|
686
771
|
|
687
772
|
// Start the hash change handling, returning `true` if the current URL matches
|
688
773
|
// an existing route, and `false` otherwise.
|
689
|
-
start : function() {
|
690
|
-
|
691
|
-
|
774
|
+
start : function(options) {
|
775
|
+
|
776
|
+
// Figure out the initial configuration. Do we need an iframe?
|
777
|
+
// Is pushState desired ... is it available?
|
778
|
+
if (historyStarted) throw new Error("Backbone.history has already been started");
|
779
|
+
this.options = _.extend({}, {root: '/'}, this.options, options);
|
780
|
+
this._wantsPushState = !!this.options.pushState;
|
781
|
+
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
|
782
|
+
var fragment = this.getFragment();
|
783
|
+
var docMode = document.documentMode;
|
784
|
+
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
692
785
|
if (oldIE) {
|
693
786
|
this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
787
|
+
this.navigate(fragment);
|
694
788
|
}
|
695
|
-
|
789
|
+
|
790
|
+
// Depending on whether we're using pushState or hashes, and whether
|
791
|
+
// 'onhashchange' is supported, determine how we check the URL state.
|
792
|
+
if (this._hasPushState) {
|
793
|
+
$(window).bind('popstate', this.checkUrl);
|
794
|
+
} else if ('onhashchange' in window && !oldIE) {
|
696
795
|
$(window).bind('hashchange', this.checkUrl);
|
697
796
|
} else {
|
698
797
|
setInterval(this.checkUrl, this.interval);
|
699
798
|
}
|
700
|
-
|
799
|
+
|
800
|
+
// Determine if we need to change the base url, for a pushState link
|
801
|
+
// opened by a non-pushState browser.
|
802
|
+
this.fragment = fragment;
|
803
|
+
historyStarted = true;
|
804
|
+
var loc = window.location;
|
805
|
+
var atRoot = loc.pathname == this.options.root;
|
806
|
+
if (this._wantsPushState && !this._hasPushState && !atRoot) {
|
807
|
+
this.fragment = this.getFragment(null, true);
|
808
|
+
window.location.replace(this.options.root + '#' + this.fragment);
|
809
|
+
// Return immediately as browser will do redirect to new url
|
810
|
+
return true;
|
811
|
+
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
812
|
+
this.fragment = loc.hash.replace(hashStrip, '');
|
813
|
+
window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
|
814
|
+
}
|
815
|
+
|
816
|
+
if (!this.options.silent) {
|
817
|
+
return this.loadUrl();
|
818
|
+
}
|
701
819
|
},
|
702
820
|
|
703
|
-
// Add a route to be tested when the
|
704
|
-
//
|
821
|
+
// Add a route to be tested when the fragment changes. Routes added later may
|
822
|
+
// override previous routes.
|
705
823
|
route : function(route, callback) {
|
706
|
-
this.handlers.
|
824
|
+
this.handlers.unshift({route : route, callback : callback});
|
707
825
|
},
|
708
826
|
|
709
827
|
// Checks the current URL to see if it has changed, and if it has,
|
710
828
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
711
|
-
checkUrl : function() {
|
829
|
+
checkUrl : function(e) {
|
712
830
|
var current = this.getFragment();
|
713
|
-
if (current == this.fragment && this.iframe)
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
current == decodeURIComponent(this.fragment)) return false;
|
718
|
-
if (this.iframe) {
|
719
|
-
window.location.hash = this.iframe.location.hash = current;
|
720
|
-
}
|
721
|
-
this.loadUrl();
|
831
|
+
if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
|
832
|
+
if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
|
833
|
+
if (this.iframe) this.navigate(current);
|
834
|
+
this.loadUrl() || this.loadUrl(window.location.hash);
|
722
835
|
},
|
723
836
|
|
724
837
|
// Attempt to load the current URL fragment. If a route succeeds with a
|
725
838
|
// match, returns `true`. If no defined routes matches the fragment,
|
726
839
|
// returns `false`.
|
727
|
-
loadUrl : function() {
|
728
|
-
var fragment = this.fragment = this.getFragment();
|
840
|
+
loadUrl : function(fragmentOverride) {
|
841
|
+
var fragment = this.fragment = this.getFragment(fragmentOverride);
|
729
842
|
var matched = _.any(this.handlers, function(handler) {
|
730
843
|
if (handler.route.test(fragment)) {
|
731
844
|
handler.callback(fragment);
|
@@ -738,14 +851,22 @@
|
|
738
851
|
// Save a fragment into the hash history. You are responsible for properly
|
739
852
|
// URL-encoding the fragment in advance. This does not trigger
|
740
853
|
// a `hashchange` event.
|
741
|
-
|
742
|
-
|
743
|
-
if (this.fragment == fragment) return;
|
744
|
-
|
745
|
-
|
746
|
-
this.
|
747
|
-
this.
|
854
|
+
navigate : function(fragment, triggerRoute) {
|
855
|
+
var frag = (fragment || '').replace(hashStrip, '');
|
856
|
+
if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
|
857
|
+
if (this._hasPushState) {
|
858
|
+
var loc = window.location;
|
859
|
+
if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
|
860
|
+
this.fragment = frag;
|
861
|
+
window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
|
862
|
+
} else {
|
863
|
+
window.location.hash = this.fragment = frag;
|
864
|
+
if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
|
865
|
+
this.iframe.document.open().close();
|
866
|
+
this.iframe.location.hash = frag;
|
867
|
+
}
|
748
868
|
}
|
869
|
+
if (triggerRoute) this.loadUrl(fragment);
|
749
870
|
}
|
750
871
|
|
751
872
|
});
|
@@ -756,10 +877,11 @@
|
|
756
877
|
// Creating a Backbone.View creates its initial element outside of the DOM,
|
757
878
|
// if an existing element is not provided...
|
758
879
|
Backbone.View = function(options) {
|
880
|
+
this.cid = _.uniqueId('view');
|
759
881
|
this._configure(options || {});
|
760
882
|
this._ensureElement();
|
761
883
|
this.delegateEvents();
|
762
|
-
this.initialize(
|
884
|
+
this.initialize.apply(this, arguments);
|
763
885
|
};
|
764
886
|
|
765
887
|
// Element lookup, scoped to DOM elements within the current view.
|
@@ -770,7 +892,10 @@
|
|
770
892
|
};
|
771
893
|
|
772
894
|
// Cached regex to split keys for `delegate`.
|
773
|
-
var eventSplitter = /^(\
|
895
|
+
var eventSplitter = /^(\S+)\s*(.*)$/;
|
896
|
+
|
897
|
+
// List of view options to be merged as properties.
|
898
|
+
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
774
899
|
|
775
900
|
// Set up all inheritable **Backbone.View** properties and methods.
|
776
901
|
_.extend(Backbone.View.prototype, Backbone.Events, {
|
@@ -827,12 +952,15 @@
|
|
827
952
|
// not `change`, `submit`, and `reset` in Internet Explorer.
|
828
953
|
delegateEvents : function(events) {
|
829
954
|
if (!(events || (events = this.events))) return;
|
830
|
-
|
955
|
+
if (_.isFunction(events)) events = events.call(this);
|
956
|
+
$(this.el).unbind('.delegateEvents' + this.cid);
|
831
957
|
for (var key in events) {
|
832
|
-
var
|
958
|
+
var method = this[events[key]];
|
959
|
+
if (!method) throw new Error('Event "' + events[key] + '" does not exist');
|
833
960
|
var match = key.match(eventSplitter);
|
834
961
|
var eventName = match[1], selector = match[2];
|
835
|
-
|
962
|
+
method = _.bind(method, this);
|
963
|
+
eventName += '.delegateEvents' + this.cid;
|
836
964
|
if (selector === '') {
|
837
965
|
$(this.el).bind(eventName, method);
|
838
966
|
} else {
|
@@ -846,22 +974,26 @@
|
|
846
974
|
// attached directly to the view.
|
847
975
|
_configure : function(options) {
|
848
976
|
if (this.options) options = _.extend({}, this.options, options);
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
if (options.className) this.className = options.className;
|
854
|
-
if (options.tagName) this.tagName = options.tagName;
|
977
|
+
for (var i = 0, l = viewOptions.length; i < l; i++) {
|
978
|
+
var attr = viewOptions[i];
|
979
|
+
if (options[attr]) this[attr] = options[attr];
|
980
|
+
}
|
855
981
|
this.options = options;
|
856
982
|
},
|
857
983
|
|
858
984
|
// Ensure that the View has a DOM element to render into.
|
985
|
+
// If `this.el` is a string, pass it through `$()`, take the first
|
986
|
+
// matching element, and re-assign it to `el`. Otherwise, create
|
987
|
+
// an element from the `id`, `className` and `tagName` proeprties.
|
859
988
|
_ensureElement : function() {
|
860
|
-
if (this.el)
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
989
|
+
if (!this.el) {
|
990
|
+
var attrs = this.attributes || {};
|
991
|
+
if (this.id) attrs.id = this.id;
|
992
|
+
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);
|
996
|
+
}
|
865
997
|
}
|
866
998
|
|
867
999
|
});
|
@@ -869,13 +1001,13 @@
|
|
869
1001
|
// The self-propagating extend function that Backbone classes use.
|
870
1002
|
var extend = function (protoProps, classProps) {
|
871
1003
|
var child = inherits(this, protoProps, classProps);
|
872
|
-
child.extend = extend;
|
1004
|
+
child.extend = this.extend;
|
873
1005
|
return child;
|
874
1006
|
};
|
875
1007
|
|
876
1008
|
// Set up inheritance for the model, collection, and view.
|
877
1009
|
Backbone.Model.extend = Backbone.Collection.extend =
|
878
|
-
Backbone.
|
1010
|
+
Backbone.Router.extend = Backbone.View.extend = extend;
|
879
1011
|
|
880
1012
|
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
881
1013
|
var methodMap = {
|
@@ -903,28 +1035,30 @@
|
|
903
1035
|
// `application/json` with the model in a param named `model`.
|
904
1036
|
// Useful when interfacing with server-side languages like **PHP** that make
|
905
1037
|
// it difficult to read the body of `PUT` requests.
|
906
|
-
Backbone.sync = function(method, model,
|
1038
|
+
Backbone.sync = function(method, model, options) {
|
907
1039
|
var type = methodMap[method];
|
908
|
-
var modelJSON = (method === 'create' || method === 'update') ?
|
909
|
-
JSON.stringify(model.toJSON()) : null;
|
910
1040
|
|
911
1041
|
// Default JSON-request options.
|
912
|
-
var params = {
|
913
|
-
url: getUrl(model),
|
1042
|
+
var params = _.extend({
|
914
1043
|
type: type,
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
}
|
1044
|
+
dataType: 'json'
|
1045
|
+
}, options);
|
1046
|
+
|
1047
|
+
// Ensure that we have a URL.
|
1048
|
+
if (!params.url) {
|
1049
|
+
params.url = getUrl(model) || urlError();
|
1050
|
+
}
|
1051
|
+
|
1052
|
+
// Ensure that we have the appropriate request data.
|
1053
|
+
if (!params.data && model && (method == 'create' || method == 'update')) {
|
1054
|
+
params.contentType = 'application/json';
|
1055
|
+
params.data = JSON.stringify(model.toJSON());
|
1056
|
+
}
|
922
1057
|
|
923
1058
|
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
924
1059
|
if (Backbone.emulateJSON) {
|
925
1060
|
params.contentType = 'application/x-www-form-urlencoded';
|
926
|
-
params.
|
927
|
-
params.data = modelJSON ? {model : modelJSON} : {};
|
1061
|
+
params.data = params.data ? {model : params.data} : {};
|
928
1062
|
}
|
929
1063
|
|
930
1064
|
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
@@ -934,13 +1068,18 @@
|
|
934
1068
|
if (Backbone.emulateJSON) params.data._method = type;
|
935
1069
|
params.type = 'POST';
|
936
1070
|
params.beforeSend = function(xhr) {
|
937
|
-
xhr.setRequestHeader(
|
1071
|
+
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
938
1072
|
};
|
939
1073
|
}
|
940
1074
|
}
|
941
1075
|
|
1076
|
+
// Don't process data on a non-GET request.
|
1077
|
+
if (params.type !== 'GET' && !Backbone.emulateJSON) {
|
1078
|
+
params.processData = false;
|
1079
|
+
}
|
1080
|
+
|
942
1081
|
// Make the request.
|
943
|
-
$.ajax(params);
|
1082
|
+
return $.ajax(params);
|
944
1083
|
};
|
945
1084
|
|
946
1085
|
// Helpers
|
@@ -964,6 +1103,9 @@
|
|
964
1103
|
child = function(){ return parent.apply(this, arguments); };
|
965
1104
|
}
|
966
1105
|
|
1106
|
+
// Inherit class (static) properties from parent.
|
1107
|
+
_.extend(child, parent);
|
1108
|
+
|
967
1109
|
// Set the prototype chain to inherit from `parent`, without calling
|
968
1110
|
// `parent`'s constructor function.
|
969
1111
|
ctor.prototype = parent.prototype;
|
@@ -976,7 +1118,7 @@
|
|
976
1118
|
// Add static properties to the constructor function, if supplied.
|
977
1119
|
if (staticProps) _.extend(child, staticProps);
|
978
1120
|
|
979
|
-
// Correctly set child's `prototype.constructor
|
1121
|
+
// Correctly set child's `prototype.constructor`.
|
980
1122
|
child.prototype.constructor = child;
|
981
1123
|
|
982
1124
|
// Set a convenience property in case the parent's prototype is needed later.
|
@@ -988,15 +1130,20 @@
|
|
988
1130
|
// Helper function to get a URL from a Model or Collection as a property
|
989
1131
|
// or as a function.
|
990
1132
|
var getUrl = function(object) {
|
991
|
-
if (!(object && object.url))
|
1133
|
+
if (!(object && object.url)) return null;
|
992
1134
|
return _.isFunction(object.url) ? object.url() : object.url;
|
993
1135
|
};
|
994
1136
|
|
1137
|
+
// Throw an error when a URL is needed, and none is supplied.
|
1138
|
+
var urlError = function() {
|
1139
|
+
throw new Error('A "url" property or function must be specified');
|
1140
|
+
};
|
1141
|
+
|
995
1142
|
// Wrap an optional error callback with a fallback error event.
|
996
1143
|
var wrapError = function(onError, model, options) {
|
997
1144
|
return function(resp) {
|
998
1145
|
if (onError) {
|
999
|
-
onError(model, resp);
|
1146
|
+
onError(model, resp, options);
|
1000
1147
|
} else {
|
1001
1148
|
model.trigger('error', model, resp, options);
|
1002
1149
|
}
|
@@ -1005,7 +1152,7 @@
|
|
1005
1152
|
|
1006
1153
|
// Helper function to escape a string for HTML rendering.
|
1007
1154
|
var escapeHTML = function(string) {
|
1008
|
-
return string.replace(/&(?!\w+;)/
|
1155
|
+
return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
1009
1156
|
};
|
1010
1157
|
|
1011
|
-
})();
|
1158
|
+
}).call(this);
|