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