backbone-rails 0.9.1 → 0.9.2
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 +280 -139
- metadata +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Backbone.js 0.9.
|
|
1
|
+
// Backbone.js 0.9.2
|
|
2
2
|
|
|
3
3
|
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
|
4
4
|
// Backbone may be freely distributed under the MIT license.
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Current version of the library. Keep in sync with `package.json`.
|
|
35
|
-
Backbone.VERSION = '0.9.
|
|
35
|
+
Backbone.VERSION = '0.9.2';
|
|
36
36
|
|
|
37
37
|
// Require Underscore, if we're on the server, and it's not already present.
|
|
38
38
|
var _ = root._;
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
// Backbone.Events
|
|
72
72
|
// -----------------
|
|
73
73
|
|
|
74
|
+
// Regular expression used to split event strings
|
|
75
|
+
var eventSplitter = /\s+/;
|
|
76
|
+
|
|
74
77
|
// A module that can be mixed in to *any object* in order to provide it with
|
|
75
78
|
// custom events. You may bind with `on` or remove with `off` callback functions
|
|
76
79
|
// to an event; trigger`-ing an event fires all callbacks in succession.
|
|
@@ -80,89 +83,110 @@
|
|
|
80
83
|
// object.on('expand', function(){ alert('expanded'); });
|
|
81
84
|
// object.trigger('expand');
|
|
82
85
|
//
|
|
83
|
-
Backbone.Events = {
|
|
86
|
+
var Events = Backbone.Events = {
|
|
84
87
|
|
|
85
|
-
// Bind
|
|
88
|
+
// Bind one or more space separated events, `events`, to a `callback`
|
|
86
89
|
// function. Passing `"all"` will bind the callback to all events fired.
|
|
87
90
|
on: function(events, callback, context) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
|
|
92
|
+
var calls, event, node, tail, list;
|
|
93
|
+
if (!callback) return this;
|
|
94
|
+
events = events.split(eventSplitter);
|
|
95
|
+
calls = this._callbacks || (this._callbacks = {});
|
|
96
|
+
|
|
97
|
+
// Create an immutable callback list, allowing traversal during
|
|
98
|
+
// modification. The tail is an empty object that will always be used
|
|
99
|
+
// as the next node.
|
|
100
|
+
while (event = events.shift()) {
|
|
101
|
+
list = calls[event];
|
|
102
|
+
node = list ? list.tail : {};
|
|
103
|
+
node.next = tail = {};
|
|
104
|
+
node.context = context;
|
|
105
|
+
node.callback = callback;
|
|
106
|
+
calls[event] = {tail: tail, next: list ? list.next : node};
|
|
100
107
|
}
|
|
108
|
+
|
|
101
109
|
return this;
|
|
102
110
|
},
|
|
103
111
|
|
|
104
112
|
// Remove one or many callbacks. If `context` is null, removes all callbacks
|
|
105
113
|
// with that function. If `callback` is null, removes all callbacks for the
|
|
106
|
-
// event. If `
|
|
114
|
+
// event. If `events` is null, removes all bound callbacks for all events.
|
|
107
115
|
off: function(events, callback, context) {
|
|
108
|
-
var
|
|
109
|
-
|
|
116
|
+
var event, calls, node, tail, cb, ctx;
|
|
117
|
+
|
|
118
|
+
// No events, or removing *all* events.
|
|
119
|
+
if (!(calls = this._callbacks)) return;
|
|
120
|
+
if (!(events || callback || context)) {
|
|
110
121
|
delete this._callbacks;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Loop through the listed events and contexts, splicing them out of the
|
|
126
|
+
// linked list of callbacks if appropriate.
|
|
127
|
+
events = events ? events.split(eventSplitter) : _.keys(calls);
|
|
128
|
+
while (event = events.shift()) {
|
|
129
|
+
node = calls[event];
|
|
130
|
+
delete calls[event];
|
|
131
|
+
if (!node || !(callback || context)) continue;
|
|
132
|
+
// Create a new list, omitting the indicated callbacks.
|
|
133
|
+
tail = node.tail;
|
|
134
|
+
while ((node = node.next) !== tail) {
|
|
135
|
+
cb = node.callback;
|
|
136
|
+
ctx = node.context;
|
|
137
|
+
if ((callback && cb !== callback) || (context && ctx !== context)) {
|
|
138
|
+
this.on(event, cb, ctx);
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
141
|
}
|
|
142
|
+
|
|
125
143
|
return this;
|
|
126
144
|
},
|
|
127
145
|
|
|
128
|
-
// Trigger
|
|
129
|
-
// same arguments as `trigger` is, apart from the event name
|
|
130
|
-
//
|
|
146
|
+
// Trigger one or many events, firing all bound callbacks. Callbacks are
|
|
147
|
+
// passed the same arguments as `trigger` is, apart from the event name
|
|
148
|
+
// (unless you're listening on `"all"`, which will cause your callback to
|
|
149
|
+
// receive the true name of the event as the first argument).
|
|
131
150
|
trigger: function(events) {
|
|
132
151
|
var event, node, calls, tail, args, all, rest;
|
|
133
152
|
if (!(calls = this._callbacks)) return this;
|
|
134
|
-
all = calls
|
|
135
|
-
|
|
136
|
-
// Save references to the current heads & tails.
|
|
137
|
-
while (event = events.shift()) {
|
|
138
|
-
if (all) events.push({next: all.next, tail: all.tail, event: event});
|
|
139
|
-
if (!(node = calls[event])) continue;
|
|
140
|
-
events.push({next: node.next, tail: node.tail});
|
|
141
|
-
}
|
|
142
|
-
// Traverse each list, stopping when the saved tail is reached.
|
|
153
|
+
all = calls.all;
|
|
154
|
+
events = events.split(eventSplitter);
|
|
143
155
|
rest = slice.call(arguments, 1);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
|
|
157
|
+
// For each event, walk through the linked list of callbacks twice,
|
|
158
|
+
// first to trigger the event, then to trigger any `"all"` callbacks.
|
|
159
|
+
while (event = events.shift()) {
|
|
160
|
+
if (node = calls[event]) {
|
|
161
|
+
tail = node.tail;
|
|
162
|
+
while ((node = node.next) !== tail) {
|
|
163
|
+
node.callback.apply(node.context || this, rest);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (node = all) {
|
|
167
|
+
tail = node.tail;
|
|
168
|
+
args = [event].concat(rest);
|
|
169
|
+
while ((node = node.next) !== tail) {
|
|
170
|
+
node.callback.apply(node.context || this, args);
|
|
171
|
+
}
|
|
149
172
|
}
|
|
150
173
|
}
|
|
174
|
+
|
|
151
175
|
return this;
|
|
152
176
|
}
|
|
153
177
|
|
|
154
178
|
};
|
|
155
179
|
|
|
156
180
|
// Aliases for backwards compatibility.
|
|
157
|
-
|
|
158
|
-
|
|
181
|
+
Events.bind = Events.on;
|
|
182
|
+
Events.unbind = Events.off;
|
|
159
183
|
|
|
160
184
|
// Backbone.Model
|
|
161
185
|
// --------------
|
|
162
186
|
|
|
163
187
|
// Create a new model, with defined attributes. A client id (`cid`)
|
|
164
188
|
// is automatically generated and assigned for you.
|
|
165
|
-
Backbone.Model = function(attributes, options) {
|
|
189
|
+
var Model = Backbone.Model = function(attributes, options) {
|
|
166
190
|
var defaults;
|
|
167
191
|
attributes || (attributes = {});
|
|
168
192
|
if (options && options.parse) attributes = this.parse(attributes);
|
|
@@ -173,16 +197,31 @@
|
|
|
173
197
|
this.attributes = {};
|
|
174
198
|
this._escapedAttributes = {};
|
|
175
199
|
this.cid = _.uniqueId('c');
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
200
|
+
this.changed = {};
|
|
201
|
+
this._silent = {};
|
|
202
|
+
this._pending = {};
|
|
203
|
+
this.set(attributes, {silent: true});
|
|
204
|
+
// Reset change tracking.
|
|
205
|
+
this.changed = {};
|
|
206
|
+
this._silent = {};
|
|
207
|
+
this._pending = {};
|
|
180
208
|
this._previousAttributes = _.clone(this.attributes);
|
|
181
209
|
this.initialize.apply(this, arguments);
|
|
182
210
|
};
|
|
183
211
|
|
|
184
212
|
// Attach all inheritable methods to the Model prototype.
|
|
185
|
-
_.extend(
|
|
213
|
+
_.extend(Model.prototype, Events, {
|
|
214
|
+
|
|
215
|
+
// A hash of attributes whose current and previous value differ.
|
|
216
|
+
changed: null,
|
|
217
|
+
|
|
218
|
+
// A hash of attributes that have silently changed since the last time
|
|
219
|
+
// `change` was called. Will become pending attributes on the next call.
|
|
220
|
+
_silent: null,
|
|
221
|
+
|
|
222
|
+
// A hash of attributes that have changed since the last `'change'` event
|
|
223
|
+
// began.
|
|
224
|
+
_pending: null,
|
|
186
225
|
|
|
187
226
|
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
|
188
227
|
// CouchDB users may want to set this to `"_id"`.
|
|
@@ -193,7 +232,7 @@
|
|
|
193
232
|
initialize: function(){},
|
|
194
233
|
|
|
195
234
|
// Return a copy of the model's `attributes` object.
|
|
196
|
-
toJSON: function() {
|
|
235
|
+
toJSON: function(options) {
|
|
197
236
|
return _.clone(this.attributes);
|
|
198
237
|
},
|
|
199
238
|
|
|
@@ -206,20 +245,22 @@
|
|
|
206
245
|
escape: function(attr) {
|
|
207
246
|
var html;
|
|
208
247
|
if (html = this._escapedAttributes[attr]) return html;
|
|
209
|
-
var val = this.
|
|
248
|
+
var val = this.get(attr);
|
|
210
249
|
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
|
|
211
250
|
},
|
|
212
251
|
|
|
213
252
|
// Returns `true` if the attribute contains a value that is not null
|
|
214
253
|
// or undefined.
|
|
215
254
|
has: function(attr) {
|
|
216
|
-
return this.
|
|
255
|
+
return this.get(attr) != null;
|
|
217
256
|
},
|
|
218
257
|
|
|
219
258
|
// Set a hash of model attributes on the object, firing `"change"` unless
|
|
220
259
|
// you choose to silence it.
|
|
221
260
|
set: function(key, value, options) {
|
|
222
261
|
var attrs, attr, val;
|
|
262
|
+
|
|
263
|
+
// Handle both `"key", value` and `{key: value}` -style arguments.
|
|
223
264
|
if (_.isObject(key) || key == null) {
|
|
224
265
|
attrs = key;
|
|
225
266
|
options = value;
|
|
@@ -231,7 +272,7 @@
|
|
|
231
272
|
// Extract attributes and options.
|
|
232
273
|
options || (options = {});
|
|
233
274
|
if (!attrs) return this;
|
|
234
|
-
if (attrs instanceof
|
|
275
|
+
if (attrs instanceof Model) attrs = attrs.attributes;
|
|
235
276
|
if (options.unset) for (attr in attrs) attrs[attr] = void 0;
|
|
236
277
|
|
|
237
278
|
// Run validation.
|
|
@@ -240,33 +281,37 @@
|
|
|
240
281
|
// Check for changes of `id`.
|
|
241
282
|
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
|
242
283
|
|
|
284
|
+
var changes = options.changes = {};
|
|
243
285
|
var now = this.attributes;
|
|
244
286
|
var escaped = this._escapedAttributes;
|
|
245
287
|
var prev = this._previousAttributes || {};
|
|
246
|
-
var alreadySetting = this._setting;
|
|
247
|
-
this._changed || (this._changed = {});
|
|
248
|
-
this._setting = true;
|
|
249
288
|
|
|
250
|
-
//
|
|
289
|
+
// For each `set` attribute...
|
|
251
290
|
for (attr in attrs) {
|
|
252
291
|
val = attrs[attr];
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
this.
|
|
292
|
+
|
|
293
|
+
// If the new and current value differ, record the change.
|
|
294
|
+
if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
|
|
295
|
+
delete escaped[attr];
|
|
296
|
+
(options.silent ? this._silent : changes)[attr] = true;
|
|
258
297
|
}
|
|
259
|
-
|
|
298
|
+
|
|
299
|
+
// Update or delete the current value.
|
|
300
|
+
options.unset ? delete now[attr] : now[attr] = val;
|
|
301
|
+
|
|
302
|
+
// If the new and previous value differ, record the change. If not,
|
|
303
|
+
// then remove changes for this attribute.
|
|
260
304
|
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
|
|
261
|
-
this.
|
|
305
|
+
this.changed[attr] = val;
|
|
306
|
+
if (!options.silent) this._pending[attr] = true;
|
|
307
|
+
} else {
|
|
308
|
+
delete this.changed[attr];
|
|
309
|
+
delete this._pending[attr];
|
|
262
310
|
}
|
|
263
311
|
}
|
|
264
312
|
|
|
265
|
-
// Fire the `"change"` events
|
|
266
|
-
if (!
|
|
267
|
-
if (!options.silent && this.hasChanged()) this.change(options);
|
|
268
|
-
this._setting = false;
|
|
269
|
-
}
|
|
313
|
+
// Fire the `"change"` events.
|
|
314
|
+
if (!options.silent) this.change(options);
|
|
270
315
|
return this;
|
|
271
316
|
},
|
|
272
317
|
|
|
@@ -304,6 +349,8 @@
|
|
|
304
349
|
// state will be `set` again.
|
|
305
350
|
save: function(key, value, options) {
|
|
306
351
|
var attrs, current;
|
|
352
|
+
|
|
353
|
+
// Handle both `("key", value)` and `({key: value})` -style calls.
|
|
307
354
|
if (_.isObject(key) || key == null) {
|
|
308
355
|
attrs = key;
|
|
309
356
|
options = value;
|
|
@@ -311,18 +358,30 @@
|
|
|
311
358
|
attrs = {};
|
|
312
359
|
attrs[key] = value;
|
|
313
360
|
}
|
|
314
|
-
|
|
315
361
|
options = options ? _.clone(options) : {};
|
|
316
|
-
|
|
362
|
+
|
|
363
|
+
// If we're "wait"-ing to set changed attributes, validate early.
|
|
364
|
+
if (options.wait) {
|
|
365
|
+
if (!this._validate(attrs, options)) return false;
|
|
366
|
+
current = _.clone(this.attributes);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Regular saves `set` attributes before persisting to the server.
|
|
317
370
|
var silentOptions = _.extend({}, options, {silent: true});
|
|
318
371
|
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
|
|
319
372
|
return false;
|
|
320
373
|
}
|
|
374
|
+
|
|
375
|
+
// After a successful server-side save, the client is (optionally)
|
|
376
|
+
// updated with the server-side state.
|
|
321
377
|
var model = this;
|
|
322
378
|
var success = options.success;
|
|
323
379
|
options.success = function(resp, status, xhr) {
|
|
324
380
|
var serverAttrs = model.parse(resp, xhr);
|
|
325
|
-
if (options.wait)
|
|
381
|
+
if (options.wait) {
|
|
382
|
+
delete options.wait;
|
|
383
|
+
serverAttrs = _.extend(attrs || {}, serverAttrs);
|
|
384
|
+
}
|
|
326
385
|
if (!model.set(serverAttrs, options)) return false;
|
|
327
386
|
if (success) {
|
|
328
387
|
success(model, resp);
|
|
@@ -330,6 +389,8 @@
|
|
|
330
389
|
model.trigger('sync', model, resp, options);
|
|
331
390
|
}
|
|
332
391
|
};
|
|
392
|
+
|
|
393
|
+
// Finish configuring and sending the Ajax request.
|
|
333
394
|
options.error = Backbone.wrapError(options.error, model, options);
|
|
334
395
|
var method = this.isNew() ? 'create' : 'update';
|
|
335
396
|
var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
|
|
@@ -349,7 +410,11 @@
|
|
|
349
410
|
model.trigger('destroy', model, model.collection, options);
|
|
350
411
|
};
|
|
351
412
|
|
|
352
|
-
if (this.isNew())
|
|
413
|
+
if (this.isNew()) {
|
|
414
|
+
triggerDestroy();
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
353
418
|
options.success = function(resp) {
|
|
354
419
|
if (options.wait) triggerDestroy();
|
|
355
420
|
if (success) {
|
|
@@ -358,6 +423,7 @@
|
|
|
358
423
|
model.trigger('sync', model, resp, options);
|
|
359
424
|
}
|
|
360
425
|
};
|
|
426
|
+
|
|
361
427
|
options.error = Backbone.wrapError(options.error, model, options);
|
|
362
428
|
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
|
|
363
429
|
if (!options.wait) triggerDestroy();
|
|
@@ -368,7 +434,7 @@
|
|
|
368
434
|
// using Backbone's restful methods, override this to change the endpoint
|
|
369
435
|
// that will be called.
|
|
370
436
|
url: function() {
|
|
371
|
-
var base = getValue(this
|
|
437
|
+
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
|
|
372
438
|
if (this.isNew()) return base;
|
|
373
439
|
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
|
|
374
440
|
},
|
|
@@ -393,18 +459,33 @@
|
|
|
393
459
|
// a `"change:attribute"` event for each changed attribute.
|
|
394
460
|
// Calling this will cause all objects observing the model to update.
|
|
395
461
|
change: function(options) {
|
|
396
|
-
|
|
462
|
+
options || (options = {});
|
|
463
|
+
var changing = this._changing;
|
|
397
464
|
this._changing = true;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
465
|
+
|
|
466
|
+
// Silent changes become pending changes.
|
|
467
|
+
for (var attr in this._silent) this._pending[attr] = true;
|
|
468
|
+
|
|
469
|
+
// Silent changes are triggered.
|
|
470
|
+
var changes = _.extend({}, options.changes, this._silent);
|
|
471
|
+
this._silent = {};
|
|
472
|
+
for (var attr in changes) {
|
|
473
|
+
this.trigger('change:' + attr, this, this.get(attr), options);
|
|
401
474
|
}
|
|
402
|
-
|
|
403
|
-
|
|
475
|
+
if (changing) return this;
|
|
476
|
+
|
|
477
|
+
// Continue firing `"change"` events while there are pending changes.
|
|
478
|
+
while (!_.isEmpty(this._pending)) {
|
|
479
|
+
this._pending = {};
|
|
404
480
|
this.trigger('change', this, options);
|
|
481
|
+
// Pending and silent changes still remain.
|
|
482
|
+
for (var attr in this.changed) {
|
|
483
|
+
if (this._pending[attr] || this._silent[attr]) continue;
|
|
484
|
+
delete this.changed[attr];
|
|
485
|
+
}
|
|
486
|
+
this._previousAttributes = _.clone(this.attributes);
|
|
405
487
|
}
|
|
406
|
-
|
|
407
|
-
delete this._changed;
|
|
488
|
+
|
|
408
489
|
this._changing = false;
|
|
409
490
|
return this;
|
|
410
491
|
},
|
|
@@ -412,8 +493,8 @@
|
|
|
412
493
|
// Determine if the model has changed since the last `"change"` event.
|
|
413
494
|
// If you specify an attribute name, determine if that attribute has changed.
|
|
414
495
|
hasChanged: function(attr) {
|
|
415
|
-
if (!arguments.length) return !_.isEmpty(this.
|
|
416
|
-
return
|
|
496
|
+
if (!arguments.length) return !_.isEmpty(this.changed);
|
|
497
|
+
return _.has(this.changed, attr);
|
|
417
498
|
},
|
|
418
499
|
|
|
419
500
|
// Return an object containing all the attributes that have changed, or
|
|
@@ -423,7 +504,7 @@
|
|
|
423
504
|
// You can also pass an attributes object to diff against the model,
|
|
424
505
|
// determining if there *would be* a change.
|
|
425
506
|
changedAttributes: function(diff) {
|
|
426
|
-
if (!diff) return this.hasChanged() ? _.clone(this.
|
|
507
|
+
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
|
427
508
|
var val, changed = false, old = this._previousAttributes;
|
|
428
509
|
for (var attr in diff) {
|
|
429
510
|
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
|
@@ -451,9 +532,9 @@
|
|
|
451
532
|
return !this.validate(this.attributes);
|
|
452
533
|
},
|
|
453
534
|
|
|
454
|
-
// Run validation against
|
|
455
|
-
// if all is well. If a specific `error` callback has
|
|
456
|
-
// call that instead of firing the general `"error"` event.
|
|
535
|
+
// Run validation against the next complete set of model attributes,
|
|
536
|
+
// returning `true` if all is well. If a specific `error` callback has
|
|
537
|
+
// been passed, call that instead of firing the general `"error"` event.
|
|
457
538
|
_validate: function(attrs, options) {
|
|
458
539
|
if (options.silent || !this.validate) return true;
|
|
459
540
|
attrs = _.extend({}, this.attributes, attrs);
|
|
@@ -475,8 +556,9 @@
|
|
|
475
556
|
// Provides a standard collection class for our sets of models, ordered
|
|
476
557
|
// or unordered. If a `comparator` is specified, the Collection will maintain
|
|
477
558
|
// its models in sort order, as they're added and removed.
|
|
478
|
-
Backbone.Collection = function(models, options) {
|
|
559
|
+
var Collection = Backbone.Collection = function(models, options) {
|
|
479
560
|
options || (options = {});
|
|
561
|
+
if (options.model) this.model = options.model;
|
|
480
562
|
if (options.comparator) this.comparator = options.comparator;
|
|
481
563
|
this._reset();
|
|
482
564
|
this.initialize.apply(this, arguments);
|
|
@@ -484,11 +566,11 @@
|
|
|
484
566
|
};
|
|
485
567
|
|
|
486
568
|
// Define the Collection's inheritable methods.
|
|
487
|
-
_.extend(
|
|
569
|
+
_.extend(Collection.prototype, Events, {
|
|
488
570
|
|
|
489
571
|
// The default model for a collection is just a **Backbone.Model**.
|
|
490
572
|
// This should be overridden in most cases.
|
|
491
|
-
model:
|
|
573
|
+
model: Model,
|
|
492
574
|
|
|
493
575
|
// Initialize is an empty function by default. Override it with your own
|
|
494
576
|
// initialization logic.
|
|
@@ -496,14 +578,14 @@
|
|
|
496
578
|
|
|
497
579
|
// The JSON representation of a Collection is an array of the
|
|
498
580
|
// models' attributes.
|
|
499
|
-
toJSON: function() {
|
|
500
|
-
return this.map(function(model){ return model.toJSON(); });
|
|
581
|
+
toJSON: function(options) {
|
|
582
|
+
return this.map(function(model){ return model.toJSON(options); });
|
|
501
583
|
},
|
|
502
584
|
|
|
503
585
|
// Add a model, or list of models to the set. Pass **silent** to avoid
|
|
504
586
|
// firing the `add` event for every new model.
|
|
505
587
|
add: function(models, options) {
|
|
506
|
-
var i, index, length, model, cid, id, cids = {}, ids = {};
|
|
588
|
+
var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
|
|
507
589
|
options || (options = {});
|
|
508
590
|
models = _.isArray(models) ? models.slice() : [models];
|
|
509
591
|
|
|
@@ -513,16 +595,24 @@
|
|
|
513
595
|
if (!(model = models[i] = this._prepareModel(models[i], options))) {
|
|
514
596
|
throw new Error("Can't add an invalid model to a collection");
|
|
515
597
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
598
|
+
cid = model.cid;
|
|
599
|
+
id = model.id;
|
|
600
|
+
if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
|
|
601
|
+
dups.push(i);
|
|
602
|
+
continue;
|
|
519
603
|
}
|
|
520
604
|
cids[cid] = ids[id] = model;
|
|
521
605
|
}
|
|
522
606
|
|
|
607
|
+
// Remove duplicates.
|
|
608
|
+
i = dups.length;
|
|
609
|
+
while (i--) {
|
|
610
|
+
models.splice(dups[i], 1);
|
|
611
|
+
}
|
|
612
|
+
|
|
523
613
|
// Listen to added models' events, and index models for lookup by
|
|
524
614
|
// `id` and by `cid`.
|
|
525
|
-
for (i = 0; i < length; i++) {
|
|
615
|
+
for (i = 0, length = models.length; i < length; i++) {
|
|
526
616
|
(model = models[i]).on('all', this._onModelEvent, this);
|
|
527
617
|
this._byCid[model.cid] = model;
|
|
528
618
|
if (model.id != null) this._byId[model.id] = model;
|
|
@@ -566,9 +656,37 @@
|
|
|
566
656
|
return this;
|
|
567
657
|
},
|
|
568
658
|
|
|
659
|
+
// Add a model to the end of the collection.
|
|
660
|
+
push: function(model, options) {
|
|
661
|
+
model = this._prepareModel(model, options);
|
|
662
|
+
this.add(model, options);
|
|
663
|
+
return model;
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
// Remove a model from the end of the collection.
|
|
667
|
+
pop: function(options) {
|
|
668
|
+
var model = this.at(this.length - 1);
|
|
669
|
+
this.remove(model, options);
|
|
670
|
+
return model;
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
// Add a model to the beginning of the collection.
|
|
674
|
+
unshift: function(model, options) {
|
|
675
|
+
model = this._prepareModel(model, options);
|
|
676
|
+
this.add(model, _.extend({at: 0}, options));
|
|
677
|
+
return model;
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
// Remove a model from the beginning of the collection.
|
|
681
|
+
shift: function(options) {
|
|
682
|
+
var model = this.at(0);
|
|
683
|
+
this.remove(model, options);
|
|
684
|
+
return model;
|
|
685
|
+
},
|
|
686
|
+
|
|
569
687
|
// Get a model from the set by id.
|
|
570
688
|
get: function(id) {
|
|
571
|
-
if (id == null) return
|
|
689
|
+
if (id == null) return void 0;
|
|
572
690
|
return this._byId[id.id != null ? id.id : id];
|
|
573
691
|
},
|
|
574
692
|
|
|
@@ -582,6 +700,17 @@
|
|
|
582
700
|
return this.models[index];
|
|
583
701
|
},
|
|
584
702
|
|
|
703
|
+
// Return models with matching attributes. Useful for simple cases of `filter`.
|
|
704
|
+
where: function(attrs) {
|
|
705
|
+
if (_.isEmpty(attrs)) return [];
|
|
706
|
+
return this.filter(function(model) {
|
|
707
|
+
for (var key in attrs) {
|
|
708
|
+
if (attrs[key] !== model.get(key)) return false;
|
|
709
|
+
}
|
|
710
|
+
return true;
|
|
711
|
+
});
|
|
712
|
+
},
|
|
713
|
+
|
|
585
714
|
// Force the collection to re-sort itself. You don't need to call this under
|
|
586
715
|
// normal circumstances, as the set will maintain sort order as each item
|
|
587
716
|
// is added.
|
|
@@ -613,7 +742,7 @@
|
|
|
613
742
|
this._removeReference(this.models[i]);
|
|
614
743
|
}
|
|
615
744
|
this._reset();
|
|
616
|
-
this.add(models, {silent: true,
|
|
745
|
+
this.add(models, _.extend({silent: true}, options));
|
|
617
746
|
if (!options.silent) this.trigger('reset', this, options);
|
|
618
747
|
return this;
|
|
619
748
|
},
|
|
@@ -679,7 +808,8 @@
|
|
|
679
808
|
|
|
680
809
|
// Prepare a model or hash of attributes to be added to this collection.
|
|
681
810
|
_prepareModel: function(model, options) {
|
|
682
|
-
|
|
811
|
+
options || (options = {});
|
|
812
|
+
if (!(model instanceof Model)) {
|
|
683
813
|
var attrs = model;
|
|
684
814
|
options.collection = this;
|
|
685
815
|
model = new this.model(attrs, options);
|
|
@@ -702,12 +832,12 @@
|
|
|
702
832
|
// Sets need to update their indexes when models change ids. All other
|
|
703
833
|
// events simply proxy through. "add" and "remove" events that originate
|
|
704
834
|
// in other collections are ignored.
|
|
705
|
-
_onModelEvent: function(
|
|
706
|
-
if ((
|
|
707
|
-
if (
|
|
835
|
+
_onModelEvent: function(event, model, collection, options) {
|
|
836
|
+
if ((event == 'add' || event == 'remove') && collection != this) return;
|
|
837
|
+
if (event == 'destroy') {
|
|
708
838
|
this.remove(model, options);
|
|
709
839
|
}
|
|
710
|
-
if (model &&
|
|
840
|
+
if (model && event === 'change:' + model.idAttribute) {
|
|
711
841
|
delete this._byId[model.previous(model.idAttribute)];
|
|
712
842
|
this._byId[model.id] = model;
|
|
713
843
|
}
|
|
@@ -725,7 +855,7 @@
|
|
|
725
855
|
|
|
726
856
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
|
727
857
|
_.each(methods, function(method) {
|
|
728
|
-
|
|
858
|
+
Collection.prototype[method] = function() {
|
|
729
859
|
return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
|
|
730
860
|
};
|
|
731
861
|
});
|
|
@@ -735,7 +865,7 @@
|
|
|
735
865
|
|
|
736
866
|
// Routers map faux-URLs to actions, and fire events when routes are
|
|
737
867
|
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
|
738
|
-
Backbone.Router = function(options) {
|
|
868
|
+
var Router = Backbone.Router = function(options) {
|
|
739
869
|
options || (options = {});
|
|
740
870
|
if (options.routes) this.routes = options.routes;
|
|
741
871
|
this._bindRoutes();
|
|
@@ -749,7 +879,7 @@
|
|
|
749
879
|
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
|
|
750
880
|
|
|
751
881
|
// Set up all inheritable **Backbone.Router** properties and methods.
|
|
752
|
-
_.extend(
|
|
882
|
+
_.extend(Router.prototype, Events, {
|
|
753
883
|
|
|
754
884
|
// Initialize is an empty function by default. Override it with your own
|
|
755
885
|
// initialization logic.
|
|
@@ -762,7 +892,7 @@
|
|
|
762
892
|
// });
|
|
763
893
|
//
|
|
764
894
|
route: function(route, name, callback) {
|
|
765
|
-
Backbone.history || (Backbone.history = new
|
|
895
|
+
Backbone.history || (Backbone.history = new History);
|
|
766
896
|
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
|
767
897
|
if (!callback) callback = this[name];
|
|
768
898
|
Backbone.history.route(route, _.bind(function(fragment) {
|
|
@@ -815,7 +945,7 @@
|
|
|
815
945
|
|
|
816
946
|
// Handles cross-browser history management, based on URL fragments. If the
|
|
817
947
|
// browser does not support `onhashchange`, falls back to polling.
|
|
818
|
-
Backbone.History = function() {
|
|
948
|
+
var History = Backbone.History = function() {
|
|
819
949
|
this.handlers = [];
|
|
820
950
|
_.bindAll(this, 'checkUrl');
|
|
821
951
|
};
|
|
@@ -827,15 +957,23 @@
|
|
|
827
957
|
var isExplorer = /msie [\w.]+/;
|
|
828
958
|
|
|
829
959
|
// Has the history handling already been started?
|
|
830
|
-
|
|
960
|
+
History.started = false;
|
|
831
961
|
|
|
832
962
|
// Set up all inheritable **Backbone.History** properties and methods.
|
|
833
|
-
_.extend(
|
|
963
|
+
_.extend(History.prototype, Events, {
|
|
834
964
|
|
|
835
965
|
// The default interval to poll for hash changes, if necessary, is
|
|
836
966
|
// twenty times a second.
|
|
837
967
|
interval: 50,
|
|
838
968
|
|
|
969
|
+
// Gets the true hash value. Cannot use location.hash directly due to bug
|
|
970
|
+
// in Firefox where location.hash will always be decoded.
|
|
971
|
+
getHash: function(windowOverride) {
|
|
972
|
+
var loc = windowOverride ? windowOverride.location : window.location;
|
|
973
|
+
var match = loc.href.match(/#(.*)$/);
|
|
974
|
+
return match ? match[1] : '';
|
|
975
|
+
},
|
|
976
|
+
|
|
839
977
|
// Get the cross-browser normalized URL fragment, either from the URL,
|
|
840
978
|
// the hash, or the override.
|
|
841
979
|
getFragment: function(fragment, forcePushState) {
|
|
@@ -845,10 +983,9 @@
|
|
|
845
983
|
var search = window.location.search;
|
|
846
984
|
if (search) fragment += search;
|
|
847
985
|
} else {
|
|
848
|
-
fragment =
|
|
986
|
+
fragment = this.getHash();
|
|
849
987
|
}
|
|
850
988
|
}
|
|
851
|
-
fragment = decodeURIComponent(fragment);
|
|
852
989
|
if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
|
|
853
990
|
return fragment.replace(routeStripper, '');
|
|
854
991
|
},
|
|
@@ -856,10 +993,11 @@
|
|
|
856
993
|
// Start the hash change handling, returning `true` if the current URL matches
|
|
857
994
|
// an existing route, and `false` otherwise.
|
|
858
995
|
start: function(options) {
|
|
996
|
+
if (History.started) throw new Error("Backbone.history has already been started");
|
|
997
|
+
History.started = true;
|
|
859
998
|
|
|
860
999
|
// Figure out the initial configuration. Do we need an iframe?
|
|
861
1000
|
// Is pushState desired ... is it available?
|
|
862
|
-
if (historyStarted) throw new Error("Backbone.history has already been started");
|
|
863
1001
|
this.options = _.extend({}, {root: '/'}, this.options, options);
|
|
864
1002
|
this._wantsHashChange = this.options.hashChange !== false;
|
|
865
1003
|
this._wantsPushState = !!this.options.pushState;
|
|
@@ -867,6 +1005,7 @@
|
|
|
867
1005
|
var fragment = this.getFragment();
|
|
868
1006
|
var docMode = document.documentMode;
|
|
869
1007
|
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
|
1008
|
+
|
|
870
1009
|
if (oldIE) {
|
|
871
1010
|
this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
|
872
1011
|
this.navigate(fragment);
|
|
@@ -885,7 +1024,6 @@
|
|
|
885
1024
|
// Determine if we need to change the base url, for a pushState link
|
|
886
1025
|
// opened by a non-pushState browser.
|
|
887
1026
|
this.fragment = fragment;
|
|
888
|
-
historyStarted = true;
|
|
889
1027
|
var loc = window.location;
|
|
890
1028
|
var atRoot = loc.pathname == this.options.root;
|
|
891
1029
|
|
|
@@ -900,7 +1038,7 @@
|
|
|
900
1038
|
// Or if we've started out with a hash-based route, but we're currently
|
|
901
1039
|
// in a browser where it could be `pushState`-based instead...
|
|
902
1040
|
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
|
903
|
-
this.fragment =
|
|
1041
|
+
this.fragment = this.getHash().replace(routeStripper, '');
|
|
904
1042
|
window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
|
|
905
1043
|
}
|
|
906
1044
|
|
|
@@ -914,7 +1052,7 @@
|
|
|
914
1052
|
stop: function() {
|
|
915
1053
|
$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
|
|
916
1054
|
clearInterval(this._checkUrlInterval);
|
|
917
|
-
|
|
1055
|
+
History.started = false;
|
|
918
1056
|
},
|
|
919
1057
|
|
|
920
1058
|
// Add a route to be tested when the fragment changes. Routes added later
|
|
@@ -927,10 +1065,10 @@
|
|
|
927
1065
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
|
928
1066
|
checkUrl: function(e) {
|
|
929
1067
|
var current = this.getFragment();
|
|
930
|
-
if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe
|
|
931
|
-
if (current == this.fragment
|
|
1068
|
+
if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
|
|
1069
|
+
if (current == this.fragment) return false;
|
|
932
1070
|
if (this.iframe) this.navigate(current);
|
|
933
|
-
this.loadUrl() || this.loadUrl(
|
|
1071
|
+
this.loadUrl() || this.loadUrl(this.getHash());
|
|
934
1072
|
},
|
|
935
1073
|
|
|
936
1074
|
// Attempt to load the current URL fragment. If a route succeeds with a
|
|
@@ -953,12 +1091,12 @@
|
|
|
953
1091
|
//
|
|
954
1092
|
// The options object can contain `trigger: true` if you wish to have the
|
|
955
1093
|
// route callback be fired (not usually desirable), or `replace: true`, if
|
|
956
|
-
// you
|
|
1094
|
+
// you wish to modify the current URL without adding an entry to the history.
|
|
957
1095
|
navigate: function(fragment, options) {
|
|
958
|
-
if (!
|
|
1096
|
+
if (!History.started) return false;
|
|
959
1097
|
if (!options || options === true) options = {trigger: options};
|
|
960
1098
|
var frag = (fragment || '').replace(routeStripper, '');
|
|
961
|
-
if (this.fragment == frag
|
|
1099
|
+
if (this.fragment == frag) return;
|
|
962
1100
|
|
|
963
1101
|
// If pushState is available, we use it to set the fragment as a real URL.
|
|
964
1102
|
if (this._hasPushState) {
|
|
@@ -971,7 +1109,7 @@
|
|
|
971
1109
|
} else if (this._wantsHashChange) {
|
|
972
1110
|
this.fragment = frag;
|
|
973
1111
|
this._updateHash(window.location, frag, options.replace);
|
|
974
|
-
if (this.iframe && (frag != this.getFragment(this.iframe
|
|
1112
|
+
if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
|
|
975
1113
|
// Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
|
|
976
1114
|
// When replace is true, we don't want this.
|
|
977
1115
|
if(!options.replace) this.iframe.document.open().close();
|
|
@@ -1002,7 +1140,7 @@
|
|
|
1002
1140
|
|
|
1003
1141
|
// Creating a Backbone.View creates its initial element outside of the DOM,
|
|
1004
1142
|
// if an existing element is not provided...
|
|
1005
|
-
Backbone.View = function(options) {
|
|
1143
|
+
var View = Backbone.View = function(options) {
|
|
1006
1144
|
this.cid = _.uniqueId('view');
|
|
1007
1145
|
this._configure(options || {});
|
|
1008
1146
|
this._ensureElement();
|
|
@@ -1011,13 +1149,13 @@
|
|
|
1011
1149
|
};
|
|
1012
1150
|
|
|
1013
1151
|
// Cached regex to split keys for `delegate`.
|
|
1014
|
-
var
|
|
1152
|
+
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
|
1015
1153
|
|
|
1016
1154
|
// List of view options to be merged as properties.
|
|
1017
1155
|
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
|
1018
1156
|
|
|
1019
1157
|
// Set up all inheritable **Backbone.View** properties and methods.
|
|
1020
|
-
_.extend(
|
|
1158
|
+
_.extend(View.prototype, Events, {
|
|
1021
1159
|
|
|
1022
1160
|
// The default `tagName` of a View's element is `"div"`.
|
|
1023
1161
|
tagName: 'div',
|
|
@@ -1061,7 +1199,8 @@
|
|
|
1061
1199
|
// Change the view's element (`this.el` property), including event
|
|
1062
1200
|
// re-delegation.
|
|
1063
1201
|
setElement: function(element, delegate) {
|
|
1064
|
-
this.$el
|
|
1202
|
+
if (this.$el) this.undelegateEvents();
|
|
1203
|
+
this.$el = (element instanceof $) ? element : $(element);
|
|
1065
1204
|
this.el = this.$el[0];
|
|
1066
1205
|
if (delegate !== false) this.delegateEvents();
|
|
1067
1206
|
return this;
|
|
@@ -1088,8 +1227,8 @@
|
|
|
1088
1227
|
for (var key in events) {
|
|
1089
1228
|
var method = events[key];
|
|
1090
1229
|
if (!_.isFunction(method)) method = this[events[key]];
|
|
1091
|
-
if (!method) throw new Error('
|
|
1092
|
-
var match = key.match(
|
|
1230
|
+
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
|
|
1231
|
+
var match = key.match(delegateEventSplitter);
|
|
1093
1232
|
var eventName = match[1], selector = match[2];
|
|
1094
1233
|
method = _.bind(method, this);
|
|
1095
1234
|
eventName += '.delegateEvents' + this.cid;
|
|
@@ -1145,8 +1284,7 @@
|
|
|
1145
1284
|
};
|
|
1146
1285
|
|
|
1147
1286
|
// Set up inheritance for the model, collection, and view.
|
|
1148
|
-
|
|
1149
|
-
Backbone.Router.extend = Backbone.View.extend = extend;
|
|
1287
|
+
Model.extend = Collection.extend = Router.extend = View.extend = extend;
|
|
1150
1288
|
|
|
1151
1289
|
// Backbone.sync
|
|
1152
1290
|
// -------------
|
|
@@ -1177,6 +1315,9 @@
|
|
|
1177
1315
|
Backbone.sync = function(method, model, options) {
|
|
1178
1316
|
var type = methodMap[method];
|
|
1179
1317
|
|
|
1318
|
+
// Default options, unless specified.
|
|
1319
|
+
options || (options = {});
|
|
1320
|
+
|
|
1180
1321
|
// Default JSON-request options.
|
|
1181
1322
|
var params = {type: type, dataType: 'json'};
|
|
1182
1323
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: backbone-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.2
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,11 +9,11 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2012-
|
|
12
|
+
date: 2012-04-06 00:00:00.000000000Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rails
|
|
16
|
-
requirement: &
|
|
16
|
+
requirement: &2154095100 !ruby/object:Gem::Requirement
|
|
17
17
|
none: false
|
|
18
18
|
requirements:
|
|
19
19
|
- - ! '>='
|
|
@@ -21,7 +21,7 @@ dependencies:
|
|
|
21
21
|
version: 3.0.0
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
|
-
version_requirements: *
|
|
24
|
+
version_requirements: *2154095100
|
|
25
25
|
description: Ships backbone and underscore to your Rails 3.1 application through the
|
|
26
26
|
new asset pipeline. Rails 3.0 is supported via generators.
|
|
27
27
|
email:
|