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