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