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