diff options
Diffstat (limited to 'webapp/vendor/backbone-1.1.2.js')
-rw-r--r-- | webapp/vendor/backbone-1.1.2.js | 1608 |
1 files changed, 1608 insertions, 0 deletions
diff --git a/webapp/vendor/backbone-1.1.2.js b/webapp/vendor/backbone-1.1.2.js new file mode 100644 index 0000000..24a550a --- /dev/null +++ b/webapp/vendor/backbone-1.1.2.js | |||
@@ -0,0 +1,1608 @@ | |||
1 | // Backbone.js 1.1.2 | ||
2 | |||
3 | // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors | ||
4 | // Backbone may be freely distributed under the MIT license. | ||
5 | // For all details and documentation: | ||
6 | // http://backbonejs.org | ||
7 | |||
8 | (function(root, factory) { | ||
9 | |||
10 | // Set up Backbone appropriately for the environment. Start with AMD. | ||
11 | if (typeof define === 'function' && define.amd) { | ||
12 | define(['underscore', 'jquery', 'exports'], function(_, $, exports) { | ||
13 | // Export global even in AMD case in case this script is loaded with | ||
14 | // others that may still expect a global Backbone. | ||
15 | root.Backbone = factory(root, exports, _, $); | ||
16 | }); | ||
17 | |||
18 | // Next for Node.js or CommonJS. jQuery may not be needed as a module. | ||
19 | } else if (typeof exports !== 'undefined') { | ||
20 | var _ = require('underscore'); | ||
21 | factory(root, exports, _); | ||
22 | |||
23 | // Finally, as a browser global. | ||
24 | } else { | ||
25 | root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); | ||
26 | } | ||
27 | |||
28 | }(this, function(root, Backbone, _, $) { | ||
29 | |||
30 | // Initial Setup | ||
31 | // ------------- | ||
32 | |||
33 | // Save the previous value of the `Backbone` variable, so that it can be | ||
34 | // restored later on, if `noConflict` is used. | ||
35 | var previousBackbone = root.Backbone; | ||
36 | |||
37 | // Create local references to array methods we'll want to use later. | ||
38 | var array = []; | ||
39 | var push = array.push; | ||
40 | var slice = array.slice; | ||
41 | var splice = array.splice; | ||
42 | |||
43 | // Current version of the library. Keep in sync with `package.json`. | ||
44 | Backbone.VERSION = '1.1.2'; | ||
45 | |||
46 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns | ||
47 | // the `$` variable. | ||
48 | Backbone.$ = $; | ||
49 | |||
50 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable | ||
51 | // to its previous owner. Returns a reference to this Backbone object. | ||
52 | Backbone.noConflict = function() { | ||
53 | root.Backbone = previousBackbone; | ||
54 | return this; | ||
55 | }; | ||
56 | |||
57 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option | ||
58 | // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and | ||
59 | // set a `X-Http-Method-Override` header. | ||
60 | Backbone.emulateHTTP = false; | ||
61 | |||
62 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct | ||
63 | // `application/json` requests ... will encode the body as | ||
64 | // `application/x-www-form-urlencoded` instead and will send the model in a | ||
65 | // form param named `model`. | ||
66 | Backbone.emulateJSON = false; | ||
67 | |||
68 | // Backbone.Events | ||
69 | // --------------- | ||
70 | |||
71 | // A module that can be mixed in to *any object* in order to provide it with | ||
72 | // custom events. You may bind with `on` or remove with `off` callback | ||
73 | // functions to an event; `trigger`-ing an event fires all callbacks in | ||
74 | // succession. | ||
75 | // | ||
76 | // var object = {}; | ||
77 | // _.extend(object, Backbone.Events); | ||
78 | // object.on('expand', function(){ alert('expanded'); }); | ||
79 | // object.trigger('expand'); | ||
80 | // | ||
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 | }, | ||
92 | |||
93 | // Bind an event to only be triggered a single time. After the first time | ||
94 | // the callback is invoked, it will be removed. | ||
95 | once: function(name, callback, context) { | ||
96 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; | ||
97 | var self = this; | ||
98 | var once = _.once(function() { | ||
99 | self.off(name, once); | ||
100 | callback.apply(this, arguments); | ||
101 | }); | ||
102 | once._callback = callback; | ||
103 | return this.on(name, once, context); | ||
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; | ||
116 | } | ||
117 | names = name ? [name] : _.keys(this._events); | ||
118 | for (i = 0, l = names.length; i < l; i++) { | ||
119 | name = names[i]; | ||
120 | if (events = this._events[name]) { | ||
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 | } | ||
133 | } | ||
134 | |||
135 | return this; | ||
136 | }, | ||
137 | |||
138 | // Trigger one or many events, firing all bound callbacks. Callbacks are | ||
139 | // passed the same arguments as `trigger` is, apart from the event name | ||
140 | // (unless you're listening on `"all"`, which will cause your callback to | ||
141 | // receive the true name of the event as the first argument). | ||
142 | trigger: function(name) { | ||
143 | if (!this._events) return this; | ||
144 | var args = slice.call(arguments, 1); | ||
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 | }, | ||
152 | |||
153 | // Tell this object to stop listening to either specific events ... or | ||
154 | // to every object it's currently listening to. | ||
155 | stopListening: function(obj, name, callback) { | ||
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; | ||
167 | } | ||
168 | |||
169 | }; | ||
170 | |||
171 | // Regular expression used to split event strings. | ||
172 | var eventSplitter = /\s+/; | ||
173 | |||
174 | // Implement fancy features of the Events API such as multiple event | ||
175 | // names `"change blur"` and jQuery-style event maps `{change: action}` | ||
176 | // in terms of the existing API. | ||
177 | var eventsApi = function(obj, action, name, rest) { | ||
178 | if (!name) return true; | ||
179 | |||
180 | // Handle event maps. | ||
181 | if (typeof name === 'object') { | ||
182 | for (var key in name) { | ||
183 | obj[action].apply(obj, [key, name[key]].concat(rest)); | ||
184 | } | ||
185 | return false; | ||
186 | } | ||
187 | |||
188 | // Handle space separated event names. | ||
189 | if (eventSplitter.test(name)) { | ||
190 | var names = name.split(eventSplitter); | ||
191 | for (var i = 0, l = names.length; i < l; i++) { | ||
192 | obj[action].apply(obj, [names[i]].concat(rest)); | ||
193 | } | ||
194 | return false; | ||
195 | } | ||
196 | |||
197 | return true; | ||
198 | }; | ||
199 | |||
200 | // A difficult-to-believe, but optimized internal dispatch function for | ||
201 | // triggering events. Tries to keep the usual cases speedy (most internal | ||
202 | // Backbone events have 3 arguments). | ||
203 | var triggerEvents = function(events, args) { | ||
204 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; | ||
205 | switch (args.length) { | ||
206 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; | ||
207 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; | ||
208 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; | ||
209 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; | ||
210 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; | ||
211 | } | ||
212 | }; | ||
213 | |||
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 | // Aliases for backwards compatibility. | ||
231 | Events.bind = Events.on; | ||
232 | Events.unbind = Events.off; | ||
233 | |||
234 | // Allow the `Backbone` object to serve as a global event bus, for folks who | ||
235 | // want global "pubsub" in a convenient place. | ||
236 | _.extend(Backbone, Events); | ||
237 | |||
238 | // Backbone.Model | ||
239 | // -------------- | ||
240 | |||
241 | // Backbone **Models** are the basic data object in the framework -- | ||
242 | // frequently representing a row in a table in a database on your server. | ||
243 | // A discrete chunk of data and a bunch of useful, related methods for | ||
244 | // performing computations and transformations on that data. | ||
245 | |||
246 | // Create a new model with the specified attributes. A client id (`cid`) | ||
247 | // is automatically generated and assigned for you. | ||
248 | var Model = Backbone.Model = function(attributes, options) { | ||
249 | var attrs = attributes || {}; | ||
250 | options || (options = {}); | ||
251 | this.cid = _.uniqueId('c'); | ||
252 | this.attributes = {}; | ||
253 | if (options.collection) this.collection = options.collection; | ||
254 | if (options.parse) attrs = this.parse(attrs, options) || {}; | ||
255 | attrs = _.defaults({}, attrs, _.result(this, 'defaults')); | ||
256 | this.set(attrs, options); | ||
257 | this.changed = {}; | ||
258 | this.initialize.apply(this, arguments); | ||
259 | }; | ||
260 | |||
261 | // Attach all inheritable methods to the Model prototype. | ||
262 | _.extend(Model.prototype, Events, { | ||
263 | |||
264 | // A hash of attributes whose current and previous value differ. | ||
265 | changed: null, | ||
266 | |||
267 | // The value returned during the last failed validation. | ||
268 | validationError: null, | ||
269 | |||
270 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and | ||
271 | // CouchDB users may want to set this to `"_id"`. | ||
272 | idAttribute: 'id', | ||
273 | |||
274 | // Initialize is an empty function by default. Override it with your own | ||
275 | // initialization logic. | ||
276 | initialize: function(){}, | ||
277 | |||
278 | // Return a copy of the model's `attributes` object. | ||
279 | toJSON: function(options) { | ||
280 | return _.clone(this.attributes); | ||
281 | }, | ||
282 | |||
283 | // Proxy `Backbone.sync` by default -- but override this if you need | ||
284 | // custom syncing semantics for *this* particular model. | ||
285 | sync: function() { | ||
286 | return Backbone.sync.apply(this, arguments); | ||
287 | }, | ||
288 | |||
289 | // Get the value of an attribute. | ||
290 | get: function(attr) { | ||
291 | return this.attributes[attr]; | ||
292 | }, | ||
293 | |||
294 | // Get the HTML-escaped value of an attribute. | ||
295 | escape: function(attr) { | ||
296 | return _.escape(this.get(attr)); | ||
297 | }, | ||
298 | |||
299 | // Returns `true` if the attribute contains a value that is not null | ||
300 | // or undefined. | ||
301 | has: function(attr) { | ||
302 | return this.get(attr) != null; | ||
303 | }, | ||
304 | |||
305 | // Set a hash of model attributes on the object, firing `"change"`. This is | ||
306 | // the core primitive operation of a model, updating the data and notifying | ||
307 | // anyone who needs to know about the change in state. The heart of the beast. | ||
308 | set: function(key, val, options) { | ||
309 | var attr, attrs, unset, changes, silent, changing, prev, current; | ||
310 | if (key == null) return this; | ||
311 | |||
312 | // Handle both `"key", value` and `{key: value}` -style arguments. | ||
313 | if (typeof key === 'object') { | ||
314 | attrs = key; | ||
315 | options = val; | ||
316 | } else { | ||
317 | (attrs = {})[key] = val; | ||
318 | } | ||
319 | |||
320 | options || (options = {}); | ||
321 | |||
322 | // Run validation. | ||
323 | if (!this._validate(attrs, options)) return false; | ||
324 | |||
325 | // Extract attributes and options. | ||
326 | unset = options.unset; | ||
327 | silent = options.silent; | ||
328 | changes = []; | ||
329 | changing = this._changing; | ||
330 | this._changing = true; | ||
331 | |||
332 | if (!changing) { | ||
333 | this._previousAttributes = _.clone(this.attributes); | ||
334 | this.changed = {}; | ||
335 | } | ||
336 | current = this.attributes, prev = this._previousAttributes; | ||
337 | |||
338 | // Check for changes of `id`. | ||
339 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; | ||
340 | |||
341 | // For each `set` attribute, update or delete the current value. | ||
342 | for (attr in attrs) { | ||
343 | val = attrs[attr]; | ||
344 | if (!_.isEqual(current[attr], val)) changes.push(attr); | ||
345 | if (!_.isEqual(prev[attr], val)) { | ||
346 | this.changed[attr] = val; | ||
347 | } else { | ||
348 | delete this.changed[attr]; | ||
349 | } | ||
350 | unset ? delete current[attr] : current[attr] = val; | ||
351 | } | ||
352 | |||
353 | // Trigger all relevant attribute changes. | ||
354 | if (!silent) { | ||
355 | if (changes.length) this._pending = options; | ||
356 | for (var i = 0, l = changes.length; i < l; i++) { | ||
357 | this.trigger('change:' + changes[i], this, current[changes[i]], options); | ||
358 | } | ||
359 | } | ||
360 | |||
361 | // You might be wondering why there's a `while` loop here. Changes can | ||
362 | // be recursively nested within `"change"` events. | ||
363 | if (changing) return this; | ||
364 | if (!silent) { | ||
365 | while (this._pending) { | ||
366 | options = this._pending; | ||
367 | this._pending = false; | ||
368 | this.trigger('change', this, options); | ||
369 | } | ||
370 | } | ||
371 | this._pending = false; | ||
372 | this._changing = false; | ||
373 | return this; | ||
374 | }, | ||
375 | |||
376 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop | ||
377 | // if the attribute doesn't exist. | ||
378 | unset: function(attr, options) { | ||
379 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); | ||
380 | }, | ||
381 | |||
382 | // Clear all attributes on the model, firing `"change"`. | ||
383 | clear: function(options) { | ||
384 | var attrs = {}; | ||
385 | for (var key in this.attributes) attrs[key] = void 0; | ||
386 | return this.set(attrs, _.extend({}, options, {unset: true})); | ||
387 | }, | ||
388 | |||
389 | // Determine if the model has changed since the last `"change"` event. | ||
390 | // If you specify an attribute name, determine if that attribute has changed. | ||
391 | hasChanged: function(attr) { | ||
392 | if (attr == null) return !_.isEmpty(this.changed); | ||
393 | return _.has(this.changed, attr); | ||
394 | }, | ||
395 | |||
396 | // Return an object containing all the attributes that have changed, or | ||
397 | // false if there are no changed attributes. Useful for determining what | ||
398 | // parts of a view need to be updated and/or what attributes need to be | ||
399 | // persisted to the server. Unset attributes will be set to undefined. | ||
400 | // You can also pass an attributes object to diff against the model, | ||
401 | // determining if there *would be* a change. | ||
402 | changedAttributes: function(diff) { | ||
403 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; | ||
404 | var val, changed = false; | ||
405 | var old = this._changing ? this._previousAttributes : this.attributes; | ||
406 | for (var attr in diff) { | ||
407 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; | ||
408 | (changed || (changed = {}))[attr] = val; | ||
409 | } | ||
410 | return changed; | ||
411 | }, | ||
412 | |||
413 | // Get the previous value of an attribute, recorded at the time the last | ||
414 | // `"change"` event was fired. | ||
415 | previous: function(attr) { | ||
416 | if (attr == null || !this._previousAttributes) return null; | ||
417 | return this._previousAttributes[attr]; | ||
418 | }, | ||
419 | |||
420 | // Get all of the attributes of the model at the time of the previous | ||
421 | // `"change"` event. | ||
422 | previousAttributes: function() { | ||
423 | return _.clone(this._previousAttributes); | ||
424 | }, | ||
425 | |||
426 | // Fetch the model from the server. If the server's representation of the | ||
427 | // model differs from its current attributes, they will be overridden, | ||
428 | // triggering a `"change"` event. | ||
429 | fetch: function(options) { | ||
430 | options = options ? _.clone(options) : {}; | ||
431 | if (options.parse === void 0) options.parse = true; | ||
432 | var model = this; | ||
433 | var success = options.success; | ||
434 | options.success = function(resp) { | ||
435 | if (!model.set(model.parse(resp, options), options)) return false; | ||
436 | if (success) success(model, resp, options); | ||
437 | model.trigger('sync', model, resp, options); | ||
438 | }; | ||
439 | wrapError(this, options); | ||
440 | return this.sync('read', this, options); | ||
441 | }, | ||
442 | |||
443 | // Set a hash of model attributes, and sync the model to the server. | ||
444 | // If the server returns an attributes hash that differs, the model's | ||
445 | // state will be `set` again. | ||
446 | save: function(key, val, options) { | ||
447 | var attrs, method, xhr, attributes = this.attributes; | ||
448 | |||
449 | // Handle both `"key", value` and `{key: value}` -style arguments. | ||
450 | if (key == null || typeof key === 'object') { | ||
451 | attrs = key; | ||
452 | options = val; | ||
453 | } else { | ||
454 | (attrs = {})[key] = val; | ||
455 | } | ||
456 | |||
457 | options = _.extend({validate: true}, options); | ||
458 | |||
459 | // If we're not waiting and attributes exist, save acts as | ||
460 | // `set(attr).save(null, opts)` with validation. Otherwise, check if | ||
461 | // the model will be valid when the attributes, if any, are set. | ||
462 | if (attrs && !options.wait) { | ||
463 | if (!this.set(attrs, options)) return false; | ||
464 | } else { | ||
465 | if (!this._validate(attrs, options)) return false; | ||
466 | } | ||
467 | |||
468 | // Set temporary attributes if `{wait: true}`. | ||
469 | if (attrs && options.wait) { | ||
470 | this.attributes = _.extend({}, attributes, attrs); | ||
471 | } | ||
472 | |||
473 | // After a successful server-side save, the client is (optionally) | ||
474 | // updated with the server-side state. | ||
475 | if (options.parse === void 0) options.parse = true; | ||
476 | var model = this; | ||
477 | var success = options.success; | ||
478 | options.success = function(resp) { | ||
479 | // Ensure attributes are restored during synchronous saves. | ||
480 | model.attributes = attributes; | ||
481 | var serverAttrs = model.parse(resp, options); | ||
482 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); | ||
483 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { | ||
484 | return false; | ||
485 | } | ||
486 | if (success) success(model, resp, options); | ||
487 | model.trigger('sync', model, resp, options); | ||
488 | }; | ||
489 | wrapError(this, options); | ||
490 | |||
491 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); | ||
492 | if (method === 'patch') options.attrs = attrs; | ||
493 | xhr = this.sync(method, this, options); | ||
494 | |||
495 | // Restore attributes. | ||
496 | if (attrs && options.wait) this.attributes = attributes; | ||
497 | |||
498 | return xhr; | ||
499 | }, | ||
500 | |||
501 | // Destroy this model on the server if it was already persisted. | ||
502 | // Optimistically removes the model from its collection, if it has one. | ||
503 | // If `wait: true` is passed, waits for the server to respond before removal. | ||
504 | destroy: function(options) { | ||
505 | options = options ? _.clone(options) : {}; | ||
506 | var model = this; | ||
507 | var success = options.success; | ||
508 | |||
509 | var destroy = function() { | ||
510 | model.trigger('destroy', model, model.collection, options); | ||
511 | }; | ||
512 | |||
513 | options.success = function(resp) { | ||
514 | if (options.wait || model.isNew()) destroy(); | ||
515 | if (success) success(model, resp, options); | ||
516 | if (!model.isNew()) model.trigger('sync', model, resp, options); | ||
517 | }; | ||
518 | |||
519 | if (this.isNew()) { | ||
520 | options.success(); | ||
521 | return false; | ||
522 | } | ||
523 | wrapError(this, options); | ||
524 | |||
525 | var xhr = this.sync('delete', this, options); | ||
526 | if (!options.wait) destroy(); | ||
527 | return xhr; | ||
528 | }, | ||
529 | |||
530 | // Default URL for the model's representation on the server -- if you're | ||
531 | // using Backbone's restful methods, override this to change the endpoint | ||
532 | // that will be called. | ||
533 | url: function() { | ||
534 | var base = | ||
535 | _.result(this, 'urlRoot') || | ||
536 | _.result(this.collection, 'url') || | ||
537 | urlError(); | ||
538 | if (this.isNew()) return base; | ||
539 | return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); | ||
540 | }, | ||
541 | |||
542 | // **parse** converts a response into the hash of attributes to be `set` on | ||
543 | // the model. The default implementation is just to pass the response along. | ||
544 | parse: function(resp, options) { | ||
545 | return resp; | ||
546 | }, | ||
547 | |||
548 | // Create a new model with identical attributes to this one. | ||
549 | clone: function() { | ||
550 | return new this.constructor(this.attributes); | ||
551 | }, | ||
552 | |||
553 | // A model is new if it has never been saved to the server, and lacks an id. | ||
554 | isNew: function() { | ||
555 | return !this.has(this.idAttribute); | ||
556 | }, | ||
557 | |||
558 | // Check if the model is currently in a valid state. | ||
559 | isValid: function(options) { | ||
560 | return this._validate({}, _.extend(options || {}, { validate: true })); | ||
561 | }, | ||
562 | |||
563 | // Run validation against the next complete set of model attributes, | ||
564 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. | ||
565 | _validate: function(attrs, options) { | ||
566 | if (!options.validate || !this.validate) return true; | ||
567 | attrs = _.extend({}, this.attributes, attrs); | ||
568 | var error = this.validationError = this.validate(attrs, options) || null; | ||
569 | if (!error) return true; | ||
570 | this.trigger('invalid', this, error, _.extend(options, {validationError: error})); | ||
571 | return false; | ||
572 | } | ||
573 | |||
574 | }); | ||
575 | |||
576 | // Underscore methods that we want to implement on the Model. | ||
577 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; | ||
578 | |||
579 | // Mix in each Underscore method as a proxy to `Model#attributes`. | ||
580 | _.each(modelMethods, function(method) { | ||
581 | Model.prototype[method] = function() { | ||
582 | var args = slice.call(arguments); | ||
583 | args.unshift(this.attributes); | ||
584 | return _[method].apply(_, args); | ||
585 | }; | ||
586 | }); | ||
587 | |||
588 | // Backbone.Collection | ||
589 | // ------------------- | ||
590 | |||
591 | // If models tend to represent a single row of data, a Backbone Collection is | ||
592 | // more analagous to a table full of data ... or a small slice or page of that | ||
593 | // table, or a collection of rows that belong together for a particular reason | ||
594 | // -- all of the messages in this particular folder, all of the documents | ||
595 | // belonging to this particular author, and so on. Collections maintain | ||
596 | // indexes of their models, both in order, and for lookup by `id`. | ||
597 | |||
598 | // Create a new **Collection**, perhaps to contain a specific type of `model`. | ||
599 | // If a `comparator` is specified, the Collection will maintain | ||
600 | // its models in sort order, as they're added and removed. | ||
601 | var Collection = Backbone.Collection = function(models, options) { | ||
602 | options || (options = {}); | ||
603 | if (options.model) this.model = options.model; | ||
604 | if (options.comparator !== void 0) this.comparator = options.comparator; | ||
605 | this._reset(); | ||
606 | this.initialize.apply(this, arguments); | ||
607 | if (models) this.reset(models, _.extend({silent: true}, options)); | ||
608 | }; | ||
609 | |||
610 | // Default options for `Collection#set`. | ||
611 | var setOptions = {add: true, remove: true, merge: true}; | ||
612 | var addOptions = {add: true, remove: false}; | ||
613 | |||
614 | // Define the Collection's inheritable methods. | ||
615 | _.extend(Collection.prototype, Events, { | ||
616 | |||
617 | // The default model for a collection is just a **Backbone.Model**. | ||
618 | // This should be overridden in most cases. | ||
619 | model: Model, | ||
620 | |||
621 | // Initialize is an empty function by default. Override it with your own | ||
622 | // initialization logic. | ||
623 | initialize: function(){}, | ||
624 | |||
625 | // The JSON representation of a Collection is an array of the | ||
626 | // models' attributes. | ||
627 | toJSON: function(options) { | ||
628 | return this.map(function(model){ return model.toJSON(options); }); | ||
629 | }, | ||
630 | |||
631 | // Proxy `Backbone.sync` by default. | ||
632 | sync: function() { | ||
633 | return Backbone.sync.apply(this, arguments); | ||
634 | }, | ||
635 | |||
636 | // Add a model, or list of models to the set. | ||
637 | add: function(models, options) { | ||
638 | return this.set(models, _.extend({merge: false}, options, addOptions)); | ||
639 | }, | ||
640 | |||
641 | // Remove a model, or a list of models from the set. | ||
642 | remove: function(models, options) { | ||
643 | var singular = !_.isArray(models); | ||
644 | models = singular ? [models] : _.clone(models); | ||
645 | options || (options = {}); | ||
646 | var i, l, index, model; | ||
647 | for (i = 0, l = models.length; i < l; i++) { | ||
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; | ||
662 | }, | ||
663 | |||
664 | // Update a collection by `set`-ing a new list of models, adding new ones, | ||
665 | // removing models that are no longer present, and merging models that | ||
666 | // already exist in the collection, as necessary. Similar to **Model#set**, | ||
667 | // the core operation for updating the data contained by the collection. | ||
668 | set: function(models, options) { | ||
669 | options = _.defaults({}, options, setOptions); | ||
670 | if (options.parse) models = this.parse(models, options); | ||
671 | var singular = !_.isArray(models); | ||
672 | models = singular ? (models ? [models] : []) : _.clone(models); | ||
673 | var i, l, id, model, attrs, existing, sort; | ||
674 | var at = options.at; | ||
675 | var targetModel = this.model; | ||
676 | var sortable = this.comparator && (at == null) && options.sort !== false; | ||
677 | 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 | |||
682 | // Turn bare objects into model references, and prevent invalid models | ||
683 | // from being added. | ||
684 | for (i = 0, l = models.length; i < l; i++) { | ||
685 | attrs = models[i] || {}; | ||
686 | if (attrs instanceof Model) { | ||
687 | id = model = attrs; | ||
688 | } else { | ||
689 | id = attrs[targetModel.prototype.idAttribute || 'id']; | ||
690 | } | ||
691 | |||
692 | // If a duplicate is found, prevent it from being added and | ||
693 | // optionally merge it into the existing model. | ||
694 | if (existing = this.get(id)) { | ||
695 | if (remove) modelMap[existing.cid] = true; | ||
696 | if (merge) { | ||
697 | attrs = attrs === model ? model.attributes : attrs; | ||
698 | if (options.parse) attrs = existing.parse(attrs, options); | ||
699 | existing.set(attrs, options); | ||
700 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; | ||
701 | } | ||
702 | models[i] = existing; | ||
703 | |||
704 | // If this is a new, valid model, push it to the `toAdd` list. | ||
705 | } else if (add) { | ||
706 | model = models[i] = this._prepareModel(attrs, options); | ||
707 | if (!model) continue; | ||
708 | toAdd.push(model); | ||
709 | this._addReference(model, options); | ||
710 | } | ||
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 | } | ||
717 | |||
718 | // Remove nonexistent models if appropriate. | ||
719 | if (remove) { | ||
720 | for (i = 0, l = this.length; i < l; ++i) { | ||
721 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); | ||
722 | } | ||
723 | if (toRemove.length) this.remove(toRemove, options); | ||
724 | } | ||
725 | |||
726 | // See if sorting is needed, update `length` and splice in new models. | ||
727 | if (toAdd.length || (order && order.length)) { | ||
728 | if (sortable) sort = true; | ||
729 | this.length += toAdd.length; | ||
730 | if (at != null) { | ||
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 | } | ||
741 | } | ||
742 | |||
743 | // Silently sort the collection if appropriate. | ||
744 | if (sort) this.sort({silent: true}); | ||
745 | |||
746 | // Unless silenced, it's time to fire all appropriate add/sort events. | ||
747 | if (!options.silent) { | ||
748 | for (i = 0, l = toAdd.length; i < l; i++) { | ||
749 | (model = toAdd[i]).trigger('add', model, this, options); | ||
750 | } | ||
751 | if (sort || (order && order.length)) this.trigger('sort', this, options); | ||
752 | } | ||
753 | |||
754 | // Return the added (or merged) model (or models). | ||
755 | return singular ? models[0] : models; | ||
756 | }, | ||
757 | |||
758 | // When you have more items than you want to add or remove individually, | ||
759 | // you can reset the entire set with a new list of models, without firing | ||
760 | // any granular `add` or `remove` events. Fires `reset` when finished. | ||
761 | // Useful for bulk operations and optimizations. | ||
762 | reset: function(models, options) { | ||
763 | options || (options = {}); | ||
764 | for (var i = 0, l = this.models.length; i < l; i++) { | ||
765 | this._removeReference(this.models[i], options); | ||
766 | } | ||
767 | options.previousModels = this.models; | ||
768 | this._reset(); | ||
769 | models = this.add(models, _.extend({silent: true}, options)); | ||
770 | if (!options.silent) this.trigger('reset', this, options); | ||
771 | return models; | ||
772 | }, | ||
773 | |||
774 | // Add a model to the end of the collection. | ||
775 | push: function(model, options) { | ||
776 | return this.add(model, _.extend({at: this.length}, options)); | ||
777 | }, | ||
778 | |||
779 | // Remove a model from the end of the collection. | ||
780 | pop: function(options) { | ||
781 | var model = this.at(this.length - 1); | ||
782 | this.remove(model, options); | ||
783 | return model; | ||
784 | }, | ||
785 | |||
786 | // Add a model to the beginning of the collection. | ||
787 | unshift: function(model, options) { | ||
788 | return this.add(model, _.extend({at: 0}, options)); | ||
789 | }, | ||
790 | |||
791 | // Remove a model from the beginning of the collection. | ||
792 | shift: function(options) { | ||
793 | var model = this.at(0); | ||
794 | this.remove(model, options); | ||
795 | return model; | ||
796 | }, | ||
797 | |||
798 | // Slice out a sub-array of models from the collection. | ||
799 | slice: function() { | ||
800 | return slice.apply(this.models, arguments); | ||
801 | }, | ||
802 | |||
803 | // Get a model from the set by id. | ||
804 | get: function(obj) { | ||
805 | if (obj == null) return void 0; | ||
806 | return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; | ||
807 | }, | ||
808 | |||
809 | // Get the model at the given index. | ||
810 | at: function(index) { | ||
811 | return this.models[index]; | ||
812 | }, | ||
813 | |||
814 | // Return models with matching attributes. Useful for simple cases of | ||
815 | // `filter`. | ||
816 | where: function(attrs, first) { | ||
817 | if (_.isEmpty(attrs)) return first ? void 0 : []; | ||
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 | }); | ||
824 | }, | ||
825 | |||
826 | // Return the first model with matching attributes. Useful for simple cases | ||
827 | // of `find`. | ||
828 | findWhere: function(attrs) { | ||
829 | return this.where(attrs, true); | ||
830 | }, | ||
831 | |||
832 | // Force the collection to re-sort itself. You don't need to call this under | ||
833 | // normal circumstances, as the set will maintain sort order as each item | ||
834 | // is added. | ||
835 | sort: function(options) { | ||
836 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); | ||
837 | options || (options = {}); | ||
838 | |||
839 | // Run sort based on type of `comparator`. | ||
840 | if (_.isString(this.comparator) || this.comparator.length === 1) { | ||
841 | this.models = this.sortBy(this.comparator, this); | ||
842 | } else { | ||
843 | this.models.sort(_.bind(this.comparator, this)); | ||
844 | } | ||
845 | |||
846 | if (!options.silent) this.trigger('sort', this, options); | ||
847 | return this; | ||
848 | }, | ||
849 | |||
850 | // Pluck an attribute from each model in the collection. | ||
851 | pluck: function(attr) { | ||
852 | return _.invoke(this.models, 'get', attr); | ||
853 | }, | ||
854 | |||
855 | // Fetch the default set of models for this collection, resetting the | ||
856 | // collection when they arrive. If `reset: true` is passed, the response | ||
857 | // data will be passed through the `reset` method instead of `set`. | ||
858 | fetch: function(options) { | ||
859 | options = options ? _.clone(options) : {}; | ||
860 | if (options.parse === void 0) options.parse = true; | ||
861 | var success = options.success; | ||
862 | var collection = this; | ||
863 | options.success = function(resp) { | ||
864 | var method = options.reset ? 'reset' : 'set'; | ||
865 | collection[method](resp, options); | ||
866 | if (success) success(collection, resp, options); | ||
867 | collection.trigger('sync', collection, resp, options); | ||
868 | }; | ||
869 | wrapError(this, options); | ||
870 | return this.sync('read', this, options); | ||
871 | }, | ||
872 | |||
873 | // Create a new instance of a model in this collection. Add the model to the | ||
874 | // collection immediately, unless `wait: true` is passed, in which case we | ||
875 | // wait for the server to agree. | ||
876 | create: function(model, options) { | ||
877 | options = options ? _.clone(options) : {}; | ||
878 | if (!(model = this._prepareModel(model, options))) return false; | ||
879 | if (!options.wait) this.add(model, options); | ||
880 | var collection = this; | ||
881 | var success = options.success; | ||
882 | options.success = function(model, resp) { | ||
883 | if (options.wait) collection.add(model, options); | ||
884 | if (success) success(model, resp, options); | ||
885 | }; | ||
886 | model.save(null, options); | ||
887 | return model; | ||
888 | }, | ||
889 | |||
890 | // **parse** converts a response into a list of models to be added to the | ||
891 | // collection. The default implementation is just to pass it through. | ||
892 | parse: function(resp, options) { | ||
893 | return resp; | ||
894 | }, | ||
895 | |||
896 | // Create a new collection with an identical list of models as this one. | ||
897 | clone: function() { | ||
898 | return new this.constructor(this.models); | ||
899 | }, | ||
900 | |||
901 | // Private method to reset all internal state. Called when the collection | ||
902 | // is first initialized or reset. | ||
903 | _reset: function() { | ||
904 | this.length = 0; | ||
905 | this.models = []; | ||
906 | this._byId = {}; | ||
907 | }, | ||
908 | |||
909 | // Prepare a hash of attributes (or other model) to be added to this | ||
910 | // collection. | ||
911 | _prepareModel: function(attrs, options) { | ||
912 | if (attrs instanceof Model) return attrs; | ||
913 | options = options ? _.clone(options) : {}; | ||
914 | options.collection = this; | ||
915 | var model = new this.model(attrs, options); | ||
916 | if (!model.validationError) return model; | ||
917 | this.trigger('invalid', this, model.validationError, options); | ||
918 | return false; | ||
919 | }, | ||
920 | |||
921 | // Internal method to create a model's ties to a collection. | ||
922 | _addReference: function(model, options) { | ||
923 | this._byId[model.cid] = model; | ||
924 | if (model.id != null) this._byId[model.id] = model; | ||
925 | if (!model.collection) model.collection = this; | ||
926 | model.on('all', this._onModelEvent, this); | ||
927 | }, | ||
928 | |||
929 | // Internal method to sever a model's ties to a collection. | ||
930 | _removeReference: function(model, options) { | ||
931 | if (this === model.collection) delete model.collection; | ||
932 | model.off('all', this._onModelEvent, this); | ||
933 | }, | ||
934 | |||
935 | // Internal method called every time a model in the set fires an event. | ||
936 | // Sets need to update their indexes when models change ids. All other | ||
937 | // events simply proxy through. "add" and "remove" events that originate | ||
938 | // in other collections are ignored. | ||
939 | _onModelEvent: function(event, model, collection, options) { | ||
940 | if ((event === 'add' || event === 'remove') && collection !== this) return; | ||
941 | if (event === 'destroy') this.remove(model, options); | ||
942 | if (model && event === 'change:' + model.idAttribute) { | ||
943 | delete this._byId[model.previous(model.idAttribute)]; | ||
944 | if (model.id != null) this._byId[model.id] = model; | ||
945 | } | ||
946 | this.trigger.apply(this, arguments); | ||
947 | } | ||
948 | |||
949 | }); | ||
950 | |||
951 | // Underscore methods that we want to implement on the Collection. | ||
952 | // 90% of the core usefulness of Backbone Collections is actually implemented | ||
953 | // right here: | ||
954 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', | ||
955 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', | ||
956 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', | ||
957 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', | ||
958 | 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', | ||
959 | 'lastIndexOf', 'isEmpty', 'chain', 'sample']; | ||
960 | |||
961 | // Mix in each Underscore method as a proxy to `Collection#models`. | ||
962 | _.each(methods, function(method) { | ||
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 | }); | ||
982 | |||
983 | // Backbone.View | ||
984 | // ------------- | ||
985 | |||
986 | // Backbone Views are almost more convention than they are actual code. A View | ||
987 | // is simply a JavaScript object that represents a logical chunk of UI in the | ||
988 | // DOM. This might be a single item, an entire list, a sidebar or panel, or | ||
989 | // even the surrounding frame which wraps your whole app. Defining a chunk of | ||
990 | // UI as a **View** allows you to define your DOM events declaratively, without | ||
991 | // having to worry about render order ... and makes it easy for the view to | ||
992 | // react to specific changes in the state of your models. | ||
993 | |||
994 | // Creating a Backbone.View creates its initial element outside of the DOM, | ||
995 | // if an existing element is not provided... | ||
996 | var View = Backbone.View = function(options) { | ||
997 | this.cid = _.uniqueId('view'); | ||
998 | options || (options = {}); | ||
999 | _.extend(this, _.pick(options, viewOptions)); | ||
1000 | this._ensureElement(); | ||
1001 | this.initialize.apply(this, arguments); | ||
1002 | this.delegateEvents(); | ||
1003 | }; | ||
1004 | |||
1005 | // Cached regex to split keys for `delegate`. | ||
1006 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; | ||
1007 | |||
1008 | // List of view options to be merged as properties. | ||
1009 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; | ||
1010 | |||
1011 | // Set up all inheritable **Backbone.View** properties and methods. | ||
1012 | _.extend(View.prototype, Events, { | ||
1013 | |||
1014 | // The default `tagName` of a View's element is `"div"`. | ||
1015 | tagName: 'div', | ||
1016 | |||
1017 | // jQuery delegate for element lookup, scoped to DOM elements within the | ||
1018 | // current view. This should be preferred to global lookups where possible. | ||
1019 | $: function(selector) { | ||
1020 | return this.$el.find(selector); | ||
1021 | }, | ||
1022 | |||
1023 | // Initialize is an empty function by default. Override it with your own | ||
1024 | // initialization logic. | ||
1025 | initialize: function(){}, | ||
1026 | |||
1027 | // **render** is the core function that your view should override, in order | ||
1028 | // to populate its element (`this.el`), with the appropriate HTML. The | ||
1029 | // convention is for **render** to always return `this`. | ||
1030 | render: function() { | ||
1031 | return this; | ||
1032 | }, | ||
1033 | |||
1034 | // Remove this view by taking the element out of the DOM, and removing any | ||
1035 | // applicable Backbone.Events listeners. | ||
1036 | remove: function() { | ||
1037 | this.$el.remove(); | ||
1038 | this.stopListening(); | ||
1039 | return this; | ||
1040 | }, | ||
1041 | |||
1042 | // Change the view's element (`this.el` property), including event | ||
1043 | // re-delegation. | ||
1044 | setElement: function(element, delegate) { | ||
1045 | if (this.$el) this.undelegateEvents(); | ||
1046 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); | ||
1047 | this.el = this.$el[0]; | ||
1048 | if (delegate !== false) this.delegateEvents(); | ||
1049 | return this; | ||
1050 | }, | ||
1051 | |||
1052 | // Set callbacks, where `this.events` is a hash of | ||
1053 | // | ||
1054 | // *{"event selector": "callback"}* | ||
1055 | // | ||
1056 | // { | ||
1057 | // 'mousedown .title': 'edit', | ||
1058 | // 'click .button': 'save', | ||
1059 | // 'click .open': function(e) { ... } | ||
1060 | // } | ||
1061 | // | ||
1062 | // pairs. Callbacks will be bound to the view, with `this` set properly. | ||
1063 | // Uses event delegation for efficiency. | ||
1064 | // 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 | delegateEvents: function(events) { | ||
1068 | if (!(events || (events = _.result(this, 'events')))) return this; | ||
1069 | this.undelegateEvents(); | ||
1070 | for (var key in events) { | ||
1071 | var method = events[key]; | ||
1072 | if (!_.isFunction(method)) method = this[events[key]]; | ||
1073 | if (!method) continue; | ||
1074 | |||
1075 | var match = key.match(delegateEventSplitter); | ||
1076 | var eventName = match[1], selector = match[2]; | ||
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 | } | ||
1084 | } | ||
1085 | return this; | ||
1086 | }, | ||
1087 | |||
1088 | // Clears all callbacks previously bound to the view with `delegateEvents`. | ||
1089 | // You usually don't need to use this, but may wish to if you have multiple | ||
1090 | // Backbone views attached to the same DOM element. | ||
1091 | undelegateEvents: function() { | ||
1092 | this.$el.off('.delegateEvents' + this.cid); | ||
1093 | return this; | ||
1094 | }, | ||
1095 | |||
1096 | // Ensure that the View has a DOM element to render into. | ||
1097 | // If `this.el` is a string, pass it through `$()`, take the first | ||
1098 | // matching element, and re-assign it to `el`. Otherwise, create | ||
1099 | // an element from the `id`, `className` and `tagName` properties. | ||
1100 | _ensureElement: function() { | ||
1101 | if (!this.el) { | ||
1102 | var attrs = _.extend({}, _.result(this, 'attributes')); | ||
1103 | if (this.id) attrs.id = _.result(this, 'id'); | ||
1104 | if (this.className) attrs['class'] = _.result(this, 'className'); | ||
1105 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); | ||
1106 | this.setElement($el, false); | ||
1107 | } else { | ||
1108 | this.setElement(_.result(this, 'el'), false); | ||
1109 | } | ||
1110 | } | ||
1111 | |||
1112 | }); | ||
1113 | |||
1114 | // Backbone.sync | ||
1115 | // ------------- | ||
1116 | |||
1117 | // Override this function to change the manner in which Backbone persists | ||
1118 | // models to the server. You will be passed the type of request, and the | ||
1119 | // model in question. By default, makes a RESTful Ajax request | ||
1120 | // to the model's `url()`. Some possible customizations could be: | ||
1121 | // | ||
1122 | // * Use `setTimeout` to batch rapid-fire updates into a single request. | ||
1123 | // * Send up the models as XML instead of JSON. | ||
1124 | // * Persist models via WebSockets instead of Ajax. | ||
1125 | // | ||
1126 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests | ||
1127 | // as `POST`, with a `_method` parameter containing the true HTTP method, | ||
1128 | // as well as all requests with the body as `application/x-www-form-urlencoded` | ||
1129 | // instead of `application/json` with the model in a param named `model`. | ||
1130 | // Useful when interfacing with server-side languages like **PHP** that make | ||
1131 | // it difficult to read the body of `PUT` requests. | ||
1132 | Backbone.sync = function(method, model, options) { | ||
1133 | var type = methodMap[method]; | ||
1134 | |||
1135 | // Default options, unless specified. | ||
1136 | _.defaults(options || (options = {}), { | ||
1137 | emulateHTTP: Backbone.emulateHTTP, | ||
1138 | emulateJSON: Backbone.emulateJSON | ||
1139 | }); | ||
1140 | |||
1141 | // Default JSON-request options. | ||
1142 | var params = {type: type, dataType: 'json'}; | ||
1143 | |||
1144 | // Ensure that we have a URL. | ||
1145 | if (!options.url) { | ||
1146 | params.url = _.result(model, 'url') || urlError(); | ||
1147 | } | ||
1148 | |||
1149 | // Ensure that we have the appropriate request data. | ||
1150 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { | ||
1151 | params.contentType = 'application/json'; | ||
1152 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); | ||
1153 | } | ||
1154 | |||
1155 | // For older servers, emulate JSON by encoding the request into an HTML-form. | ||
1156 | if (options.emulateJSON) { | ||
1157 | params.contentType = 'application/x-www-form-urlencoded'; | ||
1158 | params.data = params.data ? {model: params.data} : {}; | ||
1159 | } | ||
1160 | |||
1161 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` | ||
1162 | // And an `X-HTTP-Method-Override` header. | ||
1163 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { | ||
1164 | params.type = 'POST'; | ||
1165 | if (options.emulateJSON) params.data._method = type; | ||
1166 | var beforeSend = options.beforeSend; | ||
1167 | options.beforeSend = function(xhr) { | ||
1168 | xhr.setRequestHeader('X-HTTP-Method-Override', type); | ||
1169 | if (beforeSend) return beforeSend.apply(this, arguments); | ||
1170 | }; | ||
1171 | } | ||
1172 | |||
1173 | // Don't process data on a non-GET request. | ||
1174 | if (params.type !== 'GET' && !options.emulateJSON) { | ||
1175 | params.processData = false; | ||
1176 | } | ||
1177 | |||
1178 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer | ||
1179 | // that still has ActiveX enabled by default, override jQuery to use that | ||
1180 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. | ||
1181 | if (params.type === 'PATCH' && noXhrPatch) { | ||
1182 | params.xhr = function() { | ||
1183 | return new ActiveXObject("Microsoft.XMLHTTP"); | ||
1184 | }; | ||
1185 | } | ||
1186 | |||
1187 | // Make the request, allowing the user to override any Ajax options. | ||
1188 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); | ||
1189 | model.trigger('request', model, xhr, options); | ||
1190 | return xhr; | ||
1191 | }; | ||
1192 | |||
1193 | var noXhrPatch = | ||
1194 | typeof window !== 'undefined' && !!window.ActiveXObject && | ||
1195 | !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); | ||
1196 | |||
1197 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. | ||
1198 | var methodMap = { | ||
1199 | 'create': 'POST', | ||
1200 | 'update': 'PUT', | ||
1201 | 'patch': 'PATCH', | ||
1202 | 'delete': 'DELETE', | ||
1203 | 'read': 'GET' | ||
1204 | }; | ||
1205 | |||
1206 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. | ||
1207 | // Override this if you'd like to use a different library. | ||
1208 | Backbone.ajax = function() { | ||
1209 | return Backbone.$.ajax.apply(Backbone.$, arguments); | ||
1210 | }; | ||
1211 | |||
1212 | // Backbone.Router | ||
1213 | // --------------- | ||
1214 | |||
1215 | // Routers map faux-URLs to actions, and fire events when routes are | ||
1216 | // matched. Creating a new one sets its `routes` hash, if not set statically. | ||
1217 | var Router = Backbone.Router = function(options) { | ||
1218 | options || (options = {}); | ||
1219 | if (options.routes) this.routes = options.routes; | ||
1220 | this._bindRoutes(); | ||
1221 | this.initialize.apply(this, arguments); | ||
1222 | }; | ||
1223 | |||
1224 | // Cached regular expressions for matching named param parts and splatted | ||
1225 | // parts of route strings. | ||
1226 | var optionalParam = /\((.*?)\)/g; | ||
1227 | var namedParam = /(\(\?)?:\w+/g; | ||
1228 | var splatParam = /\*\w+/g; | ||
1229 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; | ||
1230 | |||
1231 | // Set up all inheritable **Backbone.Router** properties and methods. | ||
1232 | _.extend(Router.prototype, Events, { | ||
1233 | |||
1234 | // Initialize is an empty function by default. Override it with your own | ||
1235 | // initialization logic. | ||
1236 | initialize: function(){}, | ||
1237 | |||
1238 | // Manually bind a single named route to a callback. For example: | ||
1239 | // | ||
1240 | // this.route('search/:query/p:num', 'search', function(query, num) { | ||
1241 | // ... | ||
1242 | // }); | ||
1243 | // | ||
1244 | route: function(route, name, callback) { | ||
1245 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); | ||
1246 | if (_.isFunction(name)) { | ||
1247 | callback = name; | ||
1248 | name = ''; | ||
1249 | } | ||
1250 | if (!callback) callback = this[name]; | ||
1251 | var router = this; | ||
1252 | Backbone.history.route(route, function(fragment) { | ||
1253 | var args = router._extractParameters(route, fragment); | ||
1254 | router.execute(callback, args); | ||
1255 | router.trigger.apply(router, ['route:' + name].concat(args)); | ||
1256 | router.trigger('route', name, args); | ||
1257 | Backbone.history.trigger('route', router, name, args); | ||
1258 | }); | ||
1259 | return this; | ||
1260 | }, | ||
1261 | |||
1262 | // Execute a route handler with the provided parameters. This is an | ||
1263 | // excellent place to do pre-route setup or post-route cleanup. | ||
1264 | execute: function(callback, args) { | ||
1265 | if (callback) callback.apply(this, args); | ||
1266 | }, | ||
1267 | |||
1268 | // Simple proxy to `Backbone.history` to save a fragment into the history. | ||
1269 | navigate: function(fragment, options) { | ||
1270 | Backbone.history.navigate(fragment, options); | ||
1271 | return this; | ||
1272 | }, | ||
1273 | |||
1274 | // Bind all defined routes to `Backbone.history`. We have to reverse the | ||
1275 | // order of the routes here to support behavior where the most general | ||
1276 | // routes can be defined at the bottom of the route map. | ||
1277 | _bindRoutes: function() { | ||
1278 | if (!this.routes) return; | ||
1279 | this.routes = _.result(this, 'routes'); | ||
1280 | var route, routes = _.keys(this.routes); | ||
1281 | while ((route = routes.pop()) != null) { | ||
1282 | this.route(route, this.routes[route]); | ||
1283 | } | ||
1284 | }, | ||
1285 | |||
1286 | // Convert a route string into a regular expression, suitable for matching | ||
1287 | // against the current location hash. | ||
1288 | _routeToRegExp: function(route) { | ||
1289 | route = route.replace(escapeRegExp, '\\$&') | ||
1290 | .replace(optionalParam, '(?:$1)?') | ||
1291 | .replace(namedParam, function(match, optional) { | ||
1292 | return optional ? match : '([^/?]+)'; | ||
1293 | }) | ||
1294 | .replace(splatParam, '([^?]*?)'); | ||
1295 | return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); | ||
1296 | }, | ||
1297 | |||
1298 | // Given a route, and a URL fragment that it matches, return the array of | ||
1299 | // extracted decoded parameters. Empty or unmatched parameters will be | ||
1300 | // treated as `null` to normalize cross-browser behavior. | ||
1301 | _extractParameters: function(route, fragment) { | ||
1302 | var params = route.exec(fragment).slice(1); | ||
1303 | return _.map(params, function(param, i) { | ||
1304 | // Don't decode the search params. | ||
1305 | if (i === params.length - 1) return param || null; | ||
1306 | return param ? decodeURIComponent(param) : null; | ||
1307 | }); | ||
1308 | } | ||
1309 | |||
1310 | }); | ||
1311 | |||
1312 | // Backbone.History | ||
1313 | // ---------------- | ||
1314 | |||
1315 | // Handles cross-browser history management, based on either | ||
1316 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or | ||
1317 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) | ||
1318 | // and URL fragments. If the browser supports neither (old IE, natch), | ||
1319 | // falls back to polling. | ||
1320 | var History = Backbone.History = function() { | ||
1321 | this.handlers = []; | ||
1322 | _.bindAll(this, 'checkUrl'); | ||
1323 | |||
1324 | // Ensure that `History` can be used outside of the browser. | ||
1325 | if (typeof window !== 'undefined') { | ||
1326 | this.location = window.location; | ||
1327 | this.history = window.history; | ||
1328 | } | ||
1329 | }; | ||
1330 | |||
1331 | // Cached regex for stripping a leading hash/slash and trailing space. | ||
1332 | var routeStripper = /^[#\/]|\s+$/g; | ||
1333 | |||
1334 | // Cached regex for stripping leading and trailing slashes. | ||
1335 | var rootStripper = /^\/+|\/+$/g; | ||
1336 | |||
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 | // Cached regex for stripping urls of hash. | ||
1344 | var pathStripper = /#.*$/; | ||
1345 | |||
1346 | // Has the history handling already been started? | ||
1347 | History.started = false; | ||
1348 | |||
1349 | // Set up all inheritable **Backbone.History** properties and methods. | ||
1350 | _.extend(History.prototype, Events, { | ||
1351 | |||
1352 | // The default interval to poll for hash changes, if necessary, is | ||
1353 | // twenty times a second. | ||
1354 | interval: 50, | ||
1355 | |||
1356 | // Are we at the app root? | ||
1357 | atRoot: function() { | ||
1358 | return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; | ||
1359 | }, | ||
1360 | |||
1361 | // Gets the true hash value. Cannot use location.hash directly due to bug | ||
1362 | // in Firefox where location.hash will always be decoded. | ||
1363 | getHash: function(window) { | ||
1364 | var match = (window || this).location.href.match(/#(.*)$/); | ||
1365 | return match ? match[1] : ''; | ||
1366 | }, | ||
1367 | |||
1368 | // Get the cross-browser normalized URL fragment, either from the URL, | ||
1369 | // the hash, or the override. | ||
1370 | getFragment: function(fragment, forcePushState) { | ||
1371 | if (fragment == null) { | ||
1372 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { | ||
1373 | fragment = decodeURI(this.location.pathname + this.location.search); | ||
1374 | var root = this.root.replace(trailingSlash, ''); | ||
1375 | if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); | ||
1376 | } else { | ||
1377 | fragment = this.getHash(); | ||
1378 | } | ||
1379 | } | ||
1380 | return fragment.replace(routeStripper, ''); | ||
1381 | }, | ||
1382 | |||
1383 | // Start the hash change handling, returning `true` if the current URL matches | ||
1384 | // an existing route, and `false` otherwise. | ||
1385 | start: function(options) { | ||
1386 | if (History.started) throw new Error("Backbone.history has already been started"); | ||
1387 | History.started = true; | ||
1388 | |||
1389 | // Figure out the initial configuration. Do we need an iframe? | ||
1390 | // Is pushState desired ... is it available? | ||
1391 | this.options = _.extend({root: '/'}, this.options, options); | ||
1392 | this.root = this.options.root; | ||
1393 | this._wantsHashChange = this.options.hashChange !== false; | ||
1394 | this._wantsPushState = !!this.options.pushState; | ||
1395 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); | ||
1396 | var fragment = this.getFragment(); | ||
1397 | var docMode = document.documentMode; | ||
1398 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); | ||
1399 | |||
1400 | // Normalize root to always include a leading and trailing slash. | ||
1401 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); | ||
1402 | |||
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 | // Transition from hashChange to pushState or vice versa if both are | ||
1425 | // requested. | ||
1426 | if (this._wantsHashChange && this._wantsPushState) { | ||
1427 | |||
1428 | // If we've started off with a route from a `pushState`-enabled | ||
1429 | // browser, but we're currently in a browser that doesn't support it... | ||
1430 | if (!this._hasPushState && !this.atRoot()) { | ||
1431 | this.fragment = this.getFragment(null, true); | ||
1432 | this.location.replace(this.root + '#' + this.fragment); | ||
1433 | // Return immediately as browser will do redirect to new url | ||
1434 | return true; | ||
1435 | |||
1436 | // Or if we've started out with a hash-based route, but we're currently | ||
1437 | // in a browser where it could be `pushState`-based instead... | ||
1438 | } else if (this._hasPushState && this.atRoot() && loc.hash) { | ||
1439 | this.fragment = this.getHash().replace(routeStripper, ''); | ||
1440 | this.history.replaceState({}, document.title, this.root + this.fragment); | ||
1441 | } | ||
1442 | |||
1443 | } | ||
1444 | |||
1445 | if (!this.options.silent) return this.loadUrl(); | ||
1446 | }, | ||
1447 | |||
1448 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app, | ||
1449 | // but possibly useful for unit testing Routers. | ||
1450 | stop: function() { | ||
1451 | Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); | ||
1452 | if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); | ||
1453 | History.started = false; | ||
1454 | }, | ||
1455 | |||
1456 | // Add a route to be tested when the fragment changes. Routes added later | ||
1457 | // may override previous routes. | ||
1458 | route: function(route, callback) { | ||
1459 | this.handlers.unshift({route: route, callback: callback}); | ||
1460 | }, | ||
1461 | |||
1462 | // Checks the current URL to see if it has changed, and if it has, | ||
1463 | // calls `loadUrl`, normalizing across the hidden iframe. | ||
1464 | checkUrl: function(e) { | ||
1465 | var current = this.getFragment(); | ||
1466 | if (current === this.fragment && this.iframe) { | ||
1467 | current = this.getFragment(this.getHash(this.iframe)); | ||
1468 | } | ||
1469 | if (current === this.fragment) return false; | ||
1470 | if (this.iframe) this.navigate(current); | ||
1471 | this.loadUrl(); | ||
1472 | }, | ||
1473 | |||
1474 | // Attempt to load the current URL fragment. If a route succeeds with a | ||
1475 | // match, returns `true`. If no defined routes matches the fragment, | ||
1476 | // returns `false`. | ||
1477 | loadUrl: function(fragment) { | ||
1478 | fragment = this.fragment = this.getFragment(fragment); | ||
1479 | return _.any(this.handlers, function(handler) { | ||
1480 | if (handler.route.test(fragment)) { | ||
1481 | handler.callback(fragment); | ||
1482 | return true; | ||
1483 | } | ||
1484 | }); | ||
1485 | }, | ||
1486 | |||
1487 | // Save a fragment into the hash history, or replace the URL state if the | ||
1488 | // 'replace' option is passed. You are responsible for properly URL-encoding | ||
1489 | // the fragment in advance. | ||
1490 | // | ||
1491 | // The options object can contain `trigger: true` if you wish to have the | ||
1492 | // route callback be fired (not usually desirable), or `replace: true`, if | ||
1493 | // you wish to modify the current URL without adding an entry to the history. | ||
1494 | navigate: function(fragment, options) { | ||
1495 | if (!History.started) return false; | ||
1496 | if (!options || options === true) options = {trigger: !!options}; | ||
1497 | |||
1498 | var url = this.root + (fragment = this.getFragment(fragment || '')); | ||
1499 | |||
1500 | // Strip the hash for matching. | ||
1501 | fragment = fragment.replace(pathStripper, ''); | ||
1502 | |||
1503 | if (this.fragment === fragment) return; | ||
1504 | this.fragment = fragment; | ||
1505 | |||
1506 | // Don't include a trailing slash on the root. | ||
1507 | if (fragment === '' && url !== '/') url = url.slice(0, -1); | ||
1508 | |||
1509 | // If pushState is available, we use it to set the fragment as a real URL. | ||
1510 | if (this._hasPushState) { | ||
1511 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); | ||
1512 | |||
1513 | // If hash changes haven't been explicitly disabled, update the hash | ||
1514 | // fragment to store history. | ||
1515 | } else if (this._wantsHashChange) { | ||
1516 | this._updateHash(this.location, fragment, options.replace); | ||
1517 | if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { | ||
1518 | // Opening and closing the iframe tricks IE7 and earlier to push a | ||
1519 | // history entry on hash-tag change. When replace is true, we don't | ||
1520 | // want this. | ||
1521 | if(!options.replace) this.iframe.document.open().close(); | ||
1522 | this._updateHash(this.iframe.location, fragment, options.replace); | ||
1523 | } | ||
1524 | |||
1525 | // If you've told us that you explicitly don't want fallback hashchange- | ||
1526 | // based history, then `navigate` becomes a page refresh. | ||
1527 | } else { | ||
1528 | return this.location.assign(url); | ||
1529 | } | ||
1530 | if (options.trigger) return this.loadUrl(fragment); | ||
1531 | }, | ||
1532 | |||
1533 | // Update the hash location, either replacing the current entry, or adding | ||
1534 | // a new one to the browser history. | ||
1535 | _updateHash: function(location, fragment, replace) { | ||
1536 | if (replace) { | ||
1537 | var href = location.href.replace(/(javascript:|#).*$/, ''); | ||
1538 | location.replace(href + '#' + fragment); | ||
1539 | } else { | ||
1540 | // Some browsers require that `hash` contains a leading #. | ||
1541 | location.hash = '#' + fragment; | ||
1542 | } | ||
1543 | } | ||
1544 | |||
1545 | }); | ||
1546 | |||
1547 | // Create the default Backbone.history. | ||
1548 | Backbone.history = new History; | ||
1549 | |||
1550 | // Helpers | ||
1551 | // ------- | ||
1552 | |||
1553 | // Helper function to correctly set up the prototype chain, for subclasses. | ||
1554 | // Similar to `goog.inherits`, but uses a hash of prototype properties and | ||
1555 | // class properties to be extended. | ||
1556 | var extend = function(protoProps, staticProps) { | ||
1557 | var parent = this; | ||
1558 | var child; | ||
1559 | |||
1560 | // The constructor function for the new subclass is either defined by you | ||
1561 | // (the "constructor" property in your `extend` definition), or defaulted | ||
1562 | // by us to simply call the parent's constructor. | ||
1563 | if (protoProps && _.has(protoProps, 'constructor')) { | ||
1564 | child = protoProps.constructor; | ||
1565 | } else { | ||
1566 | child = function(){ return parent.apply(this, arguments); }; | ||
1567 | } | ||
1568 | |||
1569 | // Add static properties to the constructor function, if supplied. | ||
1570 | _.extend(child, parent, staticProps); | ||
1571 | |||
1572 | // Set the prototype chain to inherit from `parent`, without calling | ||
1573 | // `parent`'s constructor function. | ||
1574 | var Surrogate = function(){ this.constructor = child; }; | ||
1575 | Surrogate.prototype = parent.prototype; | ||
1576 | child.prototype = new Surrogate; | ||
1577 | |||
1578 | // Add prototype properties (instance properties) to the subclass, | ||
1579 | // if supplied. | ||
1580 | if (protoProps) _.extend(child.prototype, protoProps); | ||
1581 | |||
1582 | // Set a convenience property in case the parent's prototype is needed | ||
1583 | // later. | ||
1584 | child.__super__ = parent.prototype; | ||
1585 | |||
1586 | return child; | ||
1587 | }; | ||
1588 | |||
1589 | // Set up inheritance for the model, collection, router, view and history. | ||
1590 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; | ||
1591 | |||
1592 | // Throw an error when a URL is needed, and none is supplied. | ||
1593 | var urlError = function() { | ||
1594 | throw new Error('A "url" property or function must be specified'); | ||
1595 | }; | ||
1596 | |||
1597 | // Wrap an optional error callback with a fallback error event. | ||
1598 | var wrapError = function(model, options) { | ||
1599 | var error = options.error; | ||
1600 | options.error = function(resp) { | ||
1601 | if (error) error(model, resp, options); | ||
1602 | model.trigger('error', model, resp, options); | ||
1603 | }; | ||
1604 | }; | ||
1605 | |||
1606 | return Backbone; | ||
1607 | |||
1608 | })); | ||