PageRenderTime 50ms CodeModel.GetById 2ms app.highlight 39ms RepoModel.GetById 1ms app.codeStats 0ms

/backbone.js

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