| 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 | })); |