backbone-rails 0.9.10 → 1.0.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 +578 -505
- data/vendor/assets/javascripts/underscore.js +40 -34
- metadata +2 -2
@@ -1,6 +1,6 @@
|
|
1
|
-
// Backbone.js 0.
|
1
|
+
// Backbone.js 1.0.0
|
2
2
|
|
3
|
-
// (c) 2010-
|
3
|
+
// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.
|
4
4
|
// Backbone may be freely distributed under the MIT license.
|
5
5
|
// For all details and documentation:
|
6
6
|
// http://backbonejs.org
|
@@ -18,14 +18,14 @@
|
|
18
18
|
// restored later on, if `noConflict` is used.
|
19
19
|
var previousBackbone = root.Backbone;
|
20
20
|
|
21
|
-
// Create
|
21
|
+
// Create local references to array methods we'll want to use later.
|
22
22
|
var array = [];
|
23
23
|
var push = array.push;
|
24
24
|
var slice = array.slice;
|
25
25
|
var splice = array.splice;
|
26
26
|
|
27
27
|
// The top-level namespace. All public Backbone classes and modules will
|
28
|
-
// be attached to this. Exported for both
|
28
|
+
// be attached to this. Exported for both the browser and the server.
|
29
29
|
var Backbone;
|
30
30
|
if (typeof exports !== 'undefined') {
|
31
31
|
Backbone = exports;
|
@@ -34,14 +34,15 @@
|
|
34
34
|
}
|
35
35
|
|
36
36
|
// Current version of the library. Keep in sync with `package.json`.
|
37
|
-
Backbone.VERSION = '0.
|
37
|
+
Backbone.VERSION = '1.0.0';
|
38
38
|
|
39
39
|
// Require Underscore, if we're on the server, and it's not already present.
|
40
40
|
var _ = root._;
|
41
41
|
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
|
42
42
|
|
43
|
-
// For Backbone's purposes, jQuery, Zepto, or
|
44
|
-
|
43
|
+
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
|
44
|
+
// the `$` variable.
|
45
|
+
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
|
45
46
|
|
46
47
|
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
|
47
48
|
// to its previous owner. Returns a reference to this Backbone object.
|
@@ -64,45 +65,6 @@
|
|
64
65
|
// Backbone.Events
|
65
66
|
// ---------------
|
66
67
|
|
67
|
-
// Regular expression used to split event strings.
|
68
|
-
var eventSplitter = /\s+/;
|
69
|
-
|
70
|
-
// Implement fancy features of the Events API such as multiple event
|
71
|
-
// names `"change blur"` and jQuery-style event maps `{change: action}`
|
72
|
-
// in terms of the existing API.
|
73
|
-
var eventsApi = function(obj, action, name, rest) {
|
74
|
-
if (!name) return true;
|
75
|
-
if (typeof name === 'object') {
|
76
|
-
for (var key in name) {
|
77
|
-
obj[action].apply(obj, [key, name[key]].concat(rest));
|
78
|
-
}
|
79
|
-
} else if (eventSplitter.test(name)) {
|
80
|
-
var names = name.split(eventSplitter);
|
81
|
-
for (var i = 0, l = names.length; i < l; i++) {
|
82
|
-
obj[action].apply(obj, [names[i]].concat(rest));
|
83
|
-
}
|
84
|
-
} else {
|
85
|
-
return true;
|
86
|
-
}
|
87
|
-
};
|
88
|
-
|
89
|
-
// Optimized internal dispatch function for triggering events. Tries to
|
90
|
-
// keep the usual cases speedy (most Backbone events have 3 arguments).
|
91
|
-
var triggerEvents = function(events, args) {
|
92
|
-
var ev, i = -1, l = events.length;
|
93
|
-
switch (args.length) {
|
94
|
-
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
|
95
|
-
return;
|
96
|
-
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
|
97
|
-
return;
|
98
|
-
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
|
99
|
-
return;
|
100
|
-
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
|
101
|
-
return;
|
102
|
-
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
|
103
|
-
}
|
104
|
-
};
|
105
|
-
|
106
68
|
// A module that can be mixed in to *any object* in order to provide it with
|
107
69
|
// custom events. You may bind with `on` or remove with `off` callback
|
108
70
|
// functions to an event; `trigger`-ing an event fires all callbacks in
|
@@ -115,29 +77,27 @@
|
|
115
77
|
//
|
116
78
|
var Events = Backbone.Events = {
|
117
79
|
|
118
|
-
// Bind
|
119
|
-
//
|
120
|
-
// all events fired.
|
80
|
+
// Bind an event to a `callback` function. Passing `"all"` will bind
|
81
|
+
// the callback to all events fired.
|
121
82
|
on: function(name, callback, context) {
|
122
|
-
if (!
|
83
|
+
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
|
123
84
|
this._events || (this._events = {});
|
124
|
-
var
|
125
|
-
|
85
|
+
var events = this._events[name] || (this._events[name] = []);
|
86
|
+
events.push({callback: callback, context: context, ctx: context || this});
|
126
87
|
return this;
|
127
88
|
},
|
128
89
|
|
129
|
-
// Bind
|
90
|
+
// Bind an event to only be triggered a single time. After the first time
|
130
91
|
// the callback is invoked, it will be removed.
|
131
92
|
once: function(name, callback, context) {
|
132
|
-
if (!
|
93
|
+
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
|
133
94
|
var self = this;
|
134
95
|
var once = _.once(function() {
|
135
96
|
self.off(name, once);
|
136
97
|
callback.apply(this, arguments);
|
137
98
|
});
|
138
99
|
once._callback = callback;
|
139
|
-
this.on(name, once, context);
|
140
|
-
return this;
|
100
|
+
return this.on(name, once, context);
|
141
101
|
},
|
142
102
|
|
143
103
|
// Remove one or many callbacks. If `context` is null, removes all
|
@@ -145,7 +105,7 @@
|
|
145
105
|
// callbacks for the event. If `name` is null, removes all bound
|
146
106
|
// callbacks for all events.
|
147
107
|
off: function(name, callback, context) {
|
148
|
-
var
|
108
|
+
var retain, ev, events, names, i, l, j, k;
|
149
109
|
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
|
150
110
|
if (!name && !callback && !context) {
|
151
111
|
this._events = {};
|
@@ -155,19 +115,18 @@
|
|
155
115
|
names = name ? [name] : _.keys(this._events);
|
156
116
|
for (i = 0, l = names.length; i < l; i++) {
|
157
117
|
name = names[i];
|
158
|
-
if (
|
159
|
-
|
118
|
+
if (events = this._events[name]) {
|
119
|
+
this._events[name] = retain = [];
|
160
120
|
if (callback || context) {
|
161
|
-
for (j = 0, k =
|
162
|
-
ev =
|
163
|
-
if ((callback && callback !== ev.callback &&
|
164
|
-
callback !== ev.callback._callback) ||
|
121
|
+
for (j = 0, k = events.length; j < k; j++) {
|
122
|
+
ev = events[j];
|
123
|
+
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
|
165
124
|
(context && context !== ev.context)) {
|
166
|
-
|
125
|
+
retain.push(ev);
|
167
126
|
}
|
168
127
|
}
|
169
128
|
}
|
170
|
-
this._events[name]
|
129
|
+
if (!retain.length) delete this._events[name];
|
171
130
|
}
|
172
131
|
}
|
173
132
|
|
@@ -189,35 +148,82 @@
|
|
189
148
|
return this;
|
190
149
|
},
|
191
150
|
|
192
|
-
// An inversion-of-control version of `on`. Tell *this* object to listen to
|
193
|
-
// an event in another object ... keeping track of what it's listening to.
|
194
|
-
listenTo: function(obj, name, callback) {
|
195
|
-
var listeners = this._listeners || (this._listeners = {});
|
196
|
-
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
|
197
|
-
listeners[id] = obj;
|
198
|
-
obj.on(name, typeof name === 'object' ? this : callback, this);
|
199
|
-
return this;
|
200
|
-
},
|
201
|
-
|
202
151
|
// Tell this object to stop listening to either specific events ... or
|
203
152
|
// to every object it's currently listening to.
|
204
153
|
stopListening: function(obj, name, callback) {
|
205
154
|
var listeners = this._listeners;
|
206
|
-
if (!listeners) return;
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
listeners[id].off(name, callback, this);
|
214
|
-
}
|
215
|
-
this._listeners = {};
|
155
|
+
if (!listeners) return this;
|
156
|
+
var deleteListener = !name && !callback;
|
157
|
+
if (typeof name === 'object') callback = this;
|
158
|
+
if (obj) (listeners = {})[obj._listenerId] = obj;
|
159
|
+
for (var id in listeners) {
|
160
|
+
listeners[id].off(name, callback, this);
|
161
|
+
if (deleteListener) delete this._listeners[id];
|
216
162
|
}
|
217
163
|
return this;
|
218
164
|
}
|
165
|
+
|
166
|
+
};
|
167
|
+
|
168
|
+
// Regular expression used to split event strings.
|
169
|
+
var eventSplitter = /\s+/;
|
170
|
+
|
171
|
+
// Implement fancy features of the Events API such as multiple event
|
172
|
+
// names `"change blur"` and jQuery-style event maps `{change: action}`
|
173
|
+
// in terms of the existing API.
|
174
|
+
var eventsApi = function(obj, action, name, rest) {
|
175
|
+
if (!name) return true;
|
176
|
+
|
177
|
+
// Handle event maps.
|
178
|
+
if (typeof name === 'object') {
|
179
|
+
for (var key in name) {
|
180
|
+
obj[action].apply(obj, [key, name[key]].concat(rest));
|
181
|
+
}
|
182
|
+
return false;
|
183
|
+
}
|
184
|
+
|
185
|
+
// Handle space separated event names.
|
186
|
+
if (eventSplitter.test(name)) {
|
187
|
+
var names = name.split(eventSplitter);
|
188
|
+
for (var i = 0, l = names.length; i < l; i++) {
|
189
|
+
obj[action].apply(obj, [names[i]].concat(rest));
|
190
|
+
}
|
191
|
+
return false;
|
192
|
+
}
|
193
|
+
|
194
|
+
return true;
|
195
|
+
};
|
196
|
+
|
197
|
+
// A difficult-to-believe, but optimized internal dispatch function for
|
198
|
+
// triggering events. Tries to keep the usual cases speedy (most internal
|
199
|
+
// Backbone events have 3 arguments).
|
200
|
+
var triggerEvents = function(events, args) {
|
201
|
+
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
|
202
|
+
switch (args.length) {
|
203
|
+
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
|
204
|
+
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
|
205
|
+
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
|
206
|
+
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
|
207
|
+
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
|
208
|
+
}
|
219
209
|
};
|
220
210
|
|
211
|
+
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
|
212
|
+
|
213
|
+
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
|
214
|
+
// listen to an event in another object ... keeping track of what it's
|
215
|
+
// listening to.
|
216
|
+
_.each(listenMethods, function(implementation, method) {
|
217
|
+
Events[method] = function(obj, name, callback) {
|
218
|
+
var listeners = this._listeners || (this._listeners = {});
|
219
|
+
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
|
220
|
+
listeners[id] = obj;
|
221
|
+
if (typeof name === 'object') callback = this;
|
222
|
+
obj[implementation](name, callback, this);
|
223
|
+
return this;
|
224
|
+
};
|
225
|
+
});
|
226
|
+
|
221
227
|
// Aliases for backwards compatibility.
|
222
228
|
Events.bind = Events.on;
|
223
229
|
Events.unbind = Events.off;
|
@@ -229,15 +235,21 @@
|
|
229
235
|
// Backbone.Model
|
230
236
|
// --------------
|
231
237
|
|
232
|
-
//
|
238
|
+
// Backbone **Models** are the basic data object in the framework --
|
239
|
+
// frequently representing a row in a table in a database on your server.
|
240
|
+
// A discrete chunk of data and a bunch of useful, related methods for
|
241
|
+
// performing computations and transformations on that data.
|
242
|
+
|
243
|
+
// Create a new model with the specified attributes. A client id (`cid`)
|
233
244
|
// is automatically generated and assigned for you.
|
234
245
|
var Model = Backbone.Model = function(attributes, options) {
|
235
246
|
var defaults;
|
236
247
|
var attrs = attributes || {};
|
248
|
+
options || (options = {});
|
237
249
|
this.cid = _.uniqueId('c');
|
238
250
|
this.attributes = {};
|
239
|
-
|
240
|
-
if (options
|
251
|
+
_.extend(this, _.pick(options, modelOptions));
|
252
|
+
if (options.parse) attrs = this.parse(attrs, options) || {};
|
241
253
|
if (defaults = _.result(this, 'defaults')) {
|
242
254
|
attrs = _.defaults({}, attrs, defaults);
|
243
255
|
}
|
@@ -246,12 +258,18 @@
|
|
246
258
|
this.initialize.apply(this, arguments);
|
247
259
|
};
|
248
260
|
|
261
|
+
// A list of options to be attached directly to the model, if provided.
|
262
|
+
var modelOptions = ['url', 'urlRoot', 'collection'];
|
263
|
+
|
249
264
|
// Attach all inheritable methods to the Model prototype.
|
250
265
|
_.extend(Model.prototype, Events, {
|
251
266
|
|
252
267
|
// A hash of attributes whose current and previous value differ.
|
253
268
|
changed: null,
|
254
269
|
|
270
|
+
// The value returned during the last failed validation.
|
271
|
+
validationError: null,
|
272
|
+
|
255
273
|
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
256
274
|
// CouchDB users may want to set this to `"_id"`.
|
257
275
|
idAttribute: 'id',
|
@@ -265,7 +283,8 @@
|
|
265
283
|
return _.clone(this.attributes);
|
266
284
|
},
|
267
285
|
|
268
|
-
// Proxy `Backbone.sync` by default
|
286
|
+
// Proxy `Backbone.sync` by default -- but override this if you need
|
287
|
+
// custom syncing semantics for *this* particular model.
|
269
288
|
sync: function() {
|
270
289
|
return Backbone.sync.apply(this, arguments);
|
271
290
|
},
|
@@ -286,10 +305,9 @@
|
|
286
305
|
return this.get(attr) != null;
|
287
306
|
},
|
288
307
|
|
289
|
-
//
|
290
|
-
|
291
|
-
//
|
292
|
-
// you choose to silence it.
|
308
|
+
// Set a hash of model attributes on the object, firing `"change"`. This is
|
309
|
+
// the core primitive operation of a model, updating the data and notifying
|
310
|
+
// anyone who needs to know about the change in state. The heart of the beast.
|
293
311
|
set: function(key, val, options) {
|
294
312
|
var attr, attrs, unset, changes, silent, changing, prev, current;
|
295
313
|
if (key == null) return this;
|
@@ -343,6 +361,8 @@
|
|
343
361
|
}
|
344
362
|
}
|
345
363
|
|
364
|
+
// You might be wondering why there's a `while` loop here. Changes can
|
365
|
+
// be recursively nested within `"change"` events.
|
346
366
|
if (changing) return this;
|
347
367
|
if (!silent) {
|
348
368
|
while (this._pending) {
|
@@ -355,14 +375,13 @@
|
|
355
375
|
return this;
|
356
376
|
},
|
357
377
|
|
358
|
-
// Remove an attribute from the model, firing `"change"`
|
359
|
-
//
|
378
|
+
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
|
379
|
+
// if the attribute doesn't exist.
|
360
380
|
unset: function(attr, options) {
|
361
381
|
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
|
362
382
|
},
|
363
383
|
|
364
|
-
// Clear all attributes on the model, firing `"change"
|
365
|
-
// to silence it.
|
384
|
+
// Clear all attributes on the model, firing `"change"`.
|
366
385
|
clear: function(options) {
|
367
386
|
var attrs = {};
|
368
387
|
for (var key in this.attributes) attrs[key] = void 0;
|
@@ -406,19 +425,20 @@
|
|
406
425
|
return _.clone(this._previousAttributes);
|
407
426
|
},
|
408
427
|
|
409
|
-
// ---------------------------------------------------------------------
|
410
|
-
|
411
428
|
// Fetch the model from the server. If the server's representation of the
|
412
|
-
// model differs from its current attributes, they will be
|
429
|
+
// model differs from its current attributes, they will be overridden,
|
413
430
|
// triggering a `"change"` event.
|
414
431
|
fetch: function(options) {
|
415
432
|
options = options ? _.clone(options) : {};
|
416
433
|
if (options.parse === void 0) options.parse = true;
|
434
|
+
var model = this;
|
417
435
|
var success = options.success;
|
418
|
-
options.success = function(
|
436
|
+
options.success = function(resp) {
|
419
437
|
if (!model.set(model.parse(resp, options), options)) return false;
|
420
438
|
if (success) success(model, resp, options);
|
439
|
+
model.trigger('sync', model, resp, options);
|
421
440
|
};
|
441
|
+
wrapError(this, options);
|
422
442
|
return this.sync('read', this, options);
|
423
443
|
},
|
424
444
|
|
@@ -426,7 +446,7 @@
|
|
426
446
|
// If the server returns an attributes hash that differs, the model's
|
427
447
|
// state will be `set` again.
|
428
448
|
save: function(key, val, options) {
|
429
|
-
var attrs,
|
449
|
+
var attrs, method, xhr, attributes = this.attributes;
|
430
450
|
|
431
451
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
432
452
|
if (key == null || typeof key === 'object') {
|
@@ -452,8 +472,9 @@
|
|
452
472
|
// After a successful server-side save, the client is (optionally)
|
453
473
|
// updated with the server-side state.
|
454
474
|
if (options.parse === void 0) options.parse = true;
|
455
|
-
|
456
|
-
|
475
|
+
var model = this;
|
476
|
+
var success = options.success;
|
477
|
+
options.success = function(resp) {
|
457
478
|
// Ensure attributes are restored during synchronous saves.
|
458
479
|
model.attributes = attributes;
|
459
480
|
var serverAttrs = model.parse(resp, options);
|
@@ -462,9 +483,10 @@
|
|
462
483
|
return false;
|
463
484
|
}
|
464
485
|
if (success) success(model, resp, options);
|
486
|
+
model.trigger('sync', model, resp, options);
|
465
487
|
};
|
488
|
+
wrapError(this, options);
|
466
489
|
|
467
|
-
// Finish configuring and sending the Ajax request.
|
468
490
|
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
469
491
|
if (method === 'patch') options.attrs = attrs;
|
470
492
|
xhr = this.sync(method, this, options);
|
@@ -487,15 +509,17 @@
|
|
487
509
|
model.trigger('destroy', model, model.collection, options);
|
488
510
|
};
|
489
511
|
|
490
|
-
options.success = function(
|
512
|
+
options.success = function(resp) {
|
491
513
|
if (options.wait || model.isNew()) destroy();
|
492
514
|
if (success) success(model, resp, options);
|
515
|
+
if (!model.isNew()) model.trigger('sync', model, resp, options);
|
493
516
|
};
|
494
517
|
|
495
518
|
if (this.isNew()) {
|
496
|
-
options.success(
|
519
|
+
options.success();
|
497
520
|
return false;
|
498
521
|
}
|
522
|
+
wrapError(this, options);
|
499
523
|
|
500
524
|
var xhr = this.sync('delete', this, options);
|
501
525
|
if (!options.wait) destroy();
|
@@ -529,39 +553,61 @@
|
|
529
553
|
|
530
554
|
// Check if the model is currently in a valid state.
|
531
555
|
isValid: function(options) {
|
532
|
-
return
|
556
|
+
return this._validate({}, _.extend(options || {}, { validate: true }));
|
533
557
|
},
|
534
558
|
|
535
559
|
// Run validation against the next complete set of model attributes,
|
536
|
-
// returning `true` if all is well. Otherwise, fire
|
537
|
-
// `"error"` event and call the error callback, if specified.
|
560
|
+
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
|
538
561
|
_validate: function(attrs, options) {
|
539
562
|
if (!options.validate || !this.validate) return true;
|
540
563
|
attrs = _.extend({}, this.attributes, attrs);
|
541
564
|
var error = this.validationError = this.validate(attrs, options) || null;
|
542
565
|
if (!error) return true;
|
543
|
-
this.trigger('invalid', this, error, options || {});
|
566
|
+
this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
|
544
567
|
return false;
|
545
568
|
}
|
546
569
|
|
547
570
|
});
|
548
571
|
|
572
|
+
// Underscore methods that we want to implement on the Model.
|
573
|
+
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
|
574
|
+
|
575
|
+
// Mix in each Underscore method as a proxy to `Model#attributes`.
|
576
|
+
_.each(modelMethods, function(method) {
|
577
|
+
Model.prototype[method] = function() {
|
578
|
+
var args = slice.call(arguments);
|
579
|
+
args.unshift(this.attributes);
|
580
|
+
return _[method].apply(_, args);
|
581
|
+
};
|
582
|
+
});
|
583
|
+
|
549
584
|
// Backbone.Collection
|
550
585
|
// -------------------
|
551
586
|
|
552
|
-
//
|
553
|
-
//
|
587
|
+
// If models tend to represent a single row of data, a Backbone Collection is
|
588
|
+
// more analagous to a table full of data ... or a small slice or page of that
|
589
|
+
// table, or a collection of rows that belong together for a particular reason
|
590
|
+
// -- all of the messages in this particular folder, all of the documents
|
591
|
+
// belonging to this particular author, and so on. Collections maintain
|
592
|
+
// indexes of their models, both in order, and for lookup by `id`.
|
593
|
+
|
594
|
+
// Create a new **Collection**, perhaps to contain a specific type of `model`.
|
595
|
+
// If a `comparator` is specified, the Collection will maintain
|
554
596
|
// its models in sort order, as they're added and removed.
|
555
597
|
var Collection = Backbone.Collection = function(models, options) {
|
556
598
|
options || (options = {});
|
599
|
+
if (options.url) this.url = options.url;
|
557
600
|
if (options.model) this.model = options.model;
|
558
601
|
if (options.comparator !== void 0) this.comparator = options.comparator;
|
559
|
-
this.models = [];
|
560
602
|
this._reset();
|
561
603
|
this.initialize.apply(this, arguments);
|
562
604
|
if (models) this.reset(models, _.extend({silent: true}, options));
|
563
605
|
};
|
564
606
|
|
607
|
+
// Default options for `Collection#set`.
|
608
|
+
var setOptions = {add: true, remove: true, merge: true};
|
609
|
+
var addOptions = {add: true, merge: false, remove: false};
|
610
|
+
|
565
611
|
// Define the Collection's inheritable methods.
|
566
612
|
_.extend(Collection.prototype, Events, {
|
567
613
|
|
@@ -586,88 +632,118 @@
|
|
586
632
|
|
587
633
|
// Add a model, or list of models to the set.
|
588
634
|
add: function(models, options) {
|
635
|
+
return this.set(models, _.defaults(options || {}, addOptions));
|
636
|
+
},
|
637
|
+
|
638
|
+
// Remove a model, or a list of models from the set.
|
639
|
+
remove: function(models, options) {
|
589
640
|
models = _.isArray(models) ? models.slice() : [models];
|
590
641
|
options || (options = {});
|
591
|
-
var i, l,
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
642
|
+
var i, l, index, model;
|
643
|
+
for (i = 0, l = models.length; i < l; i++) {
|
644
|
+
model = this.get(models[i]);
|
645
|
+
if (!model) continue;
|
646
|
+
delete this._byId[model.id];
|
647
|
+
delete this._byId[model.cid];
|
648
|
+
index = this.indexOf(model);
|
649
|
+
this.models.splice(index, 1);
|
650
|
+
this.length--;
|
651
|
+
if (!options.silent) {
|
652
|
+
options.index = index;
|
653
|
+
model.trigger('remove', model, this, options);
|
654
|
+
}
|
655
|
+
this._removeReference(model);
|
656
|
+
}
|
657
|
+
return this;
|
658
|
+
},
|
659
|
+
|
660
|
+
// Update a collection by `set`-ing a new list of models, adding new ones,
|
661
|
+
// removing models that are no longer present, and merging models that
|
662
|
+
// already exist in the collection, as necessary. Similar to **Model#set**,
|
663
|
+
// the core operation for updating the data contained by the collection.
|
664
|
+
set: function(models, options) {
|
665
|
+
options = _.defaults(options || {}, setOptions);
|
666
|
+
if (options.parse) models = this.parse(models, options);
|
667
|
+
if (!_.isArray(models)) models = models ? [models] : [];
|
668
|
+
var i, l, model, attrs, existing, sort;
|
669
|
+
var at = options.at;
|
670
|
+
var sortable = this.comparator && (at == null) && options.sort !== false;
|
671
|
+
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
672
|
+
var toAdd = [], toRemove = [], modelMap = {};
|
596
673
|
|
597
674
|
// Turn bare objects into model references, and prevent invalid models
|
598
675
|
// from being added.
|
599
676
|
for (i = 0, l = models.length; i < l; i++) {
|
600
|
-
if (!(model = this._prepareModel(
|
601
|
-
this.trigger('invalid', this, attrs, options);
|
602
|
-
continue;
|
603
|
-
}
|
677
|
+
if (!(model = this._prepareModel(models[i], options))) continue;
|
604
678
|
|
605
679
|
// If a duplicate is found, prevent it from being added and
|
606
680
|
// optionally merge it into the existing model.
|
607
681
|
if (existing = this.get(model)) {
|
682
|
+
if (options.remove) modelMap[existing.cid] = true;
|
608
683
|
if (options.merge) {
|
609
|
-
existing.set(
|
610
|
-
if (
|
684
|
+
existing.set(model.attributes, options);
|
685
|
+
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
|
611
686
|
}
|
612
|
-
continue;
|
613
|
-
}
|
614
687
|
|
615
|
-
// This is a new model, push it to the `
|
616
|
-
add
|
688
|
+
// This is a new model, push it to the `toAdd` list.
|
689
|
+
} else if (options.add) {
|
690
|
+
toAdd.push(model);
|
617
691
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
692
|
+
// Listen to added models' events, and index models for lookup by
|
693
|
+
// `id` and by `cid`.
|
694
|
+
model.on('all', this._onModelEvent, this);
|
695
|
+
this._byId[model.cid] = model;
|
696
|
+
if (model.id != null) this._byId[model.id] = model;
|
697
|
+
}
|
698
|
+
}
|
699
|
+
|
700
|
+
// Remove nonexistent models if appropriate.
|
701
|
+
if (options.remove) {
|
702
|
+
for (i = 0, l = this.length; i < l; ++i) {
|
703
|
+
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
|
704
|
+
}
|
705
|
+
if (toRemove.length) this.remove(toRemove, options);
|
623
706
|
}
|
624
707
|
|
625
708
|
// See if sorting is needed, update `length` and splice in new models.
|
626
|
-
if (
|
627
|
-
if (
|
628
|
-
this.length +=
|
709
|
+
if (toAdd.length) {
|
710
|
+
if (sortable) sort = true;
|
711
|
+
this.length += toAdd.length;
|
629
712
|
if (at != null) {
|
630
|
-
splice.apply(this.models, [at, 0].concat(
|
713
|
+
splice.apply(this.models, [at, 0].concat(toAdd));
|
631
714
|
} else {
|
632
|
-
push.apply(this.models,
|
715
|
+
push.apply(this.models, toAdd);
|
633
716
|
}
|
634
717
|
}
|
635
718
|
|
636
719
|
// Silently sort the collection if appropriate.
|
637
|
-
if (
|
720
|
+
if (sort) this.sort({silent: true});
|
638
721
|
|
639
722
|
if (options.silent) return this;
|
640
723
|
|
641
724
|
// Trigger `add` events.
|
642
|
-
for (i = 0, l =
|
643
|
-
(model =
|
725
|
+
for (i = 0, l = toAdd.length; i < l; i++) {
|
726
|
+
(model = toAdd[i]).trigger('add', model, this, options);
|
644
727
|
}
|
645
728
|
|
646
729
|
// Trigger `sort` if the collection was sorted.
|
647
|
-
if (
|
648
|
-
|
730
|
+
if (sort) this.trigger('sort', this, options);
|
649
731
|
return this;
|
650
732
|
},
|
651
733
|
|
652
|
-
//
|
653
|
-
|
654
|
-
|
734
|
+
// When you have more items than you want to add or remove individually,
|
735
|
+
// you can reset the entire set with a new list of models, without firing
|
736
|
+
// any granular `add` or `remove` events. Fires `reset` when finished.
|
737
|
+
// Useful for bulk operations and optimizations.
|
738
|
+
reset: function(models, options) {
|
655
739
|
options || (options = {});
|
656
|
-
var i, l
|
657
|
-
|
658
|
-
model = this.get(models[i]);
|
659
|
-
if (!model) continue;
|
660
|
-
delete this._byId[model.id];
|
661
|
-
delete this._byId[model.cid];
|
662
|
-
index = this.indexOf(model);
|
663
|
-
this.models.splice(index, 1);
|
664
|
-
this.length--;
|
665
|
-
if (!options.silent) {
|
666
|
-
options.index = index;
|
667
|
-
model.trigger('remove', model, this, options);
|
668
|
-
}
|
669
|
-
this._removeReference(model);
|
740
|
+
for (var i = 0, l = this.models.length; i < l; i++) {
|
741
|
+
this._removeReference(this.models[i]);
|
670
742
|
}
|
743
|
+
options.previousModels = this.models;
|
744
|
+
this._reset();
|
745
|
+
this.add(models, _.extend({silent: true}, options));
|
746
|
+
if (!options.silent) this.trigger('reset', this, options);
|
671
747
|
return this;
|
672
748
|
},
|
673
749
|
|
@@ -707,8 +783,7 @@
|
|
707
783
|
// Get a model from the set by id.
|
708
784
|
get: function(obj) {
|
709
785
|
if (obj == null) return void 0;
|
710
|
-
this.
|
711
|
-
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
|
786
|
+
return this._byId[obj.id != null ? obj.id : obj.cid || obj];
|
712
787
|
},
|
713
788
|
|
714
789
|
// Get the model at the given index.
|
@@ -716,10 +791,11 @@
|
|
716
791
|
return this.models[index];
|
717
792
|
},
|
718
793
|
|
719
|
-
// Return models with matching attributes. Useful for simple cases of
|
720
|
-
|
721
|
-
|
722
|
-
|
794
|
+
// Return models with matching attributes. Useful for simple cases of
|
795
|
+
// `filter`.
|
796
|
+
where: function(attrs, first) {
|
797
|
+
if (_.isEmpty(attrs)) return first ? void 0 : [];
|
798
|
+
return this[first ? 'find' : 'filter'](function(model) {
|
723
799
|
for (var key in attrs) {
|
724
800
|
if (attrs[key] !== model.get(key)) return false;
|
725
801
|
}
|
@@ -727,13 +803,17 @@
|
|
727
803
|
});
|
728
804
|
},
|
729
805
|
|
806
|
+
// Return the first model with matching attributes. Useful for simple cases
|
807
|
+
// of `find`.
|
808
|
+
findWhere: function(attrs) {
|
809
|
+
return this.where(attrs, true);
|
810
|
+
},
|
811
|
+
|
730
812
|
// Force the collection to re-sort itself. You don't need to call this under
|
731
813
|
// normal circumstances, as the set will maintain sort order as each item
|
732
814
|
// is added.
|
733
815
|
sort: function(options) {
|
734
|
-
if (!this.comparator)
|
735
|
-
throw new Error('Cannot sort a set without a comparator');
|
736
|
-
}
|
816
|
+
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
|
737
817
|
options || (options = {});
|
738
818
|
|
739
819
|
// Run sort based on type of `comparator`.
|
@@ -747,75 +827,36 @@
|
|
747
827
|
return this;
|
748
828
|
},
|
749
829
|
|
830
|
+
// Figure out the smallest index at which a model should be inserted so as
|
831
|
+
// to maintain order.
|
832
|
+
sortedIndex: function(model, value, context) {
|
833
|
+
value || (value = this.comparator);
|
834
|
+
var iterator = _.isFunction(value) ? value : function(model) {
|
835
|
+
return model.get(value);
|
836
|
+
};
|
837
|
+
return _.sortedIndex(this.models, model, iterator, context);
|
838
|
+
},
|
839
|
+
|
750
840
|
// Pluck an attribute from each model in the collection.
|
751
841
|
pluck: function(attr) {
|
752
842
|
return _.invoke(this.models, 'get', attr);
|
753
843
|
},
|
754
844
|
|
755
|
-
// Smartly update a collection with a change set of models, adding,
|
756
|
-
// removing, and merging as necessary.
|
757
|
-
update: function(models, options) {
|
758
|
-
options = _.extend({add: true, merge: true, remove: true}, options);
|
759
|
-
if (options.parse) models = this.parse(models, options);
|
760
|
-
var model, i, l, existing;
|
761
|
-
var add = [], remove = [], modelMap = {};
|
762
|
-
|
763
|
-
// Allow a single model (or no argument) to be passed.
|
764
|
-
if (!_.isArray(models)) models = models ? [models] : [];
|
765
|
-
|
766
|
-
// Proxy to `add` for this case, no need to iterate...
|
767
|
-
if (options.add && !options.remove) return this.add(models, options);
|
768
|
-
|
769
|
-
// Determine which models to add and merge, and which to remove.
|
770
|
-
for (i = 0, l = models.length; i < l; i++) {
|
771
|
-
model = models[i];
|
772
|
-
existing = this.get(model);
|
773
|
-
if (options.remove && existing) modelMap[existing.cid] = true;
|
774
|
-
if ((options.add && !existing) || (options.merge && existing)) {
|
775
|
-
add.push(model);
|
776
|
-
}
|
777
|
-
}
|
778
|
-
if (options.remove) {
|
779
|
-
for (i = 0, l = this.models.length; i < l; i++) {
|
780
|
-
model = this.models[i];
|
781
|
-
if (!modelMap[model.cid]) remove.push(model);
|
782
|
-
}
|
783
|
-
}
|
784
|
-
|
785
|
-
// Remove models (if applicable) before we add and merge the rest.
|
786
|
-
if (remove.length) this.remove(remove, options);
|
787
|
-
if (add.length) this.add(add, options);
|
788
|
-
return this;
|
789
|
-
},
|
790
|
-
|
791
|
-
// When you have more items than you want to add or remove individually,
|
792
|
-
// you can reset the entire set with a new list of models, without firing
|
793
|
-
// any `add` or `remove` events. Fires `reset` when finished.
|
794
|
-
reset: function(models, options) {
|
795
|
-
options || (options = {});
|
796
|
-
if (options.parse) models = this.parse(models, options);
|
797
|
-
for (var i = 0, l = this.models.length; i < l; i++) {
|
798
|
-
this._removeReference(this.models[i]);
|
799
|
-
}
|
800
|
-
options.previousModels = this.models.slice();
|
801
|
-
this._reset();
|
802
|
-
if (models) this.add(models, _.extend({silent: true}, options));
|
803
|
-
if (!options.silent) this.trigger('reset', this, options);
|
804
|
-
return this;
|
805
|
-
},
|
806
|
-
|
807
845
|
// Fetch the default set of models for this collection, resetting the
|
808
|
-
// collection when they arrive. If `
|
809
|
-
// data will be passed through the `
|
846
|
+
// collection when they arrive. If `reset: true` is passed, the response
|
847
|
+
// data will be passed through the `reset` method instead of `set`.
|
810
848
|
fetch: function(options) {
|
811
849
|
options = options ? _.clone(options) : {};
|
812
850
|
if (options.parse === void 0) options.parse = true;
|
813
851
|
var success = options.success;
|
814
|
-
|
815
|
-
|
852
|
+
var collection = this;
|
853
|
+
options.success = function(resp) {
|
854
|
+
var method = options.reset ? 'reset' : 'set';
|
816
855
|
collection[method](resp, options);
|
817
856
|
if (success) success(collection, resp, options);
|
857
|
+
collection.trigger('sync', collection, resp, options);
|
818
858
|
};
|
859
|
+
wrapError(this, options);
|
819
860
|
return this.sync('read', this, options);
|
820
861
|
},
|
821
862
|
|
@@ -828,7 +869,7 @@
|
|
828
869
|
if (!options.wait) this.add(model, options);
|
829
870
|
var collection = this;
|
830
871
|
var success = options.success;
|
831
|
-
options.success = function(
|
872
|
+
options.success = function(resp) {
|
832
873
|
if (options.wait) collection.add(model, options);
|
833
874
|
if (success) success(model, resp, options);
|
834
875
|
};
|
@@ -847,14 +888,16 @@
|
|
847
888
|
return new this.constructor(this.models);
|
848
889
|
},
|
849
890
|
|
850
|
-
//
|
891
|
+
// Private method to reset all internal state. Called when the collection
|
892
|
+
// is first initialized or reset.
|
851
893
|
_reset: function() {
|
852
894
|
this.length = 0;
|
853
|
-
this.models
|
895
|
+
this.models = [];
|
854
896
|
this._byId = {};
|
855
897
|
},
|
856
898
|
|
857
|
-
// Prepare a
|
899
|
+
// Prepare a hash of attributes (or other model) to be added to this
|
900
|
+
// collection.
|
858
901
|
_prepareModel: function(attrs, options) {
|
859
902
|
if (attrs instanceof Model) {
|
860
903
|
if (!attrs.collection) attrs.collection = this;
|
@@ -863,11 +906,14 @@
|
|
863
906
|
options || (options = {});
|
864
907
|
options.collection = this;
|
865
908
|
var model = new this.model(attrs, options);
|
866
|
-
if (!model._validate(attrs, options))
|
909
|
+
if (!model._validate(attrs, options)) {
|
910
|
+
this.trigger('invalid', this, attrs, options);
|
911
|
+
return false;
|
912
|
+
}
|
867
913
|
return model;
|
868
914
|
},
|
869
915
|
|
870
|
-
// Internal method to
|
916
|
+
// Internal method to sever a model's ties to a collection.
|
871
917
|
_removeReference: function(model) {
|
872
918
|
if (this === model.collection) delete model.collection;
|
873
919
|
model.off('all', this._onModelEvent, this);
|
@@ -885,19 +931,13 @@
|
|
885
931
|
if (model.id != null) this._byId[model.id] = model;
|
886
932
|
}
|
887
933
|
this.trigger.apply(this, arguments);
|
888
|
-
},
|
889
|
-
|
890
|
-
sortedIndex: function (model, value, context) {
|
891
|
-
value || (value = this.comparator);
|
892
|
-
var iterator = _.isFunction(value) ? value : function(model) {
|
893
|
-
return model.get(value);
|
894
|
-
};
|
895
|
-
return _.sortedIndex(this.models, model, iterator, context);
|
896
934
|
}
|
897
935
|
|
898
936
|
});
|
899
937
|
|
900
938
|
// Underscore methods that we want to implement on the Collection.
|
939
|
+
// 90% of the core usefulness of Backbone Collections is actually implemented
|
940
|
+
// right here:
|
901
941
|
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
|
902
942
|
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
|
903
943
|
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
|
@@ -927,62 +967,303 @@
|
|
927
967
|
};
|
928
968
|
});
|
929
969
|
|
930
|
-
// Backbone.
|
931
|
-
//
|
970
|
+
// Backbone.View
|
971
|
+
// -------------
|
932
972
|
|
933
|
-
//
|
934
|
-
//
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
973
|
+
// Backbone Views are almost more convention than they are actual code. A View
|
974
|
+
// is simply a JavaScript object that represents a logical chunk of UI in the
|
975
|
+
// DOM. This might be a single item, an entire list, a sidebar or panel, or
|
976
|
+
// even the surrounding frame which wraps your whole app. Defining a chunk of
|
977
|
+
// UI as a **View** allows you to define your DOM events declaratively, without
|
978
|
+
// having to worry about render order ... and makes it easy for the view to
|
979
|
+
// react to specific changes in the state of your models.
|
980
|
+
|
981
|
+
// Creating a Backbone.View creates its initial element outside of the DOM,
|
982
|
+
// if an existing element is not provided...
|
983
|
+
var View = Backbone.View = function(options) {
|
984
|
+
this.cid = _.uniqueId('view');
|
985
|
+
this._configure(options || {});
|
986
|
+
this._ensureElement();
|
939
987
|
this.initialize.apply(this, arguments);
|
988
|
+
this.delegateEvents();
|
940
989
|
};
|
941
990
|
|
942
|
-
// Cached
|
943
|
-
|
944
|
-
var optionalParam = /\((.*?)\)/g;
|
945
|
-
var namedParam = /(\(\?)?:\w+/g;
|
946
|
-
var splatParam = /\*\w+/g;
|
947
|
-
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
991
|
+
// Cached regex to split keys for `delegate`.
|
992
|
+
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
948
993
|
|
949
|
-
//
|
950
|
-
|
994
|
+
// List of view options to be merged as properties.
|
995
|
+
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
996
|
+
|
997
|
+
// Set up all inheritable **Backbone.View** properties and methods.
|
998
|
+
_.extend(View.prototype, Events, {
|
999
|
+
|
1000
|
+
// The default `tagName` of a View's element is `"div"`.
|
1001
|
+
tagName: 'div',
|
1002
|
+
|
1003
|
+
// jQuery delegate for element lookup, scoped to DOM elements within the
|
1004
|
+
// current view. This should be prefered to global lookups where possible.
|
1005
|
+
$: function(selector) {
|
1006
|
+
return this.$el.find(selector);
|
1007
|
+
},
|
951
1008
|
|
952
1009
|
// Initialize is an empty function by default. Override it with your own
|
953
1010
|
// initialization logic.
|
954
1011
|
initialize: function(){},
|
955
1012
|
|
956
|
-
//
|
957
|
-
//
|
958
|
-
//
|
959
|
-
|
960
|
-
// });
|
961
|
-
//
|
962
|
-
route: function(route, name, callback) {
|
963
|
-
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
964
|
-
if (!callback) callback = this[name];
|
965
|
-
Backbone.history.route(route, _.bind(function(fragment) {
|
966
|
-
var args = this._extractParameters(route, fragment);
|
967
|
-
callback && callback.apply(this, args);
|
968
|
-
this.trigger.apply(this, ['route:' + name].concat(args));
|
969
|
-
this.trigger('route', name, args);
|
970
|
-
Backbone.history.trigger('route', this, name, args);
|
971
|
-
}, this));
|
1013
|
+
// **render** is the core function that your view should override, in order
|
1014
|
+
// to populate its element (`this.el`), with the appropriate HTML. The
|
1015
|
+
// convention is for **render** to always return `this`.
|
1016
|
+
render: function() {
|
972
1017
|
return this;
|
973
1018
|
},
|
974
1019
|
|
975
|
-
//
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
1020
|
+
// Remove this view by taking the element out of the DOM, and removing any
|
1021
|
+
// applicable Backbone.Events listeners.
|
1022
|
+
remove: function() {
|
1023
|
+
this.$el.remove();
|
1024
|
+
this.stopListening();
|
1025
|
+
return this;
|
1026
|
+
},
|
1027
|
+
|
1028
|
+
// Change the view's element (`this.el` property), including event
|
1029
|
+
// re-delegation.
|
1030
|
+
setElement: function(element, delegate) {
|
1031
|
+
if (this.$el) this.undelegateEvents();
|
1032
|
+
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
|
1033
|
+
this.el = this.$el[0];
|
1034
|
+
if (delegate !== false) this.delegateEvents();
|
1035
|
+
return this;
|
1036
|
+
},
|
1037
|
+
|
1038
|
+
// Set callbacks, where `this.events` is a hash of
|
1039
|
+
//
|
1040
|
+
// *{"event selector": "callback"}*
|
1041
|
+
//
|
1042
|
+
// {
|
1043
|
+
// 'mousedown .title': 'edit',
|
1044
|
+
// 'click .button': 'save'
|
1045
|
+
// 'click .open': function(e) { ... }
|
1046
|
+
// }
|
1047
|
+
//
|
1048
|
+
// pairs. Callbacks will be bound to the view, with `this` set properly.
|
1049
|
+
// Uses event delegation for efficiency.
|
1050
|
+
// Omitting the selector binds the event to `this.el`.
|
1051
|
+
// This only works for delegate-able events: not `focus`, `blur`, and
|
1052
|
+
// not `change`, `submit`, and `reset` in Internet Explorer.
|
1053
|
+
delegateEvents: function(events) {
|
1054
|
+
if (!(events || (events = _.result(this, 'events')))) return this;
|
1055
|
+
this.undelegateEvents();
|
1056
|
+
for (var key in events) {
|
1057
|
+
var method = events[key];
|
1058
|
+
if (!_.isFunction(method)) method = this[events[key]];
|
1059
|
+
if (!method) continue;
|
1060
|
+
|
1061
|
+
var match = key.match(delegateEventSplitter);
|
1062
|
+
var eventName = match[1], selector = match[2];
|
1063
|
+
method = _.bind(method, this);
|
1064
|
+
eventName += '.delegateEvents' + this.cid;
|
1065
|
+
if (selector === '') {
|
1066
|
+
this.$el.on(eventName, method);
|
1067
|
+
} else {
|
1068
|
+
this.$el.on(eventName, selector, method);
|
1069
|
+
}
|
1070
|
+
}
|
1071
|
+
return this;
|
1072
|
+
},
|
1073
|
+
|
1074
|
+
// Clears all callbacks previously bound to the view with `delegateEvents`.
|
1075
|
+
// You usually don't need to use this, but may wish to if you have multiple
|
1076
|
+
// Backbone views attached to the same DOM element.
|
1077
|
+
undelegateEvents: function() {
|
1078
|
+
this.$el.off('.delegateEvents' + this.cid);
|
1079
|
+
return this;
|
1080
|
+
},
|
1081
|
+
|
1082
|
+
// Performs the initial configuration of a View with a set of options.
|
1083
|
+
// Keys with special meaning *(e.g. model, collection, id, className)* are
|
1084
|
+
// attached directly to the view. See `viewOptions` for an exhaustive
|
1085
|
+
// list.
|
1086
|
+
_configure: function(options) {
|
1087
|
+
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
|
1088
|
+
_.extend(this, _.pick(options, viewOptions));
|
1089
|
+
this.options = options;
|
1090
|
+
},
|
1091
|
+
|
1092
|
+
// Ensure that the View has a DOM element to render into.
|
1093
|
+
// If `this.el` is a string, pass it through `$()`, take the first
|
1094
|
+
// matching element, and re-assign it to `el`. Otherwise, create
|
1095
|
+
// an element from the `id`, `className` and `tagName` properties.
|
1096
|
+
_ensureElement: function() {
|
1097
|
+
if (!this.el) {
|
1098
|
+
var attrs = _.extend({}, _.result(this, 'attributes'));
|
1099
|
+
if (this.id) attrs.id = _.result(this, 'id');
|
1100
|
+
if (this.className) attrs['class'] = _.result(this, 'className');
|
1101
|
+
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
|
1102
|
+
this.setElement($el, false);
|
1103
|
+
} else {
|
1104
|
+
this.setElement(_.result(this, 'el'), false);
|
1105
|
+
}
|
1106
|
+
}
|
1107
|
+
|
1108
|
+
});
|
1109
|
+
|
1110
|
+
// Backbone.sync
|
1111
|
+
// -------------
|
1112
|
+
|
1113
|
+
// Override this function to change the manner in which Backbone persists
|
1114
|
+
// models to the server. You will be passed the type of request, and the
|
1115
|
+
// model in question. By default, makes a RESTful Ajax request
|
1116
|
+
// to the model's `url()`. Some possible customizations could be:
|
1117
|
+
//
|
1118
|
+
// * Use `setTimeout` to batch rapid-fire updates into a single request.
|
1119
|
+
// * Send up the models as XML instead of JSON.
|
1120
|
+
// * Persist models via WebSockets instead of Ajax.
|
1121
|
+
//
|
1122
|
+
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
|
1123
|
+
// as `POST`, with a `_method` parameter containing the true HTTP method,
|
1124
|
+
// as well as all requests with the body as `application/x-www-form-urlencoded`
|
1125
|
+
// instead of `application/json` with the model in a param named `model`.
|
1126
|
+
// Useful when interfacing with server-side languages like **PHP** that make
|
1127
|
+
// it difficult to read the body of `PUT` requests.
|
1128
|
+
Backbone.sync = function(method, model, options) {
|
1129
|
+
var type = methodMap[method];
|
1130
|
+
|
1131
|
+
// Default options, unless specified.
|
1132
|
+
_.defaults(options || (options = {}), {
|
1133
|
+
emulateHTTP: Backbone.emulateHTTP,
|
1134
|
+
emulateJSON: Backbone.emulateJSON
|
1135
|
+
});
|
1136
|
+
|
1137
|
+
// Default JSON-request options.
|
1138
|
+
var params = {type: type, dataType: 'json'};
|
1139
|
+
|
1140
|
+
// Ensure that we have a URL.
|
1141
|
+
if (!options.url) {
|
1142
|
+
params.url = _.result(model, 'url') || urlError();
|
1143
|
+
}
|
1144
|
+
|
1145
|
+
// Ensure that we have the appropriate request data.
|
1146
|
+
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
1147
|
+
params.contentType = 'application/json';
|
1148
|
+
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
1149
|
+
}
|
1150
|
+
|
1151
|
+
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
1152
|
+
if (options.emulateJSON) {
|
1153
|
+
params.contentType = 'application/x-www-form-urlencoded';
|
1154
|
+
params.data = params.data ? {model: params.data} : {};
|
1155
|
+
}
|
1156
|
+
|
1157
|
+
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
1158
|
+
// And an `X-HTTP-Method-Override` header.
|
1159
|
+
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
|
1160
|
+
params.type = 'POST';
|
1161
|
+
if (options.emulateJSON) params.data._method = type;
|
1162
|
+
var beforeSend = options.beforeSend;
|
1163
|
+
options.beforeSend = function(xhr) {
|
1164
|
+
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
1165
|
+
if (beforeSend) return beforeSend.apply(this, arguments);
|
1166
|
+
};
|
1167
|
+
}
|
1168
|
+
|
1169
|
+
// Don't process data on a non-GET request.
|
1170
|
+
if (params.type !== 'GET' && !options.emulateJSON) {
|
1171
|
+
params.processData = false;
|
1172
|
+
}
|
1173
|
+
|
1174
|
+
// If we're sending a `PATCH` request, and we're in an old Internet Explorer
|
1175
|
+
// that still has ActiveX enabled by default, override jQuery to use that
|
1176
|
+
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
|
1177
|
+
if (params.type === 'PATCH' && window.ActiveXObject &&
|
1178
|
+
!(window.external && window.external.msActiveXFilteringEnabled)) {
|
1179
|
+
params.xhr = function() {
|
1180
|
+
return new ActiveXObject("Microsoft.XMLHTTP");
|
1181
|
+
};
|
1182
|
+
}
|
1183
|
+
|
1184
|
+
// Make the request, allowing the user to override any Ajax options.
|
1185
|
+
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
|
1186
|
+
model.trigger('request', model, xhr, options);
|
1187
|
+
return xhr;
|
1188
|
+
};
|
1189
|
+
|
1190
|
+
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
1191
|
+
var methodMap = {
|
1192
|
+
'create': 'POST',
|
1193
|
+
'update': 'PUT',
|
1194
|
+
'patch': 'PATCH',
|
1195
|
+
'delete': 'DELETE',
|
1196
|
+
'read': 'GET'
|
1197
|
+
};
|
1198
|
+
|
1199
|
+
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
|
1200
|
+
// Override this if you'd like to use a different library.
|
1201
|
+
Backbone.ajax = function() {
|
1202
|
+
return Backbone.$.ajax.apply(Backbone.$, arguments);
|
1203
|
+
};
|
1204
|
+
|
1205
|
+
// Backbone.Router
|
1206
|
+
// ---------------
|
1207
|
+
|
1208
|
+
// Routers map faux-URLs to actions, and fire events when routes are
|
1209
|
+
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
1210
|
+
var Router = Backbone.Router = function(options) {
|
1211
|
+
options || (options = {});
|
1212
|
+
if (options.routes) this.routes = options.routes;
|
1213
|
+
this._bindRoutes();
|
1214
|
+
this.initialize.apply(this, arguments);
|
1215
|
+
};
|
1216
|
+
|
1217
|
+
// Cached regular expressions for matching named param parts and splatted
|
1218
|
+
// parts of route strings.
|
1219
|
+
var optionalParam = /\((.*?)\)/g;
|
1220
|
+
var namedParam = /(\(\?)?:\w+/g;
|
1221
|
+
var splatParam = /\*\w+/g;
|
1222
|
+
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
1223
|
+
|
1224
|
+
// Set up all inheritable **Backbone.Router** properties and methods.
|
1225
|
+
_.extend(Router.prototype, Events, {
|
1226
|
+
|
1227
|
+
// Initialize is an empty function by default. Override it with your own
|
1228
|
+
// initialization logic.
|
1229
|
+
initialize: function(){},
|
1230
|
+
|
1231
|
+
// Manually bind a single named route to a callback. For example:
|
1232
|
+
//
|
1233
|
+
// this.route('search/:query/p:num', 'search', function(query, num) {
|
1234
|
+
// ...
|
1235
|
+
// });
|
1236
|
+
//
|
1237
|
+
route: function(route, name, callback) {
|
1238
|
+
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
1239
|
+
if (_.isFunction(name)) {
|
1240
|
+
callback = name;
|
1241
|
+
name = '';
|
1242
|
+
}
|
1243
|
+
if (!callback) callback = this[name];
|
1244
|
+
var router = this;
|
1245
|
+
Backbone.history.route(route, function(fragment) {
|
1246
|
+
var args = router._extractParameters(route, fragment);
|
1247
|
+
callback && callback.apply(router, args);
|
1248
|
+
router.trigger.apply(router, ['route:' + name].concat(args));
|
1249
|
+
router.trigger('route', name, args);
|
1250
|
+
Backbone.history.trigger('route', router, name, args);
|
1251
|
+
});
|
1252
|
+
return this;
|
1253
|
+
},
|
1254
|
+
|
1255
|
+
// Simple proxy to `Backbone.history` to save a fragment into the history.
|
1256
|
+
navigate: function(fragment, options) {
|
1257
|
+
Backbone.history.navigate(fragment, options);
|
1258
|
+
return this;
|
1259
|
+
},
|
980
1260
|
|
981
1261
|
// Bind all defined routes to `Backbone.history`. We have to reverse the
|
982
1262
|
// order of the routes here to support behavior where the most general
|
983
1263
|
// routes can be defined at the bottom of the route map.
|
984
1264
|
_bindRoutes: function() {
|
985
1265
|
if (!this.routes) return;
|
1266
|
+
this.routes = _.result(this, 'routes');
|
986
1267
|
var route, routes = _.keys(this.routes);
|
987
1268
|
while ((route = routes.pop()) != null) {
|
988
1269
|
this.route(route, this.routes[route]);
|
@@ -1002,9 +1283,13 @@
|
|
1002
1283
|
},
|
1003
1284
|
|
1004
1285
|
// Given a route, and a URL fragment that it matches, return the array of
|
1005
|
-
// extracted parameters.
|
1286
|
+
// extracted decoded parameters. Empty or unmatched parameters will be
|
1287
|
+
// treated as `null` to normalize cross-browser behavior.
|
1006
1288
|
_extractParameters: function(route, fragment) {
|
1007
|
-
|
1289
|
+
var params = route.exec(fragment).slice(1);
|
1290
|
+
return _.map(params, function(param) {
|
1291
|
+
return param ? decodeURIComponent(param) : null;
|
1292
|
+
});
|
1008
1293
|
}
|
1009
1294
|
|
1010
1295
|
});
|
@@ -1012,8 +1297,11 @@
|
|
1012
1297
|
// Backbone.History
|
1013
1298
|
// ----------------
|
1014
1299
|
|
1015
|
-
// Handles cross-browser history management, based on
|
1016
|
-
//
|
1300
|
+
// Handles cross-browser history management, based on either
|
1301
|
+
// [pushState](http://diveintohtml5.info/history.html) and real URLs, or
|
1302
|
+
// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
|
1303
|
+
// and URL fragments. If the browser supports neither (old IE, natch),
|
1304
|
+
// falls back to polling.
|
1017
1305
|
var History = Backbone.History = function() {
|
1018
1306
|
this.handlers = [];
|
1019
1307
|
_.bindAll(this, 'checkUrl');
|
@@ -1224,230 +1512,6 @@
|
|
1224
1512
|
// Create the default Backbone.history.
|
1225
1513
|
Backbone.history = new History;
|
1226
1514
|
|
1227
|
-
// Backbone.View
|
1228
|
-
// -------------
|
1229
|
-
|
1230
|
-
// Creating a Backbone.View creates its initial element outside of the DOM,
|
1231
|
-
// if an existing element is not provided...
|
1232
|
-
var View = Backbone.View = function(options) {
|
1233
|
-
this.cid = _.uniqueId('view');
|
1234
|
-
this._configure(options || {});
|
1235
|
-
this._ensureElement();
|
1236
|
-
this.initialize.apply(this, arguments);
|
1237
|
-
this.delegateEvents();
|
1238
|
-
};
|
1239
|
-
|
1240
|
-
// Cached regex to split keys for `delegate`.
|
1241
|
-
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1242
|
-
|
1243
|
-
// List of view options to be merged as properties.
|
1244
|
-
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
1245
|
-
|
1246
|
-
// Set up all inheritable **Backbone.View** properties and methods.
|
1247
|
-
_.extend(View.prototype, Events, {
|
1248
|
-
|
1249
|
-
// The default `tagName` of a View's element is `"div"`.
|
1250
|
-
tagName: 'div',
|
1251
|
-
|
1252
|
-
// jQuery delegate for element lookup, scoped to DOM elements within the
|
1253
|
-
// current view. This should be prefered to global lookups where possible.
|
1254
|
-
$: function(selector) {
|
1255
|
-
return this.$el.find(selector);
|
1256
|
-
},
|
1257
|
-
|
1258
|
-
// Initialize is an empty function by default. Override it with your own
|
1259
|
-
// initialization logic.
|
1260
|
-
initialize: function(){},
|
1261
|
-
|
1262
|
-
// **render** is the core function that your view should override, in order
|
1263
|
-
// to populate its element (`this.el`), with the appropriate HTML. The
|
1264
|
-
// convention is for **render** to always return `this`.
|
1265
|
-
render: function() {
|
1266
|
-
return this;
|
1267
|
-
},
|
1268
|
-
|
1269
|
-
// Remove this view by taking the element out of the DOM, and removing any
|
1270
|
-
// applicable Backbone.Events listeners.
|
1271
|
-
remove: function() {
|
1272
|
-
this.$el.remove();
|
1273
|
-
this.stopListening();
|
1274
|
-
return this;
|
1275
|
-
},
|
1276
|
-
|
1277
|
-
// Change the view's element (`this.el` property), including event
|
1278
|
-
// re-delegation.
|
1279
|
-
setElement: function(element, delegate) {
|
1280
|
-
if (this.$el) this.undelegateEvents();
|
1281
|
-
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
|
1282
|
-
this.el = this.$el[0];
|
1283
|
-
if (delegate !== false) this.delegateEvents();
|
1284
|
-
return this;
|
1285
|
-
},
|
1286
|
-
|
1287
|
-
// Set callbacks, where `this.events` is a hash of
|
1288
|
-
//
|
1289
|
-
// *{"event selector": "callback"}*
|
1290
|
-
//
|
1291
|
-
// {
|
1292
|
-
// 'mousedown .title': 'edit',
|
1293
|
-
// 'click .button': 'save'
|
1294
|
-
// 'click .open': function(e) { ... }
|
1295
|
-
// }
|
1296
|
-
//
|
1297
|
-
// pairs. Callbacks will be bound to the view, with `this` set properly.
|
1298
|
-
// Uses event delegation for efficiency.
|
1299
|
-
// Omitting the selector binds the event to `this.el`.
|
1300
|
-
// This only works for delegate-able events: not `focus`, `blur`, and
|
1301
|
-
// not `change`, `submit`, and `reset` in Internet Explorer.
|
1302
|
-
delegateEvents: function(events) {
|
1303
|
-
if (!(events || (events = _.result(this, 'events')))) return;
|
1304
|
-
this.undelegateEvents();
|
1305
|
-
for (var key in events) {
|
1306
|
-
var method = events[key];
|
1307
|
-
if (!_.isFunction(method)) method = this[events[key]];
|
1308
|
-
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
|
1309
|
-
var match = key.match(delegateEventSplitter);
|
1310
|
-
var eventName = match[1], selector = match[2];
|
1311
|
-
method = _.bind(method, this);
|
1312
|
-
eventName += '.delegateEvents' + this.cid;
|
1313
|
-
if (selector === '') {
|
1314
|
-
this.$el.on(eventName, method);
|
1315
|
-
} else {
|
1316
|
-
this.$el.on(eventName, selector, method);
|
1317
|
-
}
|
1318
|
-
}
|
1319
|
-
},
|
1320
|
-
|
1321
|
-
// Clears all callbacks previously bound to the view with `delegateEvents`.
|
1322
|
-
// You usually don't need to use this, but may wish to if you have multiple
|
1323
|
-
// Backbone views attached to the same DOM element.
|
1324
|
-
undelegateEvents: function() {
|
1325
|
-
this.$el.off('.delegateEvents' + this.cid);
|
1326
|
-
},
|
1327
|
-
|
1328
|
-
// Performs the initial configuration of a View with a set of options.
|
1329
|
-
// Keys with special meaning *(model, collection, id, className)*, are
|
1330
|
-
// attached directly to the view.
|
1331
|
-
_configure: function(options) {
|
1332
|
-
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
|
1333
|
-
_.extend(this, _.pick(options, viewOptions));
|
1334
|
-
this.options = options;
|
1335
|
-
},
|
1336
|
-
|
1337
|
-
// Ensure that the View has a DOM element to render into.
|
1338
|
-
// If `this.el` is a string, pass it through `$()`, take the first
|
1339
|
-
// matching element, and re-assign it to `el`. Otherwise, create
|
1340
|
-
// an element from the `id`, `className` and `tagName` properties.
|
1341
|
-
_ensureElement: function() {
|
1342
|
-
if (!this.el) {
|
1343
|
-
var attrs = _.extend({}, _.result(this, 'attributes'));
|
1344
|
-
if (this.id) attrs.id = _.result(this, 'id');
|
1345
|
-
if (this.className) attrs['class'] = _.result(this, 'className');
|
1346
|
-
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
|
1347
|
-
this.setElement($el, false);
|
1348
|
-
} else {
|
1349
|
-
this.setElement(_.result(this, 'el'), false);
|
1350
|
-
}
|
1351
|
-
}
|
1352
|
-
|
1353
|
-
});
|
1354
|
-
|
1355
|
-
// Backbone.sync
|
1356
|
-
// -------------
|
1357
|
-
|
1358
|
-
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
1359
|
-
var methodMap = {
|
1360
|
-
'create': 'POST',
|
1361
|
-
'update': 'PUT',
|
1362
|
-
'patch': 'PATCH',
|
1363
|
-
'delete': 'DELETE',
|
1364
|
-
'read': 'GET'
|
1365
|
-
};
|
1366
|
-
|
1367
|
-
// Override this function to change the manner in which Backbone persists
|
1368
|
-
// models to the server. You will be passed the type of request, and the
|
1369
|
-
// model in question. By default, makes a RESTful Ajax request
|
1370
|
-
// to the model's `url()`. Some possible customizations could be:
|
1371
|
-
//
|
1372
|
-
// * Use `setTimeout` to batch rapid-fire updates into a single request.
|
1373
|
-
// * Send up the models as XML instead of JSON.
|
1374
|
-
// * Persist models via WebSockets instead of Ajax.
|
1375
|
-
//
|
1376
|
-
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
|
1377
|
-
// as `POST`, with a `_method` parameter containing the true HTTP method,
|
1378
|
-
// as well as all requests with the body as `application/x-www-form-urlencoded`
|
1379
|
-
// instead of `application/json` with the model in a param named `model`.
|
1380
|
-
// Useful when interfacing with server-side languages like **PHP** that make
|
1381
|
-
// it difficult to read the body of `PUT` requests.
|
1382
|
-
Backbone.sync = function(method, model, options) {
|
1383
|
-
var type = methodMap[method];
|
1384
|
-
|
1385
|
-
// Default options, unless specified.
|
1386
|
-
_.defaults(options || (options = {}), {
|
1387
|
-
emulateHTTP: Backbone.emulateHTTP,
|
1388
|
-
emulateJSON: Backbone.emulateJSON
|
1389
|
-
});
|
1390
|
-
|
1391
|
-
// Default JSON-request options.
|
1392
|
-
var params = {type: type, dataType: 'json'};
|
1393
|
-
|
1394
|
-
// Ensure that we have a URL.
|
1395
|
-
if (!options.url) {
|
1396
|
-
params.url = _.result(model, 'url') || urlError();
|
1397
|
-
}
|
1398
|
-
|
1399
|
-
// Ensure that we have the appropriate request data.
|
1400
|
-
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
1401
|
-
params.contentType = 'application/json';
|
1402
|
-
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
1403
|
-
}
|
1404
|
-
|
1405
|
-
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
1406
|
-
if (options.emulateJSON) {
|
1407
|
-
params.contentType = 'application/x-www-form-urlencoded';
|
1408
|
-
params.data = params.data ? {model: params.data} : {};
|
1409
|
-
}
|
1410
|
-
|
1411
|
-
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
1412
|
-
// And an `X-HTTP-Method-Override` header.
|
1413
|
-
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
|
1414
|
-
params.type = 'POST';
|
1415
|
-
if (options.emulateJSON) params.data._method = type;
|
1416
|
-
var beforeSend = options.beforeSend;
|
1417
|
-
options.beforeSend = function(xhr) {
|
1418
|
-
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
1419
|
-
if (beforeSend) return beforeSend.apply(this, arguments);
|
1420
|
-
};
|
1421
|
-
}
|
1422
|
-
|
1423
|
-
// Don't process data on a non-GET request.
|
1424
|
-
if (params.type !== 'GET' && !options.emulateJSON) {
|
1425
|
-
params.processData = false;
|
1426
|
-
}
|
1427
|
-
|
1428
|
-
var success = options.success;
|
1429
|
-
options.success = function(resp) {
|
1430
|
-
if (success) success(model, resp, options);
|
1431
|
-
model.trigger('sync', model, resp, options);
|
1432
|
-
};
|
1433
|
-
|
1434
|
-
var error = options.error;
|
1435
|
-
options.error = function(xhr) {
|
1436
|
-
if (error) error(model, xhr, options);
|
1437
|
-
model.trigger('error', model, xhr, options);
|
1438
|
-
};
|
1439
|
-
|
1440
|
-
// Make the request, allowing the user to override any Ajax options.
|
1441
|
-
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
|
1442
|
-
model.trigger('request', model, xhr, options);
|
1443
|
-
return xhr;
|
1444
|
-
};
|
1445
|
-
|
1446
|
-
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
|
1447
|
-
Backbone.ajax = function() {
|
1448
|
-
return Backbone.$.ajax.apply(Backbone.$, arguments);
|
1449
|
-
};
|
1450
|
-
|
1451
1515
|
// Helpers
|
1452
1516
|
// -------
|
1453
1517
|
|
@@ -1495,4 +1559,13 @@
|
|
1495
1559
|
throw new Error('A "url" property or function must be specified');
|
1496
1560
|
};
|
1497
1561
|
|
1562
|
+
// Wrap an optional error callback with a fallback error event.
|
1563
|
+
var wrapError = function (model, options) {
|
1564
|
+
var error = options.error;
|
1565
|
+
options.error = function(resp) {
|
1566
|
+
if (error) error(model, resp, options);
|
1567
|
+
model.trigger('error', model, resp, options);
|
1568
|
+
};
|
1569
|
+
};
|
1570
|
+
|
1498
1571
|
}).call(this);
|