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