backbone-rails 1.1.2 → 1.2.3
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.
- checksums.yaml +7 -0
- data/vendor/assets/javascripts/backbone.js +717 -431
- data/vendor/assets/javascripts/underscore.js +756 -551
- metadata +13 -17
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fa24e20fdd39692add0b4a959eedb138732d76ce
|
4
|
+
data.tar.gz: 03bf7d7b25f3ce21e0c92bf86e0f328662c97a40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a5f563db118db9f9ffd7a488b1b676e7cc104e5559b62df35444029bda63055652066b1ee3147a739f39db1b539c1525924f47c0cc169da82acb1846b59326b
|
7
|
+
data.tar.gz: 69bf3dbd1e4808ca3a955061b6ed18f34cf7633899f3e62edb0aca6f319df2c58b13ffaf570984c4368931f0cef1f461834338cf7c216a218f8975adb3f0f679
|
@@ -1,11 +1,16 @@
|
|
1
|
-
// Backbone.js 1.
|
1
|
+
// Backbone.js 1.2.3
|
2
2
|
|
3
|
-
// (c) 2010-
|
3
|
+
// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
4
4
|
// Backbone may be freely distributed under the MIT license.
|
5
5
|
// For all details and documentation:
|
6
6
|
// http://backbonejs.org
|
7
7
|
|
8
|
-
(function(
|
8
|
+
(function(factory) {
|
9
|
+
|
10
|
+
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
11
|
+
// We use `self` instead of `window` for `WebWorker` support.
|
12
|
+
var root = (typeof self == 'object' && self.self == self && self) ||
|
13
|
+
(typeof global == 'object' && global.global == global && global);
|
9
14
|
|
10
15
|
// Set up Backbone appropriately for the environment. Start with AMD.
|
11
16
|
if (typeof define === 'function' && define.amd) {
|
@@ -17,15 +22,16 @@
|
|
17
22
|
|
18
23
|
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
|
19
24
|
} else if (typeof exports !== 'undefined') {
|
20
|
-
var _ = require('underscore')
|
21
|
-
|
25
|
+
var _ = require('underscore'), $;
|
26
|
+
try { $ = require('jquery'); } catch(e) {}
|
27
|
+
factory(root, exports, _, $);
|
22
28
|
|
23
29
|
// Finally, as a browser global.
|
24
30
|
} else {
|
25
31
|
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
|
26
32
|
}
|
27
33
|
|
28
|
-
}(
|
34
|
+
}(function(root, Backbone, _, $) {
|
29
35
|
|
30
36
|
// Initial Setup
|
31
37
|
// -------------
|
@@ -34,14 +40,11 @@
|
|
34
40
|
// restored later on, if `noConflict` is used.
|
35
41
|
var previousBackbone = root.Backbone;
|
36
42
|
|
37
|
-
// Create local
|
38
|
-
var
|
39
|
-
var push = array.push;
|
40
|
-
var slice = array.slice;
|
41
|
-
var splice = array.splice;
|
43
|
+
// Create a local reference to a common array method we'll want to use later.
|
44
|
+
var slice = Array.prototype.slice;
|
42
45
|
|
43
46
|
// Current version of the library. Keep in sync with `package.json`.
|
44
|
-
Backbone.VERSION = '1.
|
47
|
+
Backbone.VERSION = '1.2.3';
|
45
48
|
|
46
49
|
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
|
47
50
|
// the `$` variable.
|
@@ -60,17 +63,65 @@
|
|
60
63
|
Backbone.emulateHTTP = false;
|
61
64
|
|
62
65
|
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
|
63
|
-
// `application/json` requests ... will encode the body as
|
66
|
+
// `application/json` requests ... this will encode the body as
|
64
67
|
// `application/x-www-form-urlencoded` instead and will send the model in a
|
65
68
|
// form param named `model`.
|
66
69
|
Backbone.emulateJSON = false;
|
67
70
|
|
71
|
+
// Proxy Backbone class methods to Underscore functions, wrapping the model's
|
72
|
+
// `attributes` object or collection's `models` array behind the scenes.
|
73
|
+
//
|
74
|
+
// collection.filter(function(model) { return model.get('age') > 10 });
|
75
|
+
// collection.each(this.addView);
|
76
|
+
//
|
77
|
+
// `Function#apply` can be slow so we use the method's arg count, if we know it.
|
78
|
+
var addMethod = function(length, method, attribute) {
|
79
|
+
switch (length) {
|
80
|
+
case 1: return function() {
|
81
|
+
return _[method](this[attribute]);
|
82
|
+
};
|
83
|
+
case 2: return function(value) {
|
84
|
+
return _[method](this[attribute], value);
|
85
|
+
};
|
86
|
+
case 3: return function(iteratee, context) {
|
87
|
+
return _[method](this[attribute], cb(iteratee, this), context);
|
88
|
+
};
|
89
|
+
case 4: return function(iteratee, defaultVal, context) {
|
90
|
+
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
|
91
|
+
};
|
92
|
+
default: return function() {
|
93
|
+
var args = slice.call(arguments);
|
94
|
+
args.unshift(this[attribute]);
|
95
|
+
return _[method].apply(_, args);
|
96
|
+
};
|
97
|
+
}
|
98
|
+
};
|
99
|
+
var addUnderscoreMethods = function(Class, methods, attribute) {
|
100
|
+
_.each(methods, function(length, method) {
|
101
|
+
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
|
102
|
+
});
|
103
|
+
};
|
104
|
+
|
105
|
+
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
|
106
|
+
var cb = function(iteratee, instance) {
|
107
|
+
if (_.isFunction(iteratee)) return iteratee;
|
108
|
+
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
|
109
|
+
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
|
110
|
+
return iteratee;
|
111
|
+
};
|
112
|
+
var modelMatcher = function(attrs) {
|
113
|
+
var matcher = _.matches(attrs);
|
114
|
+
return function(model) {
|
115
|
+
return matcher(model.attributes);
|
116
|
+
};
|
117
|
+
};
|
118
|
+
|
68
119
|
// Backbone.Events
|
69
120
|
// ---------------
|
70
121
|
|
71
122
|
// A module that can be mixed in to *any object* in order to provide it with
|
72
|
-
// custom
|
73
|
-
//
|
123
|
+
// a custom event channel. You may bind a callback to an event with `on` or
|
124
|
+
// remove with `off`; `trigger`-ing an event fires all callbacks in
|
74
125
|
// succession.
|
75
126
|
//
|
76
127
|
// var object = {};
|
@@ -78,123 +129,234 @@
|
|
78
129
|
// object.on('expand', function(){ alert('expanded'); });
|
79
130
|
// object.trigger('expand');
|
80
131
|
//
|
81
|
-
var Events = Backbone.Events = {
|
82
|
-
|
83
|
-
// Bind an event to a `callback` function. Passing `"all"` will bind
|
84
|
-
// the callback to all events fired.
|
85
|
-
on: function(name, callback, context) {
|
86
|
-
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
|
87
|
-
this._events || (this._events = {});
|
88
|
-
var events = this._events[name] || (this._events[name] = []);
|
89
|
-
events.push({callback: callback, context: context, ctx: context || this});
|
90
|
-
return this;
|
91
|
-
},
|
132
|
+
var Events = Backbone.Events = {};
|
92
133
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
// Remove one or many callbacks. If `context` is null, removes all
|
107
|
-
// callbacks with that function. If `callback` is null, removes all
|
108
|
-
// callbacks for the event. If `name` is null, removes all bound
|
109
|
-
// callbacks for all events.
|
110
|
-
off: function(name, callback, context) {
|
111
|
-
var retain, ev, events, names, i, l, j, k;
|
112
|
-
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
|
113
|
-
if (!name && !callback && !context) {
|
114
|
-
this._events = void 0;
|
115
|
-
return this;
|
134
|
+
// Regular expression used to split event strings.
|
135
|
+
var eventSplitter = /\s+/;
|
136
|
+
|
137
|
+
// Iterates over the standard `event, callback` (as well as the fancy multiple
|
138
|
+
// space-separated events `"change blur", callback` and jQuery-style event
|
139
|
+
// maps `{event: callback}`).
|
140
|
+
var eventsApi = function(iteratee, events, name, callback, opts) {
|
141
|
+
var i = 0, names;
|
142
|
+
if (name && typeof name === 'object') {
|
143
|
+
// Handle event maps.
|
144
|
+
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
|
145
|
+
for (names = _.keys(name); i < names.length ; i++) {
|
146
|
+
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
|
116
147
|
}
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
this._events[name] = retain = [];
|
122
|
-
if (callback || context) {
|
123
|
-
for (j = 0, k = events.length; j < k; j++) {
|
124
|
-
ev = events[j];
|
125
|
-
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
|
126
|
-
(context && context !== ev.context)) {
|
127
|
-
retain.push(ev);
|
128
|
-
}
|
129
|
-
}
|
130
|
-
}
|
131
|
-
if (!retain.length) delete this._events[name];
|
132
|
-
}
|
148
|
+
} else if (name && eventSplitter.test(name)) {
|
149
|
+
// Handle space separated event names by delegating them individually.
|
150
|
+
for (names = name.split(eventSplitter); i < names.length; i++) {
|
151
|
+
events = iteratee(events, names[i], callback, opts);
|
133
152
|
}
|
153
|
+
} else {
|
154
|
+
// Finally, standard events.
|
155
|
+
events = iteratee(events, name, callback, opts);
|
156
|
+
}
|
157
|
+
return events;
|
158
|
+
};
|
134
159
|
|
135
|
-
|
136
|
-
|
160
|
+
// Bind an event to a `callback` function. Passing `"all"` will bind
|
161
|
+
// the callback to all events fired.
|
162
|
+
Events.on = function(name, callback, context) {
|
163
|
+
return internalOn(this, name, callback, context);
|
164
|
+
};
|
137
165
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
if (!eventsApi(this, 'trigger', name, args)) return this;
|
146
|
-
var events = this._events[name];
|
147
|
-
var allEvents = this._events.all;
|
148
|
-
if (events) triggerEvents(events, args);
|
149
|
-
if (allEvents) triggerEvents(allEvents, arguments);
|
150
|
-
return this;
|
151
|
-
},
|
166
|
+
// Guard the `listening` argument from the public API.
|
167
|
+
var internalOn = function(obj, name, callback, context, listening) {
|
168
|
+
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
|
169
|
+
context: context,
|
170
|
+
ctx: obj,
|
171
|
+
listening: listening
|
172
|
+
});
|
152
173
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
var listeningTo = this._listeningTo;
|
157
|
-
if (!listeningTo) return this;
|
158
|
-
var remove = !name && !callback;
|
159
|
-
if (!callback && typeof name === 'object') callback = this;
|
160
|
-
if (obj) (listeningTo = {})[obj._listenId] = obj;
|
161
|
-
for (var id in listeningTo) {
|
162
|
-
obj = listeningTo[id];
|
163
|
-
obj.off(name, callback, this);
|
164
|
-
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
|
165
|
-
}
|
166
|
-
return this;
|
174
|
+
if (listening) {
|
175
|
+
var listeners = obj._listeners || (obj._listeners = {});
|
176
|
+
listeners[listening.id] = listening;
|
167
177
|
}
|
168
178
|
|
179
|
+
return obj;
|
169
180
|
};
|
170
181
|
|
171
|
-
//
|
172
|
-
|
182
|
+
// Inversion-of-control versions of `on`. Tell *this* object to listen to
|
183
|
+
// an event in another object... keeping track of what it's listening to
|
184
|
+
// for easier unbinding later.
|
185
|
+
Events.listenTo = function(obj, name, callback) {
|
186
|
+
if (!obj) return this;
|
187
|
+
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
|
188
|
+
var listeningTo = this._listeningTo || (this._listeningTo = {});
|
189
|
+
var listening = listeningTo[id];
|
190
|
+
|
191
|
+
// This object is not listening to any other events on `obj` yet.
|
192
|
+
// Setup the necessary references to track the listening callbacks.
|
193
|
+
if (!listening) {
|
194
|
+
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
|
195
|
+
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
|
196
|
+
}
|
197
|
+
|
198
|
+
// Bind callbacks on obj, and keep track of them on listening.
|
199
|
+
internalOn(obj, name, callback, this, listening);
|
200
|
+
return this;
|
201
|
+
};
|
202
|
+
|
203
|
+
// The reducing API that adds a callback to the `events` object.
|
204
|
+
var onApi = function(events, name, callback, options) {
|
205
|
+
if (callback) {
|
206
|
+
var handlers = events[name] || (events[name] = []);
|
207
|
+
var context = options.context, ctx = options.ctx, listening = options.listening;
|
208
|
+
if (listening) listening.count++;
|
209
|
+
|
210
|
+
handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
|
211
|
+
}
|
212
|
+
return events;
|
213
|
+
};
|
214
|
+
|
215
|
+
// Remove one or many callbacks. If `context` is null, removes all
|
216
|
+
// callbacks with that function. If `callback` is null, removes all
|
217
|
+
// callbacks for the event. If `name` is null, removes all bound
|
218
|
+
// callbacks for all events.
|
219
|
+
Events.off = function(name, callback, context) {
|
220
|
+
if (!this._events) return this;
|
221
|
+
this._events = eventsApi(offApi, this._events, name, callback, {
|
222
|
+
context: context,
|
223
|
+
listeners: this._listeners
|
224
|
+
});
|
225
|
+
return this;
|
226
|
+
};
|
227
|
+
|
228
|
+
// Tell this object to stop listening to either specific events ... or
|
229
|
+
// to every object it's currently listening to.
|
230
|
+
Events.stopListening = function(obj, name, callback) {
|
231
|
+
var listeningTo = this._listeningTo;
|
232
|
+
if (!listeningTo) return this;
|
233
|
+
|
234
|
+
var ids = obj ? [obj._listenId] : _.keys(listeningTo);
|
235
|
+
|
236
|
+
for (var i = 0; i < ids.length; i++) {
|
237
|
+
var listening = listeningTo[ids[i]];
|
238
|
+
|
239
|
+
// If listening doesn't exist, this object is not currently
|
240
|
+
// listening to obj. Break out early.
|
241
|
+
if (!listening) break;
|
242
|
+
|
243
|
+
listening.obj.off(name, callback, this);
|
244
|
+
}
|
245
|
+
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
|
246
|
+
|
247
|
+
return this;
|
248
|
+
};
|
173
249
|
|
174
|
-
//
|
175
|
-
|
176
|
-
|
177
|
-
var eventsApi = function(obj, action, name, rest) {
|
178
|
-
if (!name) return true;
|
250
|
+
// The reducing API that removes a callback from the `events` object.
|
251
|
+
var offApi = function(events, name, callback, options) {
|
252
|
+
if (!events) return;
|
179
253
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
254
|
+
var i = 0, listening;
|
255
|
+
var context = options.context, listeners = options.listeners;
|
256
|
+
|
257
|
+
// Delete all events listeners and "drop" events.
|
258
|
+
if (!name && !callback && !context) {
|
259
|
+
var ids = _.keys(listeners);
|
260
|
+
for (; i < ids.length; i++) {
|
261
|
+
listening = listeners[ids[i]];
|
262
|
+
delete listeners[listening.id];
|
263
|
+
delete listening.listeningTo[listening.objId];
|
184
264
|
}
|
185
|
-
return
|
265
|
+
return;
|
186
266
|
}
|
187
267
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
268
|
+
var names = name ? [name] : _.keys(events);
|
269
|
+
for (; i < names.length; i++) {
|
270
|
+
name = names[i];
|
271
|
+
var handlers = events[name];
|
272
|
+
|
273
|
+
// Bail out if there are no events stored.
|
274
|
+
if (!handlers) break;
|
275
|
+
|
276
|
+
// Replace events if there are any remaining. Otherwise, clean up.
|
277
|
+
var remaining = [];
|
278
|
+
for (var j = 0; j < handlers.length; j++) {
|
279
|
+
var handler = handlers[j];
|
280
|
+
if (
|
281
|
+
callback && callback !== handler.callback &&
|
282
|
+
callback !== handler.callback._callback ||
|
283
|
+
context && context !== handler.context
|
284
|
+
) {
|
285
|
+
remaining.push(handler);
|
286
|
+
} else {
|
287
|
+
listening = handler.listening;
|
288
|
+
if (listening && --listening.count === 0) {
|
289
|
+
delete listeners[listening.id];
|
290
|
+
delete listening.listeningTo[listening.objId];
|
291
|
+
}
|
292
|
+
}
|
293
|
+
}
|
294
|
+
|
295
|
+
// Update tail event if the list has any events. Otherwise, clean up.
|
296
|
+
if (remaining.length) {
|
297
|
+
events[name] = remaining;
|
298
|
+
} else {
|
299
|
+
delete events[name];
|
193
300
|
}
|
194
|
-
return false;
|
195
301
|
}
|
302
|
+
if (_.size(events)) return events;
|
303
|
+
};
|
304
|
+
|
305
|
+
// Bind an event to only be triggered a single time. After the first time
|
306
|
+
// the callback is invoked, its listener will be removed. If multiple events
|
307
|
+
// are passed in using the space-separated syntax, the handler will fire
|
308
|
+
// once for each event, not once for a combination of all events.
|
309
|
+
Events.once = function(name, callback, context) {
|
310
|
+
// Map the event into a `{event: once}` object.
|
311
|
+
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
|
312
|
+
return this.on(events, void 0, context);
|
313
|
+
};
|
196
314
|
|
197
|
-
|
315
|
+
// Inversion-of-control versions of `once`.
|
316
|
+
Events.listenToOnce = function(obj, name, callback) {
|
317
|
+
// Map the event into a `{event: once}` object.
|
318
|
+
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
|
319
|
+
return this.listenTo(obj, events);
|
320
|
+
};
|
321
|
+
|
322
|
+
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
|
323
|
+
// `offer` unbinds the `onceWrapper` after it has been called.
|
324
|
+
var onceMap = function(map, name, callback, offer) {
|
325
|
+
if (callback) {
|
326
|
+
var once = map[name] = _.once(function() {
|
327
|
+
offer(name, once);
|
328
|
+
callback.apply(this, arguments);
|
329
|
+
});
|
330
|
+
once._callback = callback;
|
331
|
+
}
|
332
|
+
return map;
|
333
|
+
};
|
334
|
+
|
335
|
+
// Trigger one or many events, firing all bound callbacks. Callbacks are
|
336
|
+
// passed the same arguments as `trigger` is, apart from the event name
|
337
|
+
// (unless you're listening on `"all"`, which will cause your callback to
|
338
|
+
// receive the true name of the event as the first argument).
|
339
|
+
Events.trigger = function(name) {
|
340
|
+
if (!this._events) return this;
|
341
|
+
|
342
|
+
var length = Math.max(0, arguments.length - 1);
|
343
|
+
var args = Array(length);
|
344
|
+
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
|
345
|
+
|
346
|
+
eventsApi(triggerApi, this._events, name, void 0, args);
|
347
|
+
return this;
|
348
|
+
};
|
349
|
+
|
350
|
+
// Handles triggering the appropriate event callbacks.
|
351
|
+
var triggerApi = function(objEvents, name, cb, args) {
|
352
|
+
if (objEvents) {
|
353
|
+
var events = objEvents[name];
|
354
|
+
var allEvents = objEvents.all;
|
355
|
+
if (events && allEvents) allEvents = allEvents.slice();
|
356
|
+
if (events) triggerEvents(events, args);
|
357
|
+
if (allEvents) triggerEvents(allEvents, [name].concat(args));
|
358
|
+
}
|
359
|
+
return objEvents;
|
198
360
|
};
|
199
361
|
|
200
362
|
// A difficult-to-believe, but optimized internal dispatch function for
|
@@ -211,22 +373,6 @@
|
|
211
373
|
}
|
212
374
|
};
|
213
375
|
|
214
|
-
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
|
215
|
-
|
216
|
-
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
|
217
|
-
// listen to an event in another object ... keeping track of what it's
|
218
|
-
// listening to.
|
219
|
-
_.each(listenMethods, function(implementation, method) {
|
220
|
-
Events[method] = function(obj, name, callback) {
|
221
|
-
var listeningTo = this._listeningTo || (this._listeningTo = {});
|
222
|
-
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
|
223
|
-
listeningTo[id] = obj;
|
224
|
-
if (!callback && typeof name === 'object') callback = this;
|
225
|
-
obj[implementation](name, callback, this);
|
226
|
-
return this;
|
227
|
-
};
|
228
|
-
});
|
229
|
-
|
230
376
|
// Aliases for backwards compatibility.
|
231
377
|
Events.bind = Events.on;
|
232
378
|
Events.unbind = Events.off;
|
@@ -248,7 +394,7 @@
|
|
248
394
|
var Model = Backbone.Model = function(attributes, options) {
|
249
395
|
var attrs = attributes || {};
|
250
396
|
options || (options = {});
|
251
|
-
this.cid = _.uniqueId(
|
397
|
+
this.cid = _.uniqueId(this.cidPrefix);
|
252
398
|
this.attributes = {};
|
253
399
|
if (options.collection) this.collection = options.collection;
|
254
400
|
if (options.parse) attrs = this.parse(attrs, options) || {};
|
@@ -271,6 +417,10 @@
|
|
271
417
|
// CouchDB users may want to set this to `"_id"`.
|
272
418
|
idAttribute: 'id',
|
273
419
|
|
420
|
+
// The prefix is used to create the client id which is used to identify models locally.
|
421
|
+
// You may want to override this if you're experiencing name clashes with model ids.
|
422
|
+
cidPrefix: 'c',
|
423
|
+
|
274
424
|
// Initialize is an empty function by default. Override it with your own
|
275
425
|
// initialization logic.
|
276
426
|
initialize: function(){},
|
@@ -302,14 +452,19 @@
|
|
302
452
|
return this.get(attr) != null;
|
303
453
|
},
|
304
454
|
|
455
|
+
// Special-cased proxy to underscore's `_.matches` method.
|
456
|
+
matches: function(attrs) {
|
457
|
+
return !!_.iteratee(attrs, this)(this.attributes);
|
458
|
+
},
|
459
|
+
|
305
460
|
// Set a hash of model attributes on the object, firing `"change"`. This is
|
306
461
|
// the core primitive operation of a model, updating the data and notifying
|
307
462
|
// anyone who needs to know about the change in state. The heart of the beast.
|
308
463
|
set: function(key, val, options) {
|
309
|
-
var attr, attrs, unset, changes, silent, changing, prev, current;
|
310
464
|
if (key == null) return this;
|
311
465
|
|
312
466
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
467
|
+
var attrs;
|
313
468
|
if (typeof key === 'object') {
|
314
469
|
attrs = key;
|
315
470
|
options = val;
|
@@ -323,37 +478,40 @@
|
|
323
478
|
if (!this._validate(attrs, options)) return false;
|
324
479
|
|
325
480
|
// Extract attributes and options.
|
326
|
-
unset
|
327
|
-
silent
|
328
|
-
changes
|
329
|
-
changing
|
330
|
-
this._changing
|
481
|
+
var unset = options.unset;
|
482
|
+
var silent = options.silent;
|
483
|
+
var changes = [];
|
484
|
+
var changing = this._changing;
|
485
|
+
this._changing = true;
|
331
486
|
|
332
487
|
if (!changing) {
|
333
488
|
this._previousAttributes = _.clone(this.attributes);
|
334
489
|
this.changed = {};
|
335
490
|
}
|
336
|
-
current = this.attributes, prev = this._previousAttributes;
|
337
491
|
|
338
|
-
|
339
|
-
|
492
|
+
var current = this.attributes;
|
493
|
+
var changed = this.changed;
|
494
|
+
var prev = this._previousAttributes;
|
340
495
|
|
341
496
|
// For each `set` attribute, update or delete the current value.
|
342
|
-
for (attr in attrs) {
|
497
|
+
for (var attr in attrs) {
|
343
498
|
val = attrs[attr];
|
344
499
|
if (!_.isEqual(current[attr], val)) changes.push(attr);
|
345
500
|
if (!_.isEqual(prev[attr], val)) {
|
346
|
-
|
501
|
+
changed[attr] = val;
|
347
502
|
} else {
|
348
|
-
delete
|
503
|
+
delete changed[attr];
|
349
504
|
}
|
350
505
|
unset ? delete current[attr] : current[attr] = val;
|
351
506
|
}
|
352
507
|
|
508
|
+
// Update the `id`.
|
509
|
+
this.id = this.get(this.idAttribute);
|
510
|
+
|
353
511
|
// Trigger all relevant attribute changes.
|
354
512
|
if (!silent) {
|
355
513
|
if (changes.length) this._pending = options;
|
356
|
-
for (var i = 0
|
514
|
+
for (var i = 0; i < changes.length; i++) {
|
357
515
|
this.trigger('change:' + changes[i], this, current[changes[i]], options);
|
358
516
|
}
|
359
517
|
}
|
@@ -401,13 +559,14 @@
|
|
401
559
|
// determining if there *would be* a change.
|
402
560
|
changedAttributes: function(diff) {
|
403
561
|
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
404
|
-
var val, changed = false;
|
405
562
|
var old = this._changing ? this._previousAttributes : this.attributes;
|
563
|
+
var changed = {};
|
406
564
|
for (var attr in diff) {
|
407
|
-
|
408
|
-
|
565
|
+
var val = diff[attr];
|
566
|
+
if (_.isEqual(old[attr], val)) continue;
|
567
|
+
changed[attr] = val;
|
409
568
|
}
|
410
|
-
return changed;
|
569
|
+
return _.size(changed) ? changed : false;
|
411
570
|
},
|
412
571
|
|
413
572
|
// Get the previous value of an attribute, recorded at the time the last
|
@@ -423,17 +582,16 @@
|
|
423
582
|
return _.clone(this._previousAttributes);
|
424
583
|
},
|
425
584
|
|
426
|
-
// Fetch the model from the server
|
427
|
-
//
|
428
|
-
// triggering a `"change"` event.
|
585
|
+
// Fetch the model from the server, merging the response with the model's
|
586
|
+
// local attributes. Any changed attributes will trigger a "change" event.
|
429
587
|
fetch: function(options) {
|
430
|
-
options =
|
431
|
-
if (options.parse === void 0) options.parse = true;
|
588
|
+
options = _.extend({parse: true}, options);
|
432
589
|
var model = this;
|
433
590
|
var success = options.success;
|
434
591
|
options.success = function(resp) {
|
435
|
-
|
436
|
-
if (
|
592
|
+
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
|
593
|
+
if (!model.set(serverAttrs, options)) return false;
|
594
|
+
if (success) success.call(options.context, model, resp, options);
|
437
595
|
model.trigger('sync', model, resp, options);
|
438
596
|
};
|
439
597
|
wrapError(this, options);
|
@@ -444,9 +602,8 @@
|
|
444
602
|
// If the server returns an attributes hash that differs, the model's
|
445
603
|
// state will be `set` again.
|
446
604
|
save: function(key, val, options) {
|
447
|
-
var attrs, method, xhr, attributes = this.attributes;
|
448
|
-
|
449
605
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
606
|
+
var attrs;
|
450
607
|
if (key == null || typeof key === 'object') {
|
451
608
|
attrs = key;
|
452
609
|
options = val;
|
@@ -454,46 +611,43 @@
|
|
454
611
|
(attrs = {})[key] = val;
|
455
612
|
}
|
456
613
|
|
457
|
-
options = _.extend({validate: true}, options);
|
614
|
+
options = _.extend({validate: true, parse: true}, options);
|
615
|
+
var wait = options.wait;
|
458
616
|
|
459
617
|
// If we're not waiting and attributes exist, save acts as
|
460
618
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
461
619
|
// the model will be valid when the attributes, if any, are set.
|
462
|
-
if (attrs && !
|
620
|
+
if (attrs && !wait) {
|
463
621
|
if (!this.set(attrs, options)) return false;
|
464
622
|
} else {
|
465
623
|
if (!this._validate(attrs, options)) return false;
|
466
624
|
}
|
467
625
|
|
468
|
-
// Set temporary attributes if `{wait: true}`.
|
469
|
-
if (attrs && options.wait) {
|
470
|
-
this.attributes = _.extend({}, attributes, attrs);
|
471
|
-
}
|
472
|
-
|
473
626
|
// After a successful server-side save, the client is (optionally)
|
474
627
|
// updated with the server-side state.
|
475
|
-
if (options.parse === void 0) options.parse = true;
|
476
628
|
var model = this;
|
477
629
|
var success = options.success;
|
630
|
+
var attributes = this.attributes;
|
478
631
|
options.success = function(resp) {
|
479
632
|
// Ensure attributes are restored during synchronous saves.
|
480
633
|
model.attributes = attributes;
|
481
|
-
var serverAttrs = model.parse(resp, options);
|
482
|
-
if (
|
483
|
-
if (
|
484
|
-
|
485
|
-
}
|
486
|
-
if (success) success(model, resp, options);
|
634
|
+
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
|
635
|
+
if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
|
636
|
+
if (serverAttrs && !model.set(serverAttrs, options)) return false;
|
637
|
+
if (success) success.call(options.context, model, resp, options);
|
487
638
|
model.trigger('sync', model, resp, options);
|
488
639
|
};
|
489
640
|
wrapError(this, options);
|
490
641
|
|
491
|
-
|
492
|
-
if (
|
493
|
-
|
642
|
+
// Set temporary attributes if `{wait: true}` to properly find new ids.
|
643
|
+
if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
|
644
|
+
|
645
|
+
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
646
|
+
if (method === 'patch' && !options.attrs) options.attrs = attrs;
|
647
|
+
var xhr = this.sync(method, this, options);
|
494
648
|
|
495
649
|
// Restore attributes.
|
496
|
-
|
650
|
+
this.attributes = attributes;
|
497
651
|
|
498
652
|
return xhr;
|
499
653
|
},
|
@@ -505,25 +659,27 @@
|
|
505
659
|
options = options ? _.clone(options) : {};
|
506
660
|
var model = this;
|
507
661
|
var success = options.success;
|
662
|
+
var wait = options.wait;
|
508
663
|
|
509
664
|
var destroy = function() {
|
665
|
+
model.stopListening();
|
510
666
|
model.trigger('destroy', model, model.collection, options);
|
511
667
|
};
|
512
668
|
|
513
669
|
options.success = function(resp) {
|
514
|
-
if (
|
515
|
-
if (success) success(model, resp, options);
|
670
|
+
if (wait) destroy();
|
671
|
+
if (success) success.call(options.context, model, resp, options);
|
516
672
|
if (!model.isNew()) model.trigger('sync', model, resp, options);
|
517
673
|
};
|
518
674
|
|
675
|
+
var xhr = false;
|
519
676
|
if (this.isNew()) {
|
520
|
-
options.success
|
521
|
-
|
677
|
+
_.defer(options.success);
|
678
|
+
} else {
|
679
|
+
wrapError(this, options);
|
680
|
+
xhr = this.sync('delete', this, options);
|
522
681
|
}
|
523
|
-
|
524
|
-
|
525
|
-
var xhr = this.sync('delete', this, options);
|
526
|
-
if (!options.wait) destroy();
|
682
|
+
if (!wait) destroy();
|
527
683
|
return xhr;
|
528
684
|
},
|
529
685
|
|
@@ -536,7 +692,8 @@
|
|
536
692
|
_.result(this.collection, 'url') ||
|
537
693
|
urlError();
|
538
694
|
if (this.isNew()) return base;
|
539
|
-
|
695
|
+
var id = this.get(this.idAttribute);
|
696
|
+
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
|
540
697
|
},
|
541
698
|
|
542
699
|
// **parse** converts a response into the hash of attributes to be `set` on
|
@@ -557,7 +714,7 @@
|
|
557
714
|
|
558
715
|
// Check if the model is currently in a valid state.
|
559
716
|
isValid: function(options) {
|
560
|
-
return this._validate({}, _.
|
717
|
+
return this._validate({}, _.defaults({validate: true}, options));
|
561
718
|
},
|
562
719
|
|
563
720
|
// Run validation against the next complete set of model attributes,
|
@@ -573,23 +730,19 @@
|
|
573
730
|
|
574
731
|
});
|
575
732
|
|
576
|
-
// Underscore methods that we want to implement on the Model
|
577
|
-
|
733
|
+
// Underscore methods that we want to implement on the Model, mapped to the
|
734
|
+
// number of arguments they take.
|
735
|
+
var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
|
736
|
+
omit: 0, chain: 1, isEmpty: 1 };
|
578
737
|
|
579
738
|
// Mix in each Underscore method as a proxy to `Model#attributes`.
|
580
|
-
|
581
|
-
Model.prototype[method] = function() {
|
582
|
-
var args = slice.call(arguments);
|
583
|
-
args.unshift(this.attributes);
|
584
|
-
return _[method].apply(_, args);
|
585
|
-
};
|
586
|
-
});
|
739
|
+
addUnderscoreMethods(Model, modelMethods, 'attributes');
|
587
740
|
|
588
741
|
// Backbone.Collection
|
589
742
|
// -------------------
|
590
743
|
|
591
744
|
// If models tend to represent a single row of data, a Backbone Collection is
|
592
|
-
// more
|
745
|
+
// more analogous to a table full of data ... or a small slice or page of that
|
593
746
|
// table, or a collection of rows that belong together for a particular reason
|
594
747
|
// -- all of the messages in this particular folder, all of the documents
|
595
748
|
// belonging to this particular author, and so on. Collections maintain
|
@@ -611,6 +764,16 @@
|
|
611
764
|
var setOptions = {add: true, remove: true, merge: true};
|
612
765
|
var addOptions = {add: true, remove: false};
|
613
766
|
|
767
|
+
// Splices `insert` into `array` at index `at`.
|
768
|
+
var splice = function(array, insert, at) {
|
769
|
+
at = Math.min(Math.max(at, 0), array.length);
|
770
|
+
var tail = Array(array.length - at);
|
771
|
+
var length = insert.length;
|
772
|
+
for (var i = 0; i < tail.length; i++) tail[i] = array[i + at];
|
773
|
+
for (i = 0; i < length; i++) array[i + at] = insert[i];
|
774
|
+
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
|
775
|
+
};
|
776
|
+
|
614
777
|
// Define the Collection's inheritable methods.
|
615
778
|
_.extend(Collection.prototype, Events, {
|
616
779
|
|
@@ -625,7 +788,7 @@
|
|
625
788
|
// The JSON representation of a Collection is an array of the
|
626
789
|
// models' attributes.
|
627
790
|
toJSON: function(options) {
|
628
|
-
return this.map(function(model){ return model.toJSON(options); });
|
791
|
+
return this.map(function(model) { return model.toJSON(options); });
|
629
792
|
},
|
630
793
|
|
631
794
|
// Proxy `Backbone.sync` by default.
|
@@ -633,32 +796,21 @@
|
|
633
796
|
return Backbone.sync.apply(this, arguments);
|
634
797
|
},
|
635
798
|
|
636
|
-
// Add a model, or list of models to the set.
|
799
|
+
// Add a model, or list of models to the set. `models` may be Backbone
|
800
|
+
// Models or raw JavaScript objects to be converted to Models, or any
|
801
|
+
// combination of the two.
|
637
802
|
add: function(models, options) {
|
638
803
|
return this.set(models, _.extend({merge: false}, options, addOptions));
|
639
804
|
},
|
640
805
|
|
641
806
|
// Remove a model, or a list of models from the set.
|
642
807
|
remove: function(models, options) {
|
808
|
+
options = _.extend({}, options);
|
643
809
|
var singular = !_.isArray(models);
|
644
810
|
models = singular ? [models] : _.clone(models);
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
model = models[i] = this.get(models[i]);
|
649
|
-
if (!model) continue;
|
650
|
-
delete this._byId[model.id];
|
651
|
-
delete this._byId[model.cid];
|
652
|
-
index = this.indexOf(model);
|
653
|
-
this.models.splice(index, 1);
|
654
|
-
this.length--;
|
655
|
-
if (!options.silent) {
|
656
|
-
options.index = index;
|
657
|
-
model.trigger('remove', model, this, options);
|
658
|
-
}
|
659
|
-
this._removeReference(model, options);
|
660
|
-
}
|
661
|
-
return singular ? models[0] : models;
|
811
|
+
var removed = this._removeModels(models, options);
|
812
|
+
if (!options.silent && removed) this.trigger('update', this, options);
|
813
|
+
return singular ? removed[0] : removed;
|
662
814
|
},
|
663
815
|
|
664
816
|
// Update a collection by `set`-ing a new list of models, adding new ones,
|
@@ -666,78 +818,88 @@
|
|
666
818
|
// already exist in the collection, as necessary. Similar to **Model#set**,
|
667
819
|
// the core operation for updating the data contained by the collection.
|
668
820
|
set: function(models, options) {
|
821
|
+
if (models == null) return;
|
822
|
+
|
669
823
|
options = _.defaults({}, options, setOptions);
|
670
|
-
if (options.parse) models = this.parse(models, options);
|
824
|
+
if (options.parse && !this._isModel(models)) models = this.parse(models, options);
|
825
|
+
|
671
826
|
var singular = !_.isArray(models);
|
672
|
-
models = singular ?
|
673
|
-
|
827
|
+
models = singular ? [models] : models.slice();
|
828
|
+
|
674
829
|
var at = options.at;
|
675
|
-
|
830
|
+
if (at != null) at = +at;
|
831
|
+
if (at < 0) at += this.length + 1;
|
832
|
+
|
833
|
+
var set = [];
|
834
|
+
var toAdd = [];
|
835
|
+
var toRemove = [];
|
836
|
+
var modelMap = {};
|
837
|
+
|
838
|
+
var add = options.add;
|
839
|
+
var merge = options.merge;
|
840
|
+
var remove = options.remove;
|
841
|
+
|
842
|
+
var sort = false;
|
676
843
|
var sortable = this.comparator && (at == null) && options.sort !== false;
|
677
844
|
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
678
|
-
var toAdd = [], toRemove = [], modelMap = {};
|
679
|
-
var add = options.add, merge = options.merge, remove = options.remove;
|
680
|
-
var order = !sortable && add && remove ? [] : false;
|
681
845
|
|
682
846
|
// Turn bare objects into model references, and prevent invalid models
|
683
847
|
// from being added.
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
id = model = attrs;
|
688
|
-
} else {
|
689
|
-
id = attrs[targetModel.prototype.idAttribute || 'id'];
|
690
|
-
}
|
848
|
+
var model;
|
849
|
+
for (var i = 0; i < models.length; i++) {
|
850
|
+
model = models[i];
|
691
851
|
|
692
852
|
// If a duplicate is found, prevent it from being added and
|
693
853
|
// optionally merge it into the existing model.
|
694
|
-
|
695
|
-
|
696
|
-
if (merge) {
|
697
|
-
attrs =
|
854
|
+
var existing = this.get(model);
|
855
|
+
if (existing) {
|
856
|
+
if (merge && model !== existing) {
|
857
|
+
var attrs = this._isModel(model) ? model.attributes : model;
|
698
858
|
if (options.parse) attrs = existing.parse(attrs, options);
|
699
859
|
existing.set(attrs, options);
|
700
|
-
if (sortable && !sort
|
860
|
+
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
|
861
|
+
}
|
862
|
+
if (!modelMap[existing.cid]) {
|
863
|
+
modelMap[existing.cid] = true;
|
864
|
+
set.push(existing);
|
701
865
|
}
|
702
866
|
models[i] = existing;
|
703
867
|
|
704
868
|
// If this is a new, valid model, push it to the `toAdd` list.
|
705
869
|
} else if (add) {
|
706
|
-
model = models[i] = this._prepareModel(
|
707
|
-
if (
|
708
|
-
|
709
|
-
|
870
|
+
model = models[i] = this._prepareModel(model, options);
|
871
|
+
if (model) {
|
872
|
+
toAdd.push(model);
|
873
|
+
this._addReference(model, options);
|
874
|
+
modelMap[model.cid] = true;
|
875
|
+
set.push(model);
|
876
|
+
}
|
710
877
|
}
|
711
|
-
|
712
|
-
// Do not add multiple models with the same `id`.
|
713
|
-
model = existing || model;
|
714
|
-
if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
|
715
|
-
modelMap[model.id] = true;
|
716
878
|
}
|
717
879
|
|
718
|
-
// Remove
|
880
|
+
// Remove stale models.
|
719
881
|
if (remove) {
|
720
|
-
for (i = 0
|
721
|
-
|
882
|
+
for (i = 0; i < this.length; i++) {
|
883
|
+
model = this.models[i];
|
884
|
+
if (!modelMap[model.cid]) toRemove.push(model);
|
722
885
|
}
|
723
|
-
if (toRemove.length) this.
|
886
|
+
if (toRemove.length) this._removeModels(toRemove, options);
|
724
887
|
}
|
725
888
|
|
726
889
|
// See if sorting is needed, update `length` and splice in new models.
|
727
|
-
|
890
|
+
var orderChanged = false;
|
891
|
+
var replace = !sortable && add && remove;
|
892
|
+
if (set.length && replace) {
|
893
|
+
orderChanged = this.length != set.length || _.some(this.models, function(model, index) {
|
894
|
+
return model !== set[index];
|
895
|
+
});
|
896
|
+
this.models.length = 0;
|
897
|
+
splice(this.models, set, 0);
|
898
|
+
this.length = this.models.length;
|
899
|
+
} else if (toAdd.length) {
|
728
900
|
if (sortable) sort = true;
|
729
|
-
this.
|
730
|
-
|
731
|
-
for (i = 0, l = toAdd.length; i < l; i++) {
|
732
|
-
this.models.splice(at + i, 0, toAdd[i]);
|
733
|
-
}
|
734
|
-
} else {
|
735
|
-
if (order) this.models.length = 0;
|
736
|
-
var orderedModels = order || toAdd;
|
737
|
-
for (i = 0, l = orderedModels.length; i < l; i++) {
|
738
|
-
this.models.push(orderedModels[i]);
|
739
|
-
}
|
740
|
-
}
|
901
|
+
splice(this.models, toAdd, at == null ? this.length : at);
|
902
|
+
this.length = this.models.length;
|
741
903
|
}
|
742
904
|
|
743
905
|
// Silently sort the collection if appropriate.
|
@@ -745,10 +907,13 @@
|
|
745
907
|
|
746
908
|
// Unless silenced, it's time to fire all appropriate add/sort events.
|
747
909
|
if (!options.silent) {
|
748
|
-
for (i = 0
|
749
|
-
(
|
910
|
+
for (i = 0; i < toAdd.length; i++) {
|
911
|
+
if (at != null) options.index = at + i;
|
912
|
+
model = toAdd[i];
|
913
|
+
model.trigger('add', model, this, options);
|
750
914
|
}
|
751
|
-
if (sort ||
|
915
|
+
if (sort || orderChanged) this.trigger('sort', this, options);
|
916
|
+
if (toAdd.length || toRemove.length) this.trigger('update', this, options);
|
752
917
|
}
|
753
918
|
|
754
919
|
// Return the added (or merged) model (or models).
|
@@ -760,8 +925,8 @@
|
|
760
925
|
// any granular `add` or `remove` events. Fires `reset` when finished.
|
761
926
|
// Useful for bulk operations and optimizations.
|
762
927
|
reset: function(models, options) {
|
763
|
-
options
|
764
|
-
for (var i = 0
|
928
|
+
options = options ? _.clone(options) : {};
|
929
|
+
for (var i = 0; i < this.models.length; i++) {
|
765
930
|
this._removeReference(this.models[i], options);
|
766
931
|
}
|
767
932
|
options.previousModels = this.models;
|
@@ -779,8 +944,7 @@
|
|
779
944
|
// Remove a model from the end of the collection.
|
780
945
|
pop: function(options) {
|
781
946
|
var model = this.at(this.length - 1);
|
782
|
-
this.remove(model, options);
|
783
|
-
return model;
|
947
|
+
return this.remove(model, options);
|
784
948
|
},
|
785
949
|
|
786
950
|
// Add a model to the beginning of the collection.
|
@@ -791,8 +955,7 @@
|
|
791
955
|
// Remove a model from the beginning of the collection.
|
792
956
|
shift: function(options) {
|
793
957
|
var model = this.at(0);
|
794
|
-
this.remove(model, options);
|
795
|
-
return model;
|
958
|
+
return this.remove(model, options);
|
796
959
|
},
|
797
960
|
|
798
961
|
// Slice out a sub-array of models from the collection.
|
@@ -803,24 +966,20 @@
|
|
803
966
|
// Get a model from the set by id.
|
804
967
|
get: function(obj) {
|
805
968
|
if (obj == null) return void 0;
|
806
|
-
|
969
|
+
var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
|
970
|
+
return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
|
807
971
|
},
|
808
972
|
|
809
973
|
// Get the model at the given index.
|
810
974
|
at: function(index) {
|
975
|
+
if (index < 0) index += this.length;
|
811
976
|
return this.models[index];
|
812
977
|
},
|
813
978
|
|
814
979
|
// Return models with matching attributes. Useful for simple cases of
|
815
980
|
// `filter`.
|
816
981
|
where: function(attrs, first) {
|
817
|
-
|
818
|
-
return this[first ? 'find' : 'filter'](function(model) {
|
819
|
-
for (var key in attrs) {
|
820
|
-
if (attrs[key] !== model.get(key)) return false;
|
821
|
-
}
|
822
|
-
return true;
|
823
|
-
});
|
982
|
+
return this[first ? 'find' : 'filter'](attrs);
|
824
983
|
},
|
825
984
|
|
826
985
|
// Return the first model with matching attributes. Useful for simple cases
|
@@ -833,16 +992,19 @@
|
|
833
992
|
// normal circumstances, as the set will maintain sort order as each item
|
834
993
|
// is added.
|
835
994
|
sort: function(options) {
|
836
|
-
|
995
|
+
var comparator = this.comparator;
|
996
|
+
if (!comparator) throw new Error('Cannot sort a set without a comparator');
|
837
997
|
options || (options = {});
|
838
998
|
|
999
|
+
var length = comparator.length;
|
1000
|
+
if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
|
1001
|
+
|
839
1002
|
// Run sort based on type of `comparator`.
|
840
|
-
if (_.isString(
|
841
|
-
this.models = this.sortBy(
|
1003
|
+
if (length === 1 || _.isString(comparator)) {
|
1004
|
+
this.models = this.sortBy(comparator);
|
842
1005
|
} else {
|
843
|
-
this.models.sort(
|
1006
|
+
this.models.sort(comparator);
|
844
1007
|
}
|
845
|
-
|
846
1008
|
if (!options.silent) this.trigger('sort', this, options);
|
847
1009
|
return this;
|
848
1010
|
},
|
@@ -856,14 +1018,13 @@
|
|
856
1018
|
// collection when they arrive. If `reset: true` is passed, the response
|
857
1019
|
// data will be passed through the `reset` method instead of `set`.
|
858
1020
|
fetch: function(options) {
|
859
|
-
options =
|
860
|
-
if (options.parse === void 0) options.parse = true;
|
1021
|
+
options = _.extend({parse: true}, options);
|
861
1022
|
var success = options.success;
|
862
1023
|
var collection = this;
|
863
1024
|
options.success = function(resp) {
|
864
1025
|
var method = options.reset ? 'reset' : 'set';
|
865
1026
|
collection[method](resp, options);
|
866
|
-
if (success) success(collection, resp, options);
|
1027
|
+
if (success) success.call(options.context, collection, resp, options);
|
867
1028
|
collection.trigger('sync', collection, resp, options);
|
868
1029
|
};
|
869
1030
|
wrapError(this, options);
|
@@ -875,13 +1036,15 @@
|
|
875
1036
|
// wait for the server to agree.
|
876
1037
|
create: function(model, options) {
|
877
1038
|
options = options ? _.clone(options) : {};
|
878
|
-
|
879
|
-
|
1039
|
+
var wait = options.wait;
|
1040
|
+
model = this._prepareModel(model, options);
|
1041
|
+
if (!model) return false;
|
1042
|
+
if (!wait) this.add(model, options);
|
880
1043
|
var collection = this;
|
881
1044
|
var success = options.success;
|
882
|
-
options.success = function(model, resp) {
|
883
|
-
if (
|
884
|
-
if (success) success(model, resp,
|
1045
|
+
options.success = function(model, resp, callbackOpts) {
|
1046
|
+
if (wait) collection.add(model, callbackOpts);
|
1047
|
+
if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
|
885
1048
|
};
|
886
1049
|
model.save(null, options);
|
887
1050
|
return model;
|
@@ -895,7 +1058,15 @@
|
|
895
1058
|
|
896
1059
|
// Create a new collection with an identical list of models as this one.
|
897
1060
|
clone: function() {
|
898
|
-
return new this.constructor(this.models
|
1061
|
+
return new this.constructor(this.models, {
|
1062
|
+
model: this.model,
|
1063
|
+
comparator: this.comparator
|
1064
|
+
});
|
1065
|
+
},
|
1066
|
+
|
1067
|
+
// Define how to uniquely identify models in the collection.
|
1068
|
+
modelId: function (attrs) {
|
1069
|
+
return attrs[this.model.prototype.idAttribute || 'id'];
|
899
1070
|
},
|
900
1071
|
|
901
1072
|
// Private method to reset all internal state. Called when the collection
|
@@ -909,7 +1080,10 @@
|
|
909
1080
|
// Prepare a hash of attributes (or other model) to be added to this
|
910
1081
|
// collection.
|
911
1082
|
_prepareModel: function(attrs, options) {
|
912
|
-
if (attrs
|
1083
|
+
if (this._isModel(attrs)) {
|
1084
|
+
if (!attrs.collection) attrs.collection = this;
|
1085
|
+
return attrs;
|
1086
|
+
}
|
913
1087
|
options = options ? _.clone(options) : {};
|
914
1088
|
options.collection = this;
|
915
1089
|
var model = new this.model(attrs, options);
|
@@ -918,16 +1092,47 @@
|
|
918
1092
|
return false;
|
919
1093
|
},
|
920
1094
|
|
1095
|
+
// Internal method called by both remove and set.
|
1096
|
+
_removeModels: function(models, options) {
|
1097
|
+
var removed = [];
|
1098
|
+
for (var i = 0; i < models.length; i++) {
|
1099
|
+
var model = this.get(models[i]);
|
1100
|
+
if (!model) continue;
|
1101
|
+
|
1102
|
+
var index = this.indexOf(model);
|
1103
|
+
this.models.splice(index, 1);
|
1104
|
+
this.length--;
|
1105
|
+
|
1106
|
+
if (!options.silent) {
|
1107
|
+
options.index = index;
|
1108
|
+
model.trigger('remove', model, this, options);
|
1109
|
+
}
|
1110
|
+
|
1111
|
+
removed.push(model);
|
1112
|
+
this._removeReference(model, options);
|
1113
|
+
}
|
1114
|
+
return removed.length ? removed : false;
|
1115
|
+
},
|
1116
|
+
|
1117
|
+
// Method for checking whether an object should be considered a model for
|
1118
|
+
// the purposes of adding to the collection.
|
1119
|
+
_isModel: function (model) {
|
1120
|
+
return model instanceof Model;
|
1121
|
+
},
|
1122
|
+
|
921
1123
|
// Internal method to create a model's ties to a collection.
|
922
1124
|
_addReference: function(model, options) {
|
923
1125
|
this._byId[model.cid] = model;
|
924
|
-
|
925
|
-
if (
|
1126
|
+
var id = this.modelId(model.attributes);
|
1127
|
+
if (id != null) this._byId[id] = model;
|
926
1128
|
model.on('all', this._onModelEvent, this);
|
927
1129
|
},
|
928
1130
|
|
929
1131
|
// Internal method to sever a model's ties to a collection.
|
930
1132
|
_removeReference: function(model, options) {
|
1133
|
+
delete this._byId[model.cid];
|
1134
|
+
var id = this.modelId(model.attributes);
|
1135
|
+
if (id != null) delete this._byId[id];
|
931
1136
|
if (this === model.collection) delete model.collection;
|
932
1137
|
model.off('all', this._onModelEvent, this);
|
933
1138
|
},
|
@@ -939,9 +1144,13 @@
|
|
939
1144
|
_onModelEvent: function(event, model, collection, options) {
|
940
1145
|
if ((event === 'add' || event === 'remove') && collection !== this) return;
|
941
1146
|
if (event === 'destroy') this.remove(model, options);
|
942
|
-
if (
|
943
|
-
|
944
|
-
|
1147
|
+
if (event === 'change') {
|
1148
|
+
var prevId = this.modelId(model.previousAttributes());
|
1149
|
+
var id = this.modelId(model.attributes);
|
1150
|
+
if (prevId !== id) {
|
1151
|
+
if (prevId != null) delete this._byId[prevId];
|
1152
|
+
if (id != null) this._byId[id] = model;
|
1153
|
+
}
|
945
1154
|
}
|
946
1155
|
this.trigger.apply(this, arguments);
|
947
1156
|
}
|
@@ -951,34 +1160,17 @@
|
|
951
1160
|
// Underscore methods that we want to implement on the Collection.
|
952
1161
|
// 90% of the core usefulness of Backbone Collections is actually implemented
|
953
1162
|
// right here:
|
954
|
-
var
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
1163
|
+
var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
|
1164
|
+
foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
|
1165
|
+
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
|
1166
|
+
contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
|
1167
|
+
head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
|
1168
|
+
without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
|
1169
|
+
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
|
1170
|
+
sortBy: 3, indexBy: 3};
|
960
1171
|
|
961
1172
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
962
|
-
|
963
|
-
Collection.prototype[method] = function() {
|
964
|
-
var args = slice.call(arguments);
|
965
|
-
args.unshift(this.models);
|
966
|
-
return _[method].apply(_, args);
|
967
|
-
};
|
968
|
-
});
|
969
|
-
|
970
|
-
// Underscore methods that take a property name as an argument.
|
971
|
-
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
|
972
|
-
|
973
|
-
// Use attributes instead of properties.
|
974
|
-
_.each(attributeMethods, function(method) {
|
975
|
-
Collection.prototype[method] = function(value, context) {
|
976
|
-
var iterator = _.isFunction(value) ? value : function(model) {
|
977
|
-
return model.get(value);
|
978
|
-
};
|
979
|
-
return _[method](this.models, iterator, context);
|
980
|
-
};
|
981
|
-
});
|
1173
|
+
addUnderscoreMethods(Collection, collectionMethods, 'models');
|
982
1174
|
|
983
1175
|
// Backbone.View
|
984
1176
|
// -------------
|
@@ -995,17 +1187,15 @@
|
|
995
1187
|
// if an existing element is not provided...
|
996
1188
|
var View = Backbone.View = function(options) {
|
997
1189
|
this.cid = _.uniqueId('view');
|
998
|
-
options || (options = {});
|
999
1190
|
_.extend(this, _.pick(options, viewOptions));
|
1000
1191
|
this._ensureElement();
|
1001
1192
|
this.initialize.apply(this, arguments);
|
1002
|
-
this.delegateEvents();
|
1003
1193
|
};
|
1004
1194
|
|
1005
1195
|
// Cached regex to split keys for `delegate`.
|
1006
1196
|
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1007
1197
|
|
1008
|
-
// List of view options to be
|
1198
|
+
// List of view options to be set as properties.
|
1009
1199
|
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
1010
1200
|
|
1011
1201
|
// Set up all inheritable **Backbone.View** properties and methods.
|
@@ -1034,21 +1224,37 @@
|
|
1034
1224
|
// Remove this view by taking the element out of the DOM, and removing any
|
1035
1225
|
// applicable Backbone.Events listeners.
|
1036
1226
|
remove: function() {
|
1037
|
-
this
|
1227
|
+
this._removeElement();
|
1038
1228
|
this.stopListening();
|
1039
1229
|
return this;
|
1040
1230
|
},
|
1041
1231
|
|
1042
|
-
//
|
1043
|
-
//
|
1044
|
-
|
1045
|
-
|
1046
|
-
this.$el
|
1047
|
-
|
1048
|
-
|
1232
|
+
// Remove this view's element from the document and all event listeners
|
1233
|
+
// attached to it. Exposed for subclasses using an alternative DOM
|
1234
|
+
// manipulation API.
|
1235
|
+
_removeElement: function() {
|
1236
|
+
this.$el.remove();
|
1237
|
+
},
|
1238
|
+
|
1239
|
+
// Change the view's element (`this.el` property) and re-delegate the
|
1240
|
+
// view's events on the new element.
|
1241
|
+
setElement: function(element) {
|
1242
|
+
this.undelegateEvents();
|
1243
|
+
this._setElement(element);
|
1244
|
+
this.delegateEvents();
|
1049
1245
|
return this;
|
1050
1246
|
},
|
1051
1247
|
|
1248
|
+
// Creates the `this.el` and `this.$el` references for this view using the
|
1249
|
+
// given `el`. `el` can be a CSS selector or an HTML string, a jQuery
|
1250
|
+
// context or an element. Subclasses can override this to utilize an
|
1251
|
+
// alternative DOM manipulation API and are only required to set the
|
1252
|
+
// `this.el` property.
|
1253
|
+
_setElement: function(el) {
|
1254
|
+
this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
|
1255
|
+
this.el = this.$el[0];
|
1256
|
+
},
|
1257
|
+
|
1052
1258
|
// Set callbacks, where `this.events` is a hash of
|
1053
1259
|
//
|
1054
1260
|
// *{"event selector": "callback"}*
|
@@ -1062,37 +1268,49 @@
|
|
1062
1268
|
// pairs. Callbacks will be bound to the view, with `this` set properly.
|
1063
1269
|
// Uses event delegation for efficiency.
|
1064
1270
|
// Omitting the selector binds the event to `this.el`.
|
1065
|
-
// This only works for delegate-able events: not `focus`, `blur`, and
|
1066
|
-
// not `change`, `submit`, and `reset` in Internet Explorer.
|
1067
1271
|
delegateEvents: function(events) {
|
1068
|
-
|
1272
|
+
events || (events = _.result(this, 'events'));
|
1273
|
+
if (!events) return this;
|
1069
1274
|
this.undelegateEvents();
|
1070
1275
|
for (var key in events) {
|
1071
1276
|
var method = events[key];
|
1072
|
-
if (!_.isFunction(method)) method = this[
|
1277
|
+
if (!_.isFunction(method)) method = this[method];
|
1073
1278
|
if (!method) continue;
|
1074
|
-
|
1075
1279
|
var match = key.match(delegateEventSplitter);
|
1076
|
-
|
1077
|
-
method = _.bind(method, this);
|
1078
|
-
eventName += '.delegateEvents' + this.cid;
|
1079
|
-
if (selector === '') {
|
1080
|
-
this.$el.on(eventName, method);
|
1081
|
-
} else {
|
1082
|
-
this.$el.on(eventName, selector, method);
|
1083
|
-
}
|
1280
|
+
this.delegate(match[1], match[2], _.bind(method, this));
|
1084
1281
|
}
|
1085
1282
|
return this;
|
1086
1283
|
},
|
1087
1284
|
|
1088
|
-
//
|
1285
|
+
// Add a single event listener to the view's element (or a child element
|
1286
|
+
// using `selector`). This only works for delegate-able events: not `focus`,
|
1287
|
+
// `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
|
1288
|
+
delegate: function(eventName, selector, listener) {
|
1289
|
+
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
|
1290
|
+
return this;
|
1291
|
+
},
|
1292
|
+
|
1293
|
+
// Clears all callbacks previously bound to the view by `delegateEvents`.
|
1089
1294
|
// You usually don't need to use this, but may wish to if you have multiple
|
1090
1295
|
// Backbone views attached to the same DOM element.
|
1091
1296
|
undelegateEvents: function() {
|
1092
|
-
this.$el.off('.delegateEvents' + this.cid);
|
1297
|
+
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
|
1298
|
+
return this;
|
1299
|
+
},
|
1300
|
+
|
1301
|
+
// A finer-grained `undelegateEvents` for removing a single delegated event.
|
1302
|
+
// `selector` and `listener` are both optional.
|
1303
|
+
undelegate: function(eventName, selector, listener) {
|
1304
|
+
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
|
1093
1305
|
return this;
|
1094
1306
|
},
|
1095
1307
|
|
1308
|
+
// Produces a DOM element to be assigned to your view. Exposed for
|
1309
|
+
// subclasses using an alternative DOM manipulation API.
|
1310
|
+
_createElement: function(tagName) {
|
1311
|
+
return document.createElement(tagName);
|
1312
|
+
},
|
1313
|
+
|
1096
1314
|
// Ensure that the View has a DOM element to render into.
|
1097
1315
|
// If `this.el` is a string, pass it through `$()`, take the first
|
1098
1316
|
// matching element, and re-assign it to `el`. Otherwise, create
|
@@ -1102,11 +1320,17 @@
|
|
1102
1320
|
var attrs = _.extend({}, _.result(this, 'attributes'));
|
1103
1321
|
if (this.id) attrs.id = _.result(this, 'id');
|
1104
1322
|
if (this.className) attrs['class'] = _.result(this, 'className');
|
1105
|
-
|
1106
|
-
this.
|
1323
|
+
this.setElement(this._createElement(_.result(this, 'tagName')));
|
1324
|
+
this._setAttributes(attrs);
|
1107
1325
|
} else {
|
1108
|
-
this.setElement(_.result(this, 'el')
|
1326
|
+
this.setElement(_.result(this, 'el'));
|
1109
1327
|
}
|
1328
|
+
},
|
1329
|
+
|
1330
|
+
// Set attributes from a hash on this view's element. Exposed for
|
1331
|
+
// subclasses using an alternative DOM manipulation API.
|
1332
|
+
_setAttributes: function(attributes) {
|
1333
|
+
this.$el.attr(attributes);
|
1110
1334
|
}
|
1111
1335
|
|
1112
1336
|
});
|
@@ -1175,14 +1399,13 @@
|
|
1175
1399
|
params.processData = false;
|
1176
1400
|
}
|
1177
1401
|
|
1178
|
-
//
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
}
|
1402
|
+
// Pass along `textStatus` and `errorThrown` from jQuery.
|
1403
|
+
var error = options.error;
|
1404
|
+
options.error = function(xhr, textStatus, errorThrown) {
|
1405
|
+
options.textStatus = textStatus;
|
1406
|
+
options.errorThrown = errorThrown;
|
1407
|
+
if (error) error.call(options.context, xhr, textStatus, errorThrown);
|
1408
|
+
};
|
1186
1409
|
|
1187
1410
|
// Make the request, allowing the user to override any Ajax options.
|
1188
1411
|
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
|
@@ -1190,10 +1413,6 @@
|
|
1190
1413
|
return xhr;
|
1191
1414
|
};
|
1192
1415
|
|
1193
|
-
var noXhrPatch =
|
1194
|
-
typeof window !== 'undefined' && !!window.ActiveXObject &&
|
1195
|
-
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
|
1196
|
-
|
1197
1416
|
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
1198
1417
|
var methodMap = {
|
1199
1418
|
'create': 'POST',
|
@@ -1251,17 +1470,18 @@
|
|
1251
1470
|
var router = this;
|
1252
1471
|
Backbone.history.route(route, function(fragment) {
|
1253
1472
|
var args = router._extractParameters(route, fragment);
|
1254
|
-
router.execute(callback, args)
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1473
|
+
if (router.execute(callback, args, name) !== false) {
|
1474
|
+
router.trigger.apply(router, ['route:' + name].concat(args));
|
1475
|
+
router.trigger('route', name, args);
|
1476
|
+
Backbone.history.trigger('route', router, name, args);
|
1477
|
+
}
|
1258
1478
|
});
|
1259
1479
|
return this;
|
1260
1480
|
},
|
1261
1481
|
|
1262
1482
|
// Execute a route handler with the provided parameters. This is an
|
1263
1483
|
// excellent place to do pre-route setup or post-route cleanup.
|
1264
|
-
execute: function(callback, args) {
|
1484
|
+
execute: function(callback, args, name) {
|
1265
1485
|
if (callback) callback.apply(this, args);
|
1266
1486
|
},
|
1267
1487
|
|
@@ -1319,7 +1539,7 @@
|
|
1319
1539
|
// falls back to polling.
|
1320
1540
|
var History = Backbone.History = function() {
|
1321
1541
|
this.handlers = [];
|
1322
|
-
_.
|
1542
|
+
this.checkUrl = _.bind(this.checkUrl, this);
|
1323
1543
|
|
1324
1544
|
// Ensure that `History` can be used outside of the browser.
|
1325
1545
|
if (typeof window !== 'undefined') {
|
@@ -1334,12 +1554,6 @@
|
|
1334
1554
|
// Cached regex for stripping leading and trailing slashes.
|
1335
1555
|
var rootStripper = /^\/+|\/+$/g;
|
1336
1556
|
|
1337
|
-
// Cached regex for detecting MSIE.
|
1338
|
-
var isExplorer = /msie [\w.]+/;
|
1339
|
-
|
1340
|
-
// Cached regex for removing a trailing slash.
|
1341
|
-
var trailingSlash = /\/$/;
|
1342
|
-
|
1343
1557
|
// Cached regex for stripping urls of hash.
|
1344
1558
|
var pathStripper = /#.*$/;
|
1345
1559
|
|
@@ -1355,7 +1569,29 @@
|
|
1355
1569
|
|
1356
1570
|
// Are we at the app root?
|
1357
1571
|
atRoot: function() {
|
1358
|
-
|
1572
|
+
var path = this.location.pathname.replace(/[^\/]$/, '$&/');
|
1573
|
+
return path === this.root && !this.getSearch();
|
1574
|
+
},
|
1575
|
+
|
1576
|
+
// Does the pathname match the root?
|
1577
|
+
matchRoot: function() {
|
1578
|
+
var path = this.decodeFragment(this.location.pathname);
|
1579
|
+
var root = path.slice(0, this.root.length - 1) + '/';
|
1580
|
+
return root === this.root;
|
1581
|
+
},
|
1582
|
+
|
1583
|
+
// Unicode characters in `location.pathname` are percent encoded so they're
|
1584
|
+
// decoded for comparison. `%25` should not be decoded since it may be part
|
1585
|
+
// of an encoded parameter.
|
1586
|
+
decodeFragment: function(fragment) {
|
1587
|
+
return decodeURI(fragment.replace(/%25/g, '%2525'));
|
1588
|
+
},
|
1589
|
+
|
1590
|
+
// In IE6, the hash fragment and search params are incorrect if the
|
1591
|
+
// fragment contains `?`.
|
1592
|
+
getSearch: function() {
|
1593
|
+
var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
|
1594
|
+
return match ? match[0] : '';
|
1359
1595
|
},
|
1360
1596
|
|
1361
1597
|
// Gets the true hash value. Cannot use location.hash directly due to bug
|
@@ -1365,14 +1601,19 @@
|
|
1365
1601
|
return match ? match[1] : '';
|
1366
1602
|
},
|
1367
1603
|
|
1368
|
-
// Get the
|
1369
|
-
|
1370
|
-
|
1604
|
+
// Get the pathname and search params, without the root.
|
1605
|
+
getPath: function() {
|
1606
|
+
var path = this.decodeFragment(
|
1607
|
+
this.location.pathname + this.getSearch()
|
1608
|
+
).slice(this.root.length - 1);
|
1609
|
+
return path.charAt(0) === '/' ? path.slice(1) : path;
|
1610
|
+
},
|
1611
|
+
|
1612
|
+
// Get the cross-browser normalized URL fragment from the path or hash.
|
1613
|
+
getFragment: function(fragment) {
|
1371
1614
|
if (fragment == null) {
|
1372
|
-
if (this.
|
1373
|
-
fragment =
|
1374
|
-
var root = this.root.replace(trailingSlash, '');
|
1375
|
-
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
|
1615
|
+
if (this._usePushState || !this._wantsHashChange) {
|
1616
|
+
fragment = this.getPath();
|
1376
1617
|
} else {
|
1377
1618
|
fragment = this.getHash();
|
1378
1619
|
}
|
@@ -1383,7 +1624,7 @@
|
|
1383
1624
|
// Start the hash change handling, returning `true` if the current URL matches
|
1384
1625
|
// an existing route, and `false` otherwise.
|
1385
1626
|
start: function(options) {
|
1386
|
-
if (History.started) throw new Error(
|
1627
|
+
if (History.started) throw new Error('Backbone.history has already been started');
|
1387
1628
|
History.started = true;
|
1388
1629
|
|
1389
1630
|
// Figure out the initial configuration. Do we need an iframe?
|
@@ -1391,36 +1632,16 @@
|
|
1391
1632
|
this.options = _.extend({root: '/'}, this.options, options);
|
1392
1633
|
this.root = this.options.root;
|
1393
1634
|
this._wantsHashChange = this.options.hashChange !== false;
|
1635
|
+
this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
|
1636
|
+
this._useHashChange = this._wantsHashChange && this._hasHashChange;
|
1394
1637
|
this._wantsPushState = !!this.options.pushState;
|
1395
|
-
this._hasPushState = !!(this.
|
1396
|
-
|
1397
|
-
|
1398
|
-
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
1638
|
+
this._hasPushState = !!(this.history && this.history.pushState);
|
1639
|
+
this._usePushState = this._wantsPushState && this._hasPushState;
|
1640
|
+
this.fragment = this.getFragment();
|
1399
1641
|
|
1400
1642
|
// Normalize root to always include a leading and trailing slash.
|
1401
1643
|
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
|
1402
1644
|
|
1403
|
-
if (oldIE && this._wantsHashChange) {
|
1404
|
-
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
|
1405
|
-
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
|
1406
|
-
this.navigate(fragment);
|
1407
|
-
}
|
1408
|
-
|
1409
|
-
// Depending on whether we're using pushState or hashes, and whether
|
1410
|
-
// 'onhashchange' is supported, determine how we check the URL state.
|
1411
|
-
if (this._hasPushState) {
|
1412
|
-
Backbone.$(window).on('popstate', this.checkUrl);
|
1413
|
-
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
|
1414
|
-
Backbone.$(window).on('hashchange', this.checkUrl);
|
1415
|
-
} else if (this._wantsHashChange) {
|
1416
|
-
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
|
1417
|
-
}
|
1418
|
-
|
1419
|
-
// Determine if we need to change the base url, for a pushState link
|
1420
|
-
// opened by a non-pushState browser.
|
1421
|
-
this.fragment = fragment;
|
1422
|
-
var loc = this.location;
|
1423
|
-
|
1424
1645
|
// Transition from hashChange to pushState or vice versa if both are
|
1425
1646
|
// requested.
|
1426
1647
|
if (this._wantsHashChange && this._wantsPushState) {
|
@@ -1428,27 +1649,75 @@
|
|
1428
1649
|
// If we've started off with a route from a `pushState`-enabled
|
1429
1650
|
// browser, but we're currently in a browser that doesn't support it...
|
1430
1651
|
if (!this._hasPushState && !this.atRoot()) {
|
1431
|
-
|
1432
|
-
this.location.replace(
|
1652
|
+
var root = this.root.slice(0, -1) || '/';
|
1653
|
+
this.location.replace(root + '#' + this.getPath());
|
1433
1654
|
// Return immediately as browser will do redirect to new url
|
1434
1655
|
return true;
|
1435
1656
|
|
1436
1657
|
// Or if we've started out with a hash-based route, but we're currently
|
1437
1658
|
// in a browser where it could be `pushState`-based instead...
|
1438
|
-
} else if (this._hasPushState && this.atRoot()
|
1439
|
-
this.
|
1440
|
-
this.history.replaceState({}, document.title, this.root + this.fragment);
|
1659
|
+
} else if (this._hasPushState && this.atRoot()) {
|
1660
|
+
this.navigate(this.getHash(), {replace: true});
|
1441
1661
|
}
|
1442
1662
|
|
1443
1663
|
}
|
1444
1664
|
|
1665
|
+
// Proxy an iframe to handle location events if the browser doesn't
|
1666
|
+
// support the `hashchange` event, HTML5 history, or the user wants
|
1667
|
+
// `hashChange` but not `pushState`.
|
1668
|
+
if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
|
1669
|
+
this.iframe = document.createElement('iframe');
|
1670
|
+
this.iframe.src = 'javascript:0';
|
1671
|
+
this.iframe.style.display = 'none';
|
1672
|
+
this.iframe.tabIndex = -1;
|
1673
|
+
var body = document.body;
|
1674
|
+
// Using `appendChild` will throw on IE < 9 if the document is not ready.
|
1675
|
+
var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
|
1676
|
+
iWindow.document.open();
|
1677
|
+
iWindow.document.close();
|
1678
|
+
iWindow.location.hash = '#' + this.fragment;
|
1679
|
+
}
|
1680
|
+
|
1681
|
+
// Add a cross-platform `addEventListener` shim for older browsers.
|
1682
|
+
var addEventListener = window.addEventListener || function (eventName, listener) {
|
1683
|
+
return attachEvent('on' + eventName, listener);
|
1684
|
+
};
|
1685
|
+
|
1686
|
+
// Depending on whether we're using pushState or hashes, and whether
|
1687
|
+
// 'onhashchange' is supported, determine how we check the URL state.
|
1688
|
+
if (this._usePushState) {
|
1689
|
+
addEventListener('popstate', this.checkUrl, false);
|
1690
|
+
} else if (this._useHashChange && !this.iframe) {
|
1691
|
+
addEventListener('hashchange', this.checkUrl, false);
|
1692
|
+
} else if (this._wantsHashChange) {
|
1693
|
+
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
|
1694
|
+
}
|
1695
|
+
|
1445
1696
|
if (!this.options.silent) return this.loadUrl();
|
1446
1697
|
},
|
1447
1698
|
|
1448
1699
|
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
|
1449
1700
|
// but possibly useful for unit testing Routers.
|
1450
1701
|
stop: function() {
|
1451
|
-
|
1702
|
+
// Add a cross-platform `removeEventListener` shim for older browsers.
|
1703
|
+
var removeEventListener = window.removeEventListener || function (eventName, listener) {
|
1704
|
+
return detachEvent('on' + eventName, listener);
|
1705
|
+
};
|
1706
|
+
|
1707
|
+
// Remove window listeners.
|
1708
|
+
if (this._usePushState) {
|
1709
|
+
removeEventListener('popstate', this.checkUrl, false);
|
1710
|
+
} else if (this._useHashChange && !this.iframe) {
|
1711
|
+
removeEventListener('hashchange', this.checkUrl, false);
|
1712
|
+
}
|
1713
|
+
|
1714
|
+
// Clean up the iframe if necessary.
|
1715
|
+
if (this.iframe) {
|
1716
|
+
document.body.removeChild(this.iframe);
|
1717
|
+
this.iframe = null;
|
1718
|
+
}
|
1719
|
+
|
1720
|
+
// Some environments will throw when clearing an undefined interval.
|
1452
1721
|
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
|
1453
1722
|
History.started = false;
|
1454
1723
|
},
|
@@ -1463,9 +1732,13 @@
|
|
1463
1732
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
1464
1733
|
checkUrl: function(e) {
|
1465
1734
|
var current = this.getFragment();
|
1735
|
+
|
1736
|
+
// If the user pressed the back button, the iframe's hash will have
|
1737
|
+
// changed and we should use that for comparison.
|
1466
1738
|
if (current === this.fragment && this.iframe) {
|
1467
|
-
current = this.
|
1739
|
+
current = this.getHash(this.iframe.contentWindow);
|
1468
1740
|
}
|
1741
|
+
|
1469
1742
|
if (current === this.fragment) return false;
|
1470
1743
|
if (this.iframe) this.navigate(current);
|
1471
1744
|
this.loadUrl();
|
@@ -1475,8 +1748,10 @@
|
|
1475
1748
|
// match, returns `true`. If no defined routes matches the fragment,
|
1476
1749
|
// returns `false`.
|
1477
1750
|
loadUrl: function(fragment) {
|
1751
|
+
// If the root doesn't match, no routes can match either.
|
1752
|
+
if (!this.matchRoot()) return false;
|
1478
1753
|
fragment = this.fragment = this.getFragment(fragment);
|
1479
|
-
return _.
|
1754
|
+
return _.some(this.handlers, function(handler) {
|
1480
1755
|
if (handler.route.test(fragment)) {
|
1481
1756
|
handler.callback(fragment);
|
1482
1757
|
return true;
|
@@ -1495,31 +1770,42 @@
|
|
1495
1770
|
if (!History.started) return false;
|
1496
1771
|
if (!options || options === true) options = {trigger: !!options};
|
1497
1772
|
|
1498
|
-
|
1773
|
+
// Normalize the fragment.
|
1774
|
+
fragment = this.getFragment(fragment || '');
|
1499
1775
|
|
1500
|
-
//
|
1501
|
-
|
1776
|
+
// Don't include a trailing slash on the root.
|
1777
|
+
var root = this.root;
|
1778
|
+
if (fragment === '' || fragment.charAt(0) === '?') {
|
1779
|
+
root = root.slice(0, -1) || '/';
|
1780
|
+
}
|
1781
|
+
var url = root + fragment;
|
1782
|
+
|
1783
|
+
// Strip the hash and decode for matching.
|
1784
|
+
fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
|
1502
1785
|
|
1503
1786
|
if (this.fragment === fragment) return;
|
1504
1787
|
this.fragment = fragment;
|
1505
1788
|
|
1506
|
-
// Don't include a trailing slash on the root.
|
1507
|
-
if (fragment === '' && url !== '/') url = url.slice(0, -1);
|
1508
|
-
|
1509
1789
|
// If pushState is available, we use it to set the fragment as a real URL.
|
1510
|
-
if (this.
|
1790
|
+
if (this._usePushState) {
|
1511
1791
|
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
|
1512
1792
|
|
1513
1793
|
// If hash changes haven't been explicitly disabled, update the hash
|
1514
1794
|
// fragment to store history.
|
1515
1795
|
} else if (this._wantsHashChange) {
|
1516
1796
|
this._updateHash(this.location, fragment, options.replace);
|
1517
|
-
if (this.iframe && (fragment !== this.
|
1797
|
+
if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
|
1798
|
+
var iWindow = this.iframe.contentWindow;
|
1799
|
+
|
1518
1800
|
// Opening and closing the iframe tricks IE7 and earlier to push a
|
1519
1801
|
// history entry on hash-tag change. When replace is true, we don't
|
1520
1802
|
// want this.
|
1521
|
-
if(!options.replace)
|
1522
|
-
|
1803
|
+
if (!options.replace) {
|
1804
|
+
iWindow.document.open();
|
1805
|
+
iWindow.document.close();
|
1806
|
+
}
|
1807
|
+
|
1808
|
+
this._updateHash(iWindow.location, fragment, options.replace);
|
1523
1809
|
}
|
1524
1810
|
|
1525
1811
|
// If you've told us that you explicitly don't want fallback hashchange-
|
@@ -1550,7 +1836,7 @@
|
|
1550
1836
|
// Helpers
|
1551
1837
|
// -------
|
1552
1838
|
|
1553
|
-
// Helper function to correctly set up the prototype chain
|
1839
|
+
// Helper function to correctly set up the prototype chain for subclasses.
|
1554
1840
|
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
1555
1841
|
// class properties to be extended.
|
1556
1842
|
var extend = function(protoProps, staticProps) {
|
@@ -1559,7 +1845,7 @@
|
|
1559
1845
|
|
1560
1846
|
// The constructor function for the new subclass is either defined by you
|
1561
1847
|
// (the "constructor" property in your `extend` definition), or defaulted
|
1562
|
-
// by us to simply call the parent
|
1848
|
+
// by us to simply call the parent constructor.
|
1563
1849
|
if (protoProps && _.has(protoProps, 'constructor')) {
|
1564
1850
|
child = protoProps.constructor;
|
1565
1851
|
} else {
|
@@ -1570,7 +1856,7 @@
|
|
1570
1856
|
_.extend(child, parent, staticProps);
|
1571
1857
|
|
1572
1858
|
// Set the prototype chain to inherit from `parent`, without calling
|
1573
|
-
// `parent`
|
1859
|
+
// `parent` constructor function.
|
1574
1860
|
var Surrogate = function(){ this.constructor = child; };
|
1575
1861
|
Surrogate.prototype = parent.prototype;
|
1576
1862
|
child.prototype = new Surrogate;
|
@@ -1598,7 +1884,7 @@
|
|
1598
1884
|
var wrapError = function(model, options) {
|
1599
1885
|
var error = options.error;
|
1600
1886
|
options.error = function(resp) {
|
1601
|
-
if (error) error(model, resp, options);
|
1887
|
+
if (error) error.call(options.context, model, resp, options);
|
1602
1888
|
model.trigger('error', model, resp, options);
|
1603
1889
|
};
|
1604
1890
|
};
|