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