backbone-rails 0.3.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/vendor/assets/javascripts/backbone.js +325 -187
- data/vendor/assets/javascripts/backbone.js.old +1011 -0
- metadata +4 -5
@@ -1,4 +1,4 @@
|
|
1
|
-
// Backbone.js 0.
|
1
|
+
// Backbone.js 0.5.0
|
2
2
|
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
3
3
|
// Backbone may be freely distributed under the MIT license.
|
4
4
|
// For all details and documentation:
|
@@ -9,24 +9,37 @@
|
|
9
9
|
// Initial Setup
|
10
10
|
// -------------
|
11
11
|
|
12
|
+
// Save a reference to the global object.
|
13
|
+
var root = this;
|
14
|
+
|
15
|
+
// Save the previous value of the `Backbone` variable.
|
16
|
+
var previousBackbone = root.Backbone;
|
17
|
+
|
12
18
|
// The top-level namespace. All public Backbone classes and modules will
|
13
19
|
// be attached to this. Exported for both CommonJS and the browser.
|
14
20
|
var Backbone;
|
15
21
|
if (typeof exports !== 'undefined') {
|
16
22
|
Backbone = exports;
|
17
23
|
} else {
|
18
|
-
Backbone =
|
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.0';
|
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
44
|
// Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will
|
32
45
|
// fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
|
@@ -57,7 +70,7 @@
|
|
57
70
|
// Passing `"all"` will bind the callback to all events fired.
|
58
71
|
bind : function(ev, callback) {
|
59
72
|
var calls = this._callbacks || (this._callbacks = {});
|
60
|
-
var list =
|
73
|
+
var list = calls[ev] || (calls[ev] = []);
|
61
74
|
list.push(callback);
|
62
75
|
return this;
|
63
76
|
},
|
@@ -77,7 +90,7 @@
|
|
77
90
|
if (!list) return this;
|
78
91
|
for (var i = 0, l = list.length; i < l; i++) {
|
79
92
|
if (callback === list[i]) {
|
80
|
-
list
|
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.apply(this, args);
|
118
|
+
}
|
119
|
+
}
|
103
120
|
}
|
104
121
|
}
|
105
122
|
return this;
|
@@ -113,15 +130,20 @@
|
|
113
130
|
// Create a new model, with defined attributes. A client id (`cid`)
|
114
131
|
// is automatically generated and assigned for you.
|
115
132
|
Backbone.Model = function(attributes, options) {
|
133
|
+
var defaults;
|
116
134
|
attributes || (attributes = {});
|
117
|
-
if (
|
135
|
+
if (defaults = this.defaults) {
|
136
|
+
if (_.isFunction(defaults)) defaults = defaults();
|
137
|
+
attributes = _.extend({}, defaults, attributes);
|
138
|
+
}
|
118
139
|
this.attributes = {};
|
119
140
|
this._escapedAttributes = {};
|
120
141
|
this.cid = _.uniqueId('c');
|
121
142
|
this.set(attributes, {silent : true});
|
143
|
+
this._changed = false;
|
122
144
|
this._previousAttributes = _.clone(this.attributes);
|
123
145
|
if (options && options.collection) this.collection = options.collection;
|
124
|
-
this.initialize(
|
146
|
+
this.initialize.apply(this, arguments);
|
125
147
|
};
|
126
148
|
|
127
149
|
// Attach all inheritable methods to the Model prototype.
|
@@ -134,6 +156,10 @@
|
|
134
156
|
// Has the item been changed since the last `"change"` event?
|
135
157
|
_changed : false,
|
136
158
|
|
159
|
+
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
160
|
+
// CouchDB users may want to set this to `"_id"`.
|
161
|
+
idAttribute : 'id',
|
162
|
+
|
137
163
|
// Initialize is an empty function by default. Override it with your own
|
138
164
|
// initialization logic.
|
139
165
|
initialize : function(){},
|
@@ -153,7 +179,13 @@
|
|
153
179
|
var html;
|
154
180
|
if (html = this._escapedAttributes[attr]) return html;
|
155
181
|
var val = this.attributes[attr];
|
156
|
-
return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val);
|
182
|
+
return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
|
183
|
+
},
|
184
|
+
|
185
|
+
// Returns `true` if the attribute contains a value that is not null
|
186
|
+
// or undefined.
|
187
|
+
has : function(attr) {
|
188
|
+
return this.attributes[attr] != null;
|
157
189
|
},
|
158
190
|
|
159
191
|
// Set a hash of model attributes on the object, firing `"change"` unless you
|
@@ -170,7 +202,11 @@
|
|
170
202
|
if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
|
171
203
|
|
172
204
|
// Check for changes of `id`.
|
173
|
-
if (
|
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
|
-
|
536
|
-
var already = this.getByCid(model);
|
583
|
+
model = this._prepareModel(model, options);
|
584
|
+
if (!model) return false;
|
585
|
+
var already = this.getByCid(model) || this.get(model);
|
537
586
|
if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
|
538
587
|
this._byId[model.id] = model;
|
539
588
|
this._byCid[model.cid] = model;
|
540
|
-
|
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);
|
@@ -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,16 +730,21 @@
|
|
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
|
-
var hashStrip =
|
741
|
+
var hashStrip = /^#*!?/;
|
742
|
+
|
743
|
+
// Cached regex for detecting MSIE.
|
744
|
+
var isExplorer = /msie [\w.]+/;
|
745
|
+
|
746
|
+
// Has the history handling already been started?
|
747
|
+
var historyStarted = false;
|
674
748
|
|
675
749
|
// Set up all inheritable **Backbone.History** properties and methods.
|
676
750
|
_.extend(Backbone.History.prototype, {
|
@@ -679,53 +753,87 @@
|
|
679
753
|
// twenty times a second.
|
680
754
|
interval: 50,
|
681
755
|
|
682
|
-
// Get the cross-browser normalized URL fragment
|
683
|
-
|
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 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 started = this.loadUrl() || this.loadUrl(window.location.hash);
|
805
|
+
var atRoot = window.location.pathname == this.options.root;
|
806
|
+
if (this._wantsPushState && !this._hasPushState && !atRoot) {
|
807
|
+
this.fragment = this.getFragment(null, true);
|
808
|
+
window.location = this.options.root + '#' + this.fragment;
|
809
|
+
} else if (this._wantsPushState && this._hasPushState && atRoot && window.location.hash) {
|
810
|
+
this.navigate(window.location.hash);
|
811
|
+
} else {
|
812
|
+
return started;
|
813
|
+
}
|
701
814
|
},
|
702
815
|
|
703
|
-
// Add a route to be tested when the
|
704
|
-
//
|
816
|
+
// Add a route to be tested when the fragment changes. Routes added later may
|
817
|
+
// override previous routes.
|
705
818
|
route : function(route, callback) {
|
706
|
-
this.handlers.
|
819
|
+
this.handlers.unshift({route : route, callback : callback});
|
707
820
|
},
|
708
821
|
|
709
822
|
// Checks the current URL to see if it has changed, and if it has,
|
710
823
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
711
|
-
checkUrl : function() {
|
824
|
+
checkUrl : function(e) {
|
712
825
|
var current = this.getFragment();
|
713
|
-
if (current == this.fragment && this.iframe)
|
714
|
-
|
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();
|
826
|
+
if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
|
827
|
+
if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
|
828
|
+
if (this.iframe) this.navigate(current);
|
829
|
+
this.loadUrl() || this.loadUrl(window.location.hash);
|
722
830
|
},
|
723
831
|
|
724
832
|
// Attempt to load the current URL fragment. If a route succeeds with a
|
725
833
|
// match, returns `true`. If no defined routes matches the fragment,
|
726
834
|
// returns `false`.
|
727
|
-
loadUrl : function() {
|
728
|
-
var fragment = this.fragment = this.getFragment();
|
835
|
+
loadUrl : function(fragmentOverride) {
|
836
|
+
var fragment = this.fragment = this.getFragment(fragmentOverride);
|
729
837
|
var matched = _.any(this.handlers, function(handler) {
|
730
838
|
if (handler.route.test(fragment)) {
|
731
839
|
handler.callback(fragment);
|
@@ -738,14 +846,22 @@
|
|
738
846
|
// Save a fragment into the hash history. You are responsible for properly
|
739
847
|
// URL-encoding the fragment in advance. This does not trigger
|
740
848
|
// a `hashchange` event.
|
741
|
-
|
849
|
+
navigate : function(fragment, triggerRoute) {
|
742
850
|
fragment = (fragment || '').replace(hashStrip, '');
|
743
|
-
if (this.fragment == fragment) return;
|
744
|
-
|
745
|
-
|
746
|
-
this.
|
747
|
-
this.
|
851
|
+
if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
|
852
|
+
if (this._hasPushState) {
|
853
|
+
var loc = window.location;
|
854
|
+
if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
|
855
|
+
this.fragment = fragment;
|
856
|
+
window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + fragment);
|
857
|
+
} else {
|
858
|
+
window.location.hash = this.fragment = fragment;
|
859
|
+
if (this.iframe && (fragment != this.getFragment(this.iframe.location.hash))) {
|
860
|
+
this.iframe.document.open().close();
|
861
|
+
this.iframe.location.hash = fragment;
|
862
|
+
}
|
748
863
|
}
|
864
|
+
if (triggerRoute) this.loadUrl(fragment);
|
749
865
|
}
|
750
866
|
|
751
867
|
});
|
@@ -756,10 +872,11 @@
|
|
756
872
|
// Creating a Backbone.View creates its initial element outside of the DOM,
|
757
873
|
// if an existing element is not provided...
|
758
874
|
Backbone.View = function(options) {
|
875
|
+
this.cid = _.uniqueId('view');
|
759
876
|
this._configure(options || {});
|
760
877
|
this._ensureElement();
|
761
878
|
this.delegateEvents();
|
762
|
-
this.initialize(
|
879
|
+
this.initialize.apply(this, arguments);
|
763
880
|
};
|
764
881
|
|
765
882
|
// Element lookup, scoped to DOM elements within the current view.
|
@@ -770,7 +887,10 @@
|
|
770
887
|
};
|
771
888
|
|
772
889
|
// Cached regex to split keys for `delegate`.
|
773
|
-
var eventSplitter = /^(\
|
890
|
+
var eventSplitter = /^(\S+)\s*(.*)$/;
|
891
|
+
|
892
|
+
// List of view options to be merged as properties.
|
893
|
+
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
774
894
|
|
775
895
|
// Set up all inheritable **Backbone.View** properties and methods.
|
776
896
|
_.extend(Backbone.View.prototype, Backbone.Events, {
|
@@ -827,12 +947,14 @@
|
|
827
947
|
// not `change`, `submit`, and `reset` in Internet Explorer.
|
828
948
|
delegateEvents : function(events) {
|
829
949
|
if (!(events || (events = this.events))) return;
|
830
|
-
$(this.el).unbind();
|
950
|
+
$(this.el).unbind('.delegateEvents' + this.cid);
|
831
951
|
for (var key in events) {
|
832
|
-
var
|
952
|
+
var method = this[events[key]];
|
953
|
+
if (!method) throw new Error('Event "' + events[key] + '" does not exist');
|
833
954
|
var match = key.match(eventSplitter);
|
834
955
|
var eventName = match[1], selector = match[2];
|
835
|
-
|
956
|
+
method = _.bind(method, this);
|
957
|
+
eventName += '.delegateEvents' + this.cid;
|
836
958
|
if (selector === '') {
|
837
959
|
$(this.el).bind(eventName, method);
|
838
960
|
} else {
|
@@ -846,22 +968,26 @@
|
|
846
968
|
// attached directly to the view.
|
847
969
|
_configure : function(options) {
|
848
970
|
if (this.options) options = _.extend({}, this.options, options);
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
if (options.className) this.className = options.className;
|
854
|
-
if (options.tagName) this.tagName = options.tagName;
|
971
|
+
for (var i = 0, l = viewOptions.length; i < l; i++) {
|
972
|
+
var attr = viewOptions[i];
|
973
|
+
if (options[attr]) this[attr] = options[attr];
|
974
|
+
}
|
855
975
|
this.options = options;
|
856
976
|
},
|
857
977
|
|
858
978
|
// Ensure that the View has a DOM element to render into.
|
979
|
+
// If `this.el` is a string, pass it through `$()`, take the first
|
980
|
+
// matching element, and re-assign it to `el`. Otherwise, create
|
981
|
+
// an element from the `id`, `className` and `tagName` proeprties.
|
859
982
|
_ensureElement : function() {
|
860
|
-
if (this.el)
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
983
|
+
if (!this.el) {
|
984
|
+
var attrs = this.attributes || {};
|
985
|
+
if (this.id) attrs.id = this.id;
|
986
|
+
if (this.className) attrs['class'] = this.className;
|
987
|
+
this.el = this.make(this.tagName, attrs);
|
988
|
+
} else if (_.isString(this.el)) {
|
989
|
+
this.el = $(this.el).get(0);
|
990
|
+
}
|
865
991
|
}
|
866
992
|
|
867
993
|
});
|
@@ -869,13 +995,13 @@
|
|
869
995
|
// The self-propagating extend function that Backbone classes use.
|
870
996
|
var extend = function (protoProps, classProps) {
|
871
997
|
var child = inherits(this, protoProps, classProps);
|
872
|
-
child.extend = extend;
|
998
|
+
child.extend = this.extend;
|
873
999
|
return child;
|
874
1000
|
};
|
875
1001
|
|
876
1002
|
// Set up inheritance for the model, collection, and view.
|
877
1003
|
Backbone.Model.extend = Backbone.Collection.extend =
|
878
|
-
Backbone.
|
1004
|
+
Backbone.Router.extend = Backbone.View.extend = extend;
|
879
1005
|
|
880
1006
|
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
881
1007
|
var methodMap = {
|
@@ -903,28 +1029,32 @@
|
|
903
1029
|
// `application/json` with the model in a param named `model`.
|
904
1030
|
// Useful when interfacing with server-side languages like **PHP** that make
|
905
1031
|
// it difficult to read the body of `PUT` requests.
|
906
|
-
Backbone.sync = function(method, model,
|
1032
|
+
Backbone.sync = function(method, model, options) {
|
907
1033
|
var type = methodMap[method];
|
908
|
-
var modelJSON = (method === 'create' || method === 'update') ?
|
909
|
-
JSON.stringify(model.toJSON()) : null;
|
910
1034
|
|
911
1035
|
// Default JSON-request options.
|
912
|
-
var params = {
|
913
|
-
url: getUrl(model),
|
1036
|
+
var params = _.extend({
|
914
1037
|
type: type,
|
915
|
-
contentType: 'application/json',
|
916
|
-
data: modelJSON,
|
917
1038
|
dataType: 'json',
|
918
|
-
processData: false
|
919
|
-
|
920
|
-
|
921
|
-
|
1039
|
+
processData: false
|
1040
|
+
}, options);
|
1041
|
+
|
1042
|
+
// Ensure that we have a URL.
|
1043
|
+
if (!params.url) {
|
1044
|
+
params.url = getUrl(model) || urlError();
|
1045
|
+
}
|
1046
|
+
|
1047
|
+
// Ensure that we have the appropriate request data.
|
1048
|
+
if (!params.data && model && (method == 'create' || method == 'update')) {
|
1049
|
+
params.contentType = 'application/json';
|
1050
|
+
params.data = JSON.stringify(model.toJSON());
|
1051
|
+
}
|
922
1052
|
|
923
1053
|
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
924
1054
|
if (Backbone.emulateJSON) {
|
925
1055
|
params.contentType = 'application/x-www-form-urlencoded';
|
926
1056
|
params.processData = true;
|
927
|
-
params.data =
|
1057
|
+
params.data = params.data ? {model : params.data} : {};
|
928
1058
|
}
|
929
1059
|
|
930
1060
|
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
@@ -934,13 +1064,13 @@
|
|
934
1064
|
if (Backbone.emulateJSON) params.data._method = type;
|
935
1065
|
params.type = 'POST';
|
936
1066
|
params.beforeSend = function(xhr) {
|
937
|
-
xhr.setRequestHeader(
|
1067
|
+
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
938
1068
|
};
|
939
1069
|
}
|
940
1070
|
}
|
941
1071
|
|
942
1072
|
// Make the request.
|
943
|
-
$.ajax(params);
|
1073
|
+
return $.ajax(params);
|
944
1074
|
};
|
945
1075
|
|
946
1076
|
// Helpers
|
@@ -964,6 +1094,9 @@
|
|
964
1094
|
child = function(){ return parent.apply(this, arguments); };
|
965
1095
|
}
|
966
1096
|
|
1097
|
+
// Inherit class (static) properties from parent.
|
1098
|
+
_.extend(child, parent);
|
1099
|
+
|
967
1100
|
// Set the prototype chain to inherit from `parent`, without calling
|
968
1101
|
// `parent`'s constructor function.
|
969
1102
|
ctor.prototype = parent.prototype;
|
@@ -976,7 +1109,7 @@
|
|
976
1109
|
// Add static properties to the constructor function, if supplied.
|
977
1110
|
if (staticProps) _.extend(child, staticProps);
|
978
1111
|
|
979
|
-
// Correctly set child's `prototype.constructor
|
1112
|
+
// Correctly set child's `prototype.constructor`.
|
980
1113
|
child.prototype.constructor = child;
|
981
1114
|
|
982
1115
|
// Set a convenience property in case the parent's prototype is needed later.
|
@@ -988,15 +1121,20 @@
|
|
988
1121
|
// Helper function to get a URL from a Model or Collection as a property
|
989
1122
|
// or as a function.
|
990
1123
|
var getUrl = function(object) {
|
991
|
-
if (!(object && object.url))
|
1124
|
+
if (!(object && object.url)) return null;
|
992
1125
|
return _.isFunction(object.url) ? object.url() : object.url;
|
993
1126
|
};
|
994
1127
|
|
1128
|
+
// Throw an error when a URL is needed, and none is supplied.
|
1129
|
+
var urlError = function() {
|
1130
|
+
throw new Error('A "url" property or function must be specified');
|
1131
|
+
};
|
1132
|
+
|
995
1133
|
// Wrap an optional error callback with a fallback error event.
|
996
1134
|
var wrapError = function(onError, model, options) {
|
997
1135
|
return function(resp) {
|
998
1136
|
if (onError) {
|
999
|
-
onError(model, resp);
|
1137
|
+
onError(model, resp, options);
|
1000
1138
|
} else {
|
1001
1139
|
model.trigger('error', model, resp, options);
|
1002
1140
|
}
|
@@ -1005,7 +1143,7 @@
|
|
1005
1143
|
|
1006
1144
|
// Helper function to escape a string for HTML rendering.
|
1007
1145
|
var escapeHTML = function(string) {
|
1008
|
-
return string.replace(/&(?!\w+;)/
|
1146
|
+
return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
1009
1147
|
};
|
1010
1148
|
|
1011
|
-
})();
|
1149
|
+
}).call(this);
|