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