From 5ce976bf6a6c795e295190bcc48c39e52f9afe6f Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 30 Jan 2012 16:24:40 -0500 Subject: [PATCH] Backbone.js 0.9.0 --- LICENSE | 2 +- backbone-min.js | 35 ++ backbone.js | 4 +- docs/backbone.html | 817 ++++++++++++++++++++++++--------------------- docs/todos.html | 2 +- index.html | 314 +++++++++++++---- package.json | 4 +- 7 files changed, 727 insertions(+), 451 deletions(-) create mode 100644 backbone-min.js diff --git a/LICENSE b/LICENSE index 85972e15c..f79bb0058 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2011 Jeremy Ashkenas, DocumentCloud +Copyright (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/backbone-min.js b/backbone-min.js new file mode 100644 index 000000000..e2cab5852 --- /dev/null +++ b/backbone-min.js @@ -0,0 +1,35 @@ +// Backbone.js 0.9.0 +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var i=this,r=i.Backbone,s=Array.prototype.slice,t=Array.prototype.splice,g;g=typeof exports!=="undefined"?exports:i.Backbone={};g.VERSION="0.9.0";var f=i._;!f&&typeof require!=="undefined"&&(f=require("underscore"));var h=i.jQuery||i.Zepto||i.ender;g.noConflict=function(){i.Backbone=r;return this};g.emulateHTTP=false;g.emulateJSON=false;g.Events={on:function(a,b,c){for(var d,a=a.split(/\s+/),e=this._callbacks||(this._callbacks={});d=a.shift();){d=e[d]||(e[d]={});var f=d.tail||(d.tail= +d.next={});f.callback=b;f.context=c;d.tail=f.next={}}return this},off:function(a,b,c){var d,e,f;if(a){if(e=this._callbacks)for(a=a.split(/\s+/);d=a.shift();)if(f=e[d],delete e[d],b&&f)for(;(f=f.next)&&f.next;)if(!(f.callback===b&&(!c||f.context===c)))this.on(d,f.callback,f.context)}else delete this._callbacks;return this},trigger:function(a){var b,c,d,e;if(!(d=this._callbacks))return this;e=d.all;for((a=a.split(/\s+/)).push(null);b=a.shift();)e&&a.push({next:e.next,tail:e.tail,event:b}),(c=d[b])&& +a.push({next:c.next,tail:c.tail});for(e=s.call(arguments,1);c=a.pop();){b=c.tail;for(d=c.event?[c.event].concat(e):e;(c=c.next)!==b;)c.callback.apply(c.context||this,d)}return this}};g.Events.bind=g.Events.on;g.Events.unbind=g.Events.off;g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=j(this,"defaults"))a=f.extend({},c,a);if(b&&b.collection)this.collection=b.collection;this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this._changed={};if(!this.set(a,{silent:true}))throw Error("Can't create an invalid model"); +this._changed={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(g.Model.prototype,g.Events,{idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];return this._escapedAttributes[a]=f.escape(b==null?"":""+b)},has:function(a){return this.attributes[a]!=null},set:function(a,b,c){var d, +e;f.isObject(a)||a==null?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;if(d instanceof g.Model)d=d.attributes;if(c.unset)for(e in d)d[e]=void 0;if(this.validate&&!this._performValidation(d,c))return false;if(this.idAttribute in d)this.id=d[this.idAttribute];var b=this.attributes,k=this._escapedAttributes,n=this._previousAttributes||{},u=this._changing;this._changing=true;for(e in d)if(a=d[e],f.isEqual(b[e],a)||delete k[e],c.unset?delete b[e]:b[e]=a,delete this._changed[e],!f.isEqual(n[e],a)|| +f.has(b,e)!=f.has(n,e))this._changed[e]=a;if(!u)!c.silent&&this.hasChanged()&&this.change(c),this._changing=false;return this},unset:function(a,b){(b||(b={})).unset=true;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=true;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return false;c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)}, +save:function(a,b,c){var d;f.isObject(a)||a==null?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(d&&!this[c.wait?"_performValidation":"set"](d,c))return false;var e=this,k=c.success;c.success=function(a,b,g){b=e.parse(a,g);c.wait&&(b=f.extend(d||{},b));if(!e.set(b,c))return false;k?k(e,a):e.trigger("sync",e,a,c)};c.error=g.wrapError(c.error,e,c);a=this.isNew()?"create":"update";return(this.sync||g.sync).call(this,a,this,c)},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy", +b,b.collection,a)};if(this.isNew())return d();a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=j(this.collection,"url")||j(this,"urlRoot")||o();return this.isNew()?a:a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return this.id== +null},change:function(a){for(var b in this._changed)this.trigger("change:"+b,this,this._changed[b],a);this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed={}},hasChanged:function(a){return a?f.has(this._changed,a):!f.isEmpty(this._changed)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this._changed):false;var b,c=false,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!a|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(f.extend({},this.attributes,a),b);return c?(b.error?b.error(this,c,b):this.trigger("error",this,c,b),false):true}});g.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:true,parse:b.parse})};f.extend(g.Collection.prototype, +g.Events,{model:g.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){var c,d,e,g,h,i={},j={};b||(b={});a=f.isArray(a)?a.slice():[a];for(c=0,d=a.length;c').hide().appendTo("body")[0].contentWindow,this.navigate(a);if(this._hasPushState)h(window).bind("popstate",this.checkUrl);else if(this._wantsHashChange&&"onhashchange"in window&&!b)h(window).bind("hashchange",this.checkUrl);else if(this._wantsHashChange)this._checkUrlInterval=setInterval(this.checkUrl,this.interval); +this.fragment=a;l=true;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,true),window.location.replace(this.options.root+"#"+this.fragment),true;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(m,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()}, +stop:function(){h(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);l=false},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return false;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b= +this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),true})},navigate:function(a,b){if(!l)return false;if(!b||b===true)b={trigger:b};var c=(a||"").replace(m,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c)))this._hasPushState?(c.indexOf(this.options.root)!=0&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location, +c,b.replace),this.iframe&&c!=this.getFragment(this.iframe.location.hash)&&(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a)},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()}; +var z=/^(\S+)\s*(.*)$/,p="model,collection,el,id,attributes,className,tagName".split(",");f.extend(g.View.prototype,g.Events,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},setElement:function(a,b){this.$el=h(a);this.el=this.$el[0];b!==false&&this.delegateEvents()},delegateEvents:function(a){if(a||(a= +j(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(z),e=d[1],d=d[2],c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=p.length;b backbone.js

backbone.js

Backbone.js 0.5.3
-(c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+      backbone.js           

backbone.js

Backbone.js 0.9.0
+(c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
 Backbone may be freely distributed under the MIT license.
 For all details and documentation:
-http://documentcloud.github.com/backbone
-
(function(){

Initial Setup

Save a reference to the global object.

  var root = this;

Save the previous value of the Backbone variable.

  var previousBackbone = root.Backbone;

Create a local reference to slice.

  var slice = Array.prototype.slice;

The top-level namespace. All public Backbone classes and modules will +http://backbonejs.org +

(function(){

Initial Setup

Save a reference to the global object (window in the browser, global +on the server).

  var root = this;

Save the previous value of the Backbone variable, so that it can be +restored later on, if noConflict is used.

  var previousBackbone = root.Backbone;

Create a local reference to slice/splice.

  var slice = Array.prototype.slice;
+  var splice = Array.prototype.splice;

The top-level namespace. All public Backbone classes and modules will be attached to this. Exported for both CommonJS and the browser.

  var Backbone;
   if (typeof exports !== 'undefined') {
     Backbone = exports;
   } else {
     Backbone = root.Backbone = {};
-  }

Current version of the library. Keep in sync with package.json.

  Backbone.VERSION = '0.5.3';

Require Underscore, if we're on the server, and it's not already present.

  var _ = root._;
-  if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;

For Backbone's purposes, jQuery, Zepto, or Ender owns the $ variable.

  var $ = root.jQuery || root.Zepto || root.ender;

Runs Backbone.js in noConflict mode, returning the Backbone variable + }

Current version of the library. Keep in sync with package.json.

  Backbone.VERSION = '0.9.0';

Require Underscore, if we're on the server, and it's not already present.

  var _ = root._;
+  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');

For Backbone's purposes, jQuery, Zepto, or Ender owns the $ variable.

  var $ = root.jQuery || root.Zepto || root.ender;

Runs Backbone.js in noConflict mode, returning the Backbone variable to its previous owner. Returns a reference to this Backbone object.

  Backbone.noConflict = function() {
     root.Backbone = previousBackbone;
     return this;
-  };

Turn on emulateHTTP to support legacy HTTP servers. Setting this option will -fake "PUT" and "DELETE" requests via the _method parameter and set a -X-Http-Method-Override header.

  Backbone.emulateHTTP = false;

Turn on emulateJSON to support legacy servers that can't deal with direct + };

Turn on emulateHTTP to support legacy HTTP servers. Setting this option +will fake "PUT" and "DELETE" requests via the _method parameter and +set a X-Http-Method-Override header.

  Backbone.emulateHTTP = false;

Turn on emulateJSON to support legacy servers that can't deal with direct application/json requests ... will encode the body as application/x-www-form-urlencoded instead and will send the model in a form param named model.

  Backbone.emulateJSON = false;

Backbone.Events

A module that can be mixed in to any object in order to provide it with -custom events. You may bind or unbind a callback function to an event; -trigger-ing an event fires all callbacks in succession.

+custom events. You may bind with on or remove with off callback functions +to an event; trigger`-ing an event fires all callbacks in succession.

var object = {};
 _.extend(object, Backbone.Events);
-object.bind('expand', function(){ alert('expanded'); });
+object.on('expand', function(){ alert('expanded'); });
 object.trigger('expand');
-
  Backbone.Events = {

Bind an event, specified by a string name, ev, to a callback function. -Passing "all" will bind the callback to all events fired.

    bind : function(ev, callback, context) {
+
  Backbone.Events = {

Bind an event, specified by a string name, ev, to a callback +function. Passing "all" will bind the callback to all events fired.

    on: function(events, callback, context) {
+      var ev;
+      events = events.split(/\s+/);
       var calls = this._callbacks || (this._callbacks = {});
-      var list  = calls[ev] || (calls[ev] = {});
-      var tail = list.tail || (list.tail = list.next = {});
-      tail.callback = callback;
-      tail.context = context;
-      list.tail = tail.next = {};
+      while (ev = events.shift()) {

Create an immutable callback list, allowing traversal during +modification. The tail is an empty object that will always be used +as the next node.

        var list  = calls[ev] || (calls[ev] = {});
+        var tail = list.tail || (list.tail = list.next = {});
+        tail.callback = callback;
+        tail.context = context;
+        list.tail = tail.next = {};
+      }
       return this;
-    },

Remove one or many callbacks. If callback is null, removes all -callbacks for the event. If ev is null, removes all bound callbacks -for all events.

    unbind : function(ev, callback) {
-      var calls, node, prev;
-      if (!ev) {
-        this._callbacks = null;
+    },

Remove one or many callbacks. If context is null, removes all callbacks +with that function. If callback is null, removes all callbacks for the +event. If ev is null, removes all bound callbacks for all events.

    off: function(events, callback, context) {
+      var ev, calls, node;
+      if (!events) {
+        delete this._callbacks;
       } else if (calls = this._callbacks) {
-        if (!callback) {
-          calls[ev] = {};
-        } else if (node = calls[ev]) {
-          while ((prev = node) && (node = node.next)) {
-            if (node.callback !== callback) continue;
-            prev.next = node.next;
-            node.context = node.callback = null;
-            break;
+        events = events.split(/\s+/);
+        while (ev = events.shift()) {
+          node = calls[ev];
+          delete calls[ev];
+          if (!callback || !node) continue;

Create a new list, omitting the indicated event/context pairs.

          while ((node = node.next) && node.next) {
+            if (node.callback === callback &&
+              (!context || node.context === context)) continue;
+            this.on(ev, node.callback, node.context);
           }
         }
       }
       return this;
-    },

Trigger an event, firing all bound callbacks. Callbacks are passed the + },

Trigger an event, firing all bound callbacks. Callbacks are passed the same arguments as trigger is, apart from the event name. -Listening for "all" passes the true event name as the first argument.

    trigger : function(eventName) {
-      var node, calls, callback, args, ev, events = ['all', eventName];
+Listening for "all" passes the true event name as the first argument.

    trigger: function(events) {
+      var event, node, calls, tail, args, all, rest;
       if (!(calls = this._callbacks)) return this;
-      while (ev = events.pop()) {
-        if (!(node = calls[ev])) continue;
-        args = ev == 'all' ? arguments : slice.call(arguments, 1);
-        while (node = node.next) if (callback = node.callback) callback.apply(node.context || this, args);
+      all = calls['all'];
+      (events = events.split(/\s+/)).push(null);

Save references to the current heads & tails.

      while (event = events.shift()) {
+        if (all) events.push({next: all.next, tail: all.tail, event: event});
+        if (!(node = calls[event])) continue;
+        events.push({next: node.next, tail: node.tail});
+      }

Traverse each list, stopping when the saved tail is reached.

      rest = slice.call(arguments, 1);
+      while (node = events.pop()) {
+        tail = node.tail;
+        args = node.event ? [node.event].concat(rest) : rest;
+        while ((node = node.next) !== tail) {
+          node.callback.apply(node.context || this, args);
+        }
       }
       return this;
     }
 
-  };

Backbone.Model

Create a new model, with defined attributes. A client id (cid) + };

Aliases for backwards compatibility.

  Backbone.Events.bind   = Backbone.Events.on;
+  Backbone.Events.unbind = Backbone.Events.off;

Backbone.Model

Create a new model, with defined attributes. A client id (cid) is automatically generated and assigned for you.

  Backbone.Model = function(attributes, options) {
     var defaults;
     attributes || (attributes = {});
-    if (defaults = this.defaults) {
-      if (_.isFunction(defaults)) defaults = defaults.call(this);
+    if (options && options.parse) attributes = this.parse(attributes);
+    if (defaults = getValue(this, 'defaults')) {
       attributes = _.extend({}, defaults, attributes);
     }
+    if (options && options.collection) this.collection = options.collection;
     this.attributes = {};
     this._escapedAttributes = {};
     this.cid = _.uniqueId('c');
-    this.set(attributes, {silent : true});
-    this._changed = false;
+    this._changed = {};
+    if (!this.set(attributes, {silent: true})) {
+      throw new Error("Can't create an invalid model");
+    }
+    this._changed = {};
     this._previousAttributes = _.clone(this.attributes);
-    if (options && options.collection) this.collection = options.collection;
-    this.initialize(attributes, options);
-  };

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Events, {

Has the item been changed since the last "change" event?

    _changed : false,

The default name for the JSON id attribute is "id". MongoDB and -CouchDB users may want to set this to "_id".

    idAttribute : 'id',

Initialize is an empty function by default. Override it with your own -initialization logic.

    initialize : function(){},

Return a copy of the model's attributes object.

    toJSON : function() {
+    this.initialize.apply(this, arguments);
+  };

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Events, {

The default name for the JSON id attribute is "id". MongoDB and +CouchDB users may want to set this to "_id".

    idAttribute: 'id',

Initialize is an empty function by default. Override it with your own +initialization logic.

    initialize: function(){},

Return a copy of the model's attributes object.

    toJSON: function() {
       return _.clone(this.attributes);
-    },

Get the value of an attribute.

    get : function(attr) {
+    },

Get the value of an attribute.

    get: function(attr) {
       return this.attributes[attr];
-    },

Get the HTML-escaped value of an attribute.

    escape : function(attr) {
+    },

Get the HTML-escaped value of an attribute.

    escape: function(attr) {
       var html;
       if (html = this._escapedAttributes[attr]) return html;
       var val = this.attributes[attr];
       return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
-    },

Returns true if the attribute contains a value that is not null -or undefined.

    has : function(attr) {
+    },

Returns true if the attribute contains a value that is not null +or undefined.

    has: function(attr) {
       return this.attributes[attr] != null;
-    },

Set a hash of model attributes on the object, firing "change" unless you -choose to silence it.

    set : function(attrs, options) {

Extract attributes and options.

      options || (options = {});
+    },

Set a hash of model attributes on the object, firing "change" unless +you choose to silence it.

    set: function(key, value, options) {
+      var attrs, attr, val;
+      if (_.isObject(key) || key == null) {
+        attrs = key;
+        options = value;
+      } else {
+        attrs = {};
+        attrs[key] = value;
+      }

Extract attributes and options.

      options || (options = {});
       if (!attrs) return this;
-      if (attrs.attributes) attrs = attrs.attributes;
-      var now = this.attributes, escaped = this._escapedAttributes;

Run validation.

      if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;

Check for changes of id.

      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];

We're about to start triggering change events.

      var alreadyChanging = this._changing;
-      this._changing = true;

Update attributes.

      for (var attr in attrs) {
-        var val = attrs[attr];
-        if (!_.isEqual(now[attr], val)) {
-          now[attr] = val;
-          delete escaped[attr];
-          this._changed = true;
-          if (!options.silent) this.trigger('change:' + attr, this, val, options);
+      if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
+      if (options.unset) for (var attr in attrs) attrs[attr] = void 0;

Run validation.

      if (this.validate && !this._performValidation(attrs, options)) return false;

Check for changes of id.

      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+      var now = this.attributes;
+      var escaped = this._escapedAttributes;
+      var prev = this._previousAttributes || {};
+      var alreadyChanging = this._changing;
+      this._changing = true;

Update attributes.

      for (attr in attrs) {
+        val = attrs[attr];
+        if (!_.isEqual(now[attr], val)) delete escaped[attr];
+        options.unset ? delete now[attr] : now[attr] = val;
+        delete this._changed[attr];
+        if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
+          this._changed[attr] = val;
         }
-      }

Fire the "change" event, if the model has been changed.

      if (!alreadyChanging) {
-        if (!options.silent && this._changed) this.change(options);
+      }

Fire the "change" events, if the model has been changed.

      if (!alreadyChanging) {
+        if (!options.silent && this.hasChanged()) this.change(options);
         this._changing = false;
       }
       return this;
-    },

Remove an attribute from the model, firing "change" unless you choose -to silence it. unset is a noop if the attribute doesn't exist.

    unset : function(attr, options) {
-      if (!(attr in this.attributes)) return this;
-      options || (options = {});
-      var value = this.attributes[attr];

Run validation.

      var validObj = {};
-      validObj[attr] = void 0;
-      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;

changedAttributes needs to know if an attribute has been unset.

      (this._unsetAttributes || (this._unsetAttributes = [])).push(attr);

Remove the attribute.

      delete this.attributes[attr];
-      delete this._escapedAttributes[attr];
-      if (attr == this.idAttribute) delete this.id;
-      this._changed = true;
-      if (!options.silent) {
-        this.trigger('change:' + attr, this, void 0, options);
-        this.change(options);
-      }
-      return this;
+    },

Remove an attribute from the model, firing "change" unless you choose +to silence it. unset is a noop if the attribute doesn't exist.

    unset: function(attr, options) {
+      (options || (options = {})).unset = true;
+      return this.set(attr, null, options);
     },

Clear all attributes on the model, firing "change" unless you choose -to silence it.

    clear : function(options) {
-      options || (options = {});
-      var attr;
-      var old = this.attributes;

Run validation.

      var validObj = {};
-      for (attr in old) validObj[attr] = void 0;
-      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
-
-      this.attributes = {};
-      this._escapedAttributes = {};
-      this._changed = true;
-      if (!options.silent) {
-        for (attr in old) {
-          this.trigger('change:' + attr, this, void 0, options);
-        }
-        this.change(options);
-      }
-      return this;
-    },

Fetch the model from the server. If the server's representation of the +to silence it.

    clear: function(options) {
+      (options || (options = {})).unset = true;
+      return this.set(_.clone(this.attributes), options);
+    },

Fetch the model from the server. If the server's representation of the model differs from its current attributes, they will be overriden, -triggering a "change" event.

    fetch : function(options) {
-      options || (options = {});
+triggering a "change" event.

    fetch: function(options) {
+      options = options ? _.clone(options) : {};
       var model = this;
       var success = options.success;
       options.success = function(resp, status, xhr) {
         if (!model.set(model.parse(resp, xhr), options)) return false;
         if (success) success(model, resp);
       };
-      options.error = wrapError(options.error, model, options);
+      options.error = Backbone.wrapError(options.error, model, options);
       return (this.sync || Backbone.sync).call(this, 'read', this, options);
-    },

Set a hash of model attributes, and sync the model to the server. + },

Set a hash of model attributes, and sync the model to the server. If the server returns an attributes hash that differs, the model's -state will be set again.

    save : function(attrs, options) {
-      options || (options = {});
-      if (attrs && !this.set(attrs, options)) return false;
+state will be set again.

    save: function(key, value, options) {
+      var attrs;
+      if (_.isObject(key) || key == null) {
+        attrs = key;
+        options = value;
+      } else {
+        attrs = {};
+        attrs[key] = value;
+      }
+
+      options = options ? _.clone(options) : {};
+      if (attrs && !this[options.wait ? '_performValidation' : 'set'](attrs, options)) return false;
       var model = this;
       var success = options.success;
       options.success = function(resp, status, xhr) {
-        if (!model.set(model.parse(resp, xhr), options)) return false;
-        if (success) success(model, resp, xhr);
+        var serverAttrs = model.parse(resp, xhr);
+        if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
+        if (!model.set(serverAttrs, options)) return false;
+        if (success) {
+          success(model, resp);
+        } else {
+          model.trigger('sync', model, resp, options);
+        }
       };
-      options.error = wrapError(options.error, model, options);
+      options.error = Backbone.wrapError(options.error, model, options);
       var method = this.isNew() ? 'create' : 'update';
       return (this.sync || Backbone.sync).call(this, method, this, options);
-    },

Destroy this model on the server if it was already persisted. Upon success, the model is removed -from its collection, if it has one.

    destroy : function(options) {
-      options || (options = {});
-      if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
+    },

Destroy this model on the server if it was already persisted. +Optimistically removes the model from its collection, if it has one. +If wait: true is passed, waits for the server to respond before removal.

    destroy: function(options) {
+      options = options ? _.clone(options) : {};
       var model = this;
       var success = options.success;
-      options.success = function(resp) {
+
+      var triggerDestroy = function() {
         model.trigger('destroy', model, model.collection, options);
-        if (success) success(model, resp);
       };
-      options.error = wrapError(options.error, model, options);
-      return (this.sync || Backbone.sync).call(this, 'delete', this, options);
-    },

Default URL for the model's representation on the server -- if you're + + if (this.isNew()) return triggerDestroy(); + options.success = function(resp) { + if (options.wait) triggerDestroy(); + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + options.error = Backbone.wrapError(options.error, model, options); + var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); + if (!options.wait) triggerDestroy(); + return xhr; + },

Default URL for the model's representation on the server -- if you're using Backbone's restful methods, override this to change the endpoint -that will be called.

    url : function() {
-      var base = getUrl(this.collection) || this.urlRoot || urlError();
+that will be called.

    url: function() {
+      var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();
       if (this.isNew()) return base;
       return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
-    },

parse converts a response into the hash of attributes to be set on -the model. The default implementation is just to pass the response along.

    parse : function(resp, xhr) {
+    },

parse converts a response into the hash of attributes to be set on +the model. The default implementation is just to pass the response along.

    parse: function(resp, xhr) {
       return resp;
-    },

Create a new model with identical attributes to this one.

    clone : function() {
-      return new this.constructor(this);
-    },

A model is new if it has never been saved to the server, and lacks an id.

    isNew : function() {
+    },

Create a new model with identical attributes to this one.

    clone: function() {
+      return new this.constructor(this.attributes);
+    },

A model is new if it has never been saved to the server, and lacks an id.

    isNew: function() {
       return this.id == null;
-    },

Call this method to manually fire a change event for this model. -Calling this will cause all objects observing the model to update.

    change : function(options) {
+    },

Call this method to manually fire a "change" event for this model and +a "change:attribute" event for each changed attribute. +Calling this will cause all objects observing the model to update.

    change: function(options) {
+      for (var attr in this._changed) {
+        this.trigger('change:' + attr, this, this._changed[attr], options);
+      }
       this.trigger('change', this, options);
       this._previousAttributes = _.clone(this.attributes);
-      this._unsetAttributes = null;
-      this._changed = false;
-    },

Determine if the model has changed since the last "change" event. -If you specify an attribute name, determine if that attribute has changed.

    hasChanged : function(attr) {
-      if (attr) return this._previousAttributes[attr] != this.attributes[attr];
-      return this._changed;
-    },

Return an object containing all the attributes that have changed, or false -if there are no changed attributes. Useful for determining what parts of a -view need to be updated and/or what attributes need to be persisted to -the server. Unset attributes will be set to undefined.

    changedAttributes : function(now) {
-      now || (now = this.attributes);
-      var old = this._previousAttributes, unset = this._unsetAttributes;
-
-      var changed = false;
-      for (var attr in now) {
-        if (!_.isEqual(old[attr], now[attr])) {
-          changed || (changed = {});
-          changed[attr] = now[attr];
-        }
-      }
-
-      if (unset) {
-        changed || (changed = {});
-        var len = unset.length;
-        while (len--) changed[unset[len]] = void 0;
+      this._changed = {};
+    },

Determine if the model has changed since the last "change" event. +If you specify an attribute name, determine if that attribute has changed.

    hasChanged: function(attr) {
+      if (attr) return _.has(this._changed, attr);
+      return !_.isEmpty(this._changed);
+    },

Return an object containing all the attributes that have changed, or +false if there are no changed attributes. Useful for determining what +parts of a view need to be updated and/or what attributes need to be +persisted to the server. Unset attributes will be set to undefined. +You can also pass an attributes object to diff against the model, +determining if there would be a change.

    changedAttributes: function(diff) {
+      if (!diff) return this.hasChanged() ? _.clone(this._changed) : false;
+      var val, changed = false, old = this._previousAttributes;
+      for (var attr in diff) {
+        if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+        (changed || (changed = {}))[attr] = val;
       }
-
       return changed;
-    },

Get the previous value of an attribute, recorded at the time the last -"change" event was fired.

    previous : function(attr) {
+    },

Get the previous value of an attribute, recorded at the time the last +"change" event was fired.

    previous: function(attr) {
       if (!attr || !this._previousAttributes) return null;
       return this._previousAttributes[attr];
-    },

Get all of the attributes of the model at the time of the previous -"change" event.

    previousAttributes : function() {
+    },

Get all of the attributes of the model at the time of the previous +"change" event.

    previousAttributes: function() {
       return _.clone(this._previousAttributes);
-    },

Run validation against a set of incoming attributes, returning true + },

Run validation against a set of incoming attributes, returning true if all is well. If a specific error callback has been passed, -call that instead of firing the general "error" event.

    _performValidation : function(attrs, options) {
-      var error = this.validate(attrs);
+call that instead of firing the general "error" event.

    _performValidation: function(attrs, options) {
+      var newAttrs = _.extend({}, this.attributes, attrs);
+      var error = this.validate(newAttrs, options);
       if (error) {
         if (options.error) {
           options.error(this, error, options);
@@ -254,158 +284,170 @@
       return true;
     }
 
-  });

Backbone.Collection

Provides a standard collection class for our sets of models, ordered + });

Backbone.Collection

Provides a standard collection class for our sets of models, ordered or unordered. If a comparator is specified, the Collection will maintain its models in sort order, as they're added and removed.

  Backbone.Collection = function(models, options) {
     options || (options = {});
     if (options.comparator) this.comparator = options.comparator;
-    _.bindAll(this, '_onModelEvent', '_removeReference');
     this._reset();
-    if (models) this.reset(models, {silent: true});
     this.initialize.apply(this, arguments);
-  };

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Events, {

The default model for a collection is just a Backbone.Model. -This should be overridden in most cases.

    model : Backbone.Model,

Initialize is an empty function by default. Override it with your own -initialization logic.

    initialize : function(){},

The JSON representation of a Collection is an array of the -models' attributes.

    toJSON : function() {
+    if (models) this.reset(models, {silent: true, parse: options.parse});
+  };

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Events, {

The default model for a collection is just a Backbone.Model. +This should be overridden in most cases.

    model: Backbone.Model,

Initialize is an empty function by default. Override it with your own +initialization logic.

    initialize: function(){},

The JSON representation of a Collection is an array of the +models' attributes.

    toJSON: function() {
       return this.map(function(model){ return model.toJSON(); });
-    },

Add a model, or list of models to the set. Pass silent to avoid -firing the added event for every new model.

    add : function(models, options) {
-      if (_.isArray(models)) {
-        for (var i = 0, l = models.length; i < l; i++) {
-          this._add(models[i], options);
+    },

Add a model, or list of models to the set. Pass silent to avoid +firing the add event for every new model.

    add: function(models, options) {
+      var i, index, length, model, cid, id, cids = {}, ids = {};
+      options || (options = {});
+      models = _.isArray(models) ? models.slice() : [models];

Begin by turning bare objects into model references, and preventing +invalid models or duplicate models from being added.

      for (i = 0, length = models.length; i < length; i++) {
+        if (!(model = models[i] = this._prepareModel(models[i], options))) {
+          throw new Error("Can't add an invalid model to a collection");
         }
-      } else {
-        this._add(models, options);
+        if (cids[cid = model.cid] || this._byCid[cid] ||
+          (((id = model.id) != null) && (ids[id] || this._byId[id]))) {
+          throw new Error("Can't add the same model to a collection twice");
+        }
+        cids[cid] = ids[id] = model;
+      }

Listen to added models' events, and index models for lookup by +id and by cid.

      for (i = 0; i < length; i++) {
+        (model = models[i]).on('all', this._onModelEvent, this);
+        this._byCid[model.cid] = model;
+        if (model.id != null) this._byId[model.id] = model;
+      }

Insert models into the collection, re-sorting if needed, and triggering +add events unless silenced.

      this.length += length;
+      index = options.at != null ? options.at : this.models.length;
+      splice.apply(this.models, [index, 0].concat(models));
+      if (this.comparator) this.sort({silent: true});
+      if (options.silent) return this;
+      for (i = 0, length = this.models.length; i < length; i++) {
+        if (!cids[(model = this.models[i]).cid]) continue;
+        options.index = i;
+        model.trigger('add', model, this, options);
       }
       return this;
-    },

Remove a model, or a list of models from the set. Pass silent to avoid -firing the removed event for every model removed.

    remove : function(models, options) {
-      if (_.isArray(models)) {
-        for (var i = 0, l = models.length; i < l; i++) {
-          this._remove(models[i], options);
+    },

Remove a model, or a list of models from the set. Pass silent to avoid +firing the remove event for every model removed.

    remove: function(models, options) {
+      var i, l, index, model;
+      options || (options = {});
+      models = _.isArray(models) ? models.slice() : [models];
+      for (i = 0, l = models.length; i < l; i++) {
+        model = this.getByCid(models[i]) || this.get(models[i]);
+        if (!model) continue;
+        delete this._byId[model.id];
+        delete this._byCid[model.cid];
+        index = this.indexOf(model);
+        this.models.splice(index, 1);
+        this.length--;
+        if (!options.silent) {
+          options.index = index;
+          model.trigger('remove', model, this, options);
         }
-      } else {
-        this._remove(models, options);
+        this._removeReference(model);
       }
       return this;
-    },

Get a model from the set by id.

    get : function(id) {
+    },

Get a model from the set by id.

    get: function(id) {
       if (id == null) return null;
       return this._byId[id.id != null ? id.id : id];
-    },

Get a model from the set by client id.

    getByCid : function(cid) {
+    },

Get a model from the set by client id.

    getByCid: function(cid) {
       return cid && this._byCid[cid.cid || cid];
-    },

Get the model at the given index.

    at : function(index) {
+    },

Get the model at the given index.

    at: function(index) {
       return this.models[index];
-    },

Force the collection to re-sort itself. You don't need to call this under normal -circumstances, as the set will maintain sort order as each item is added.

    sort : function(options) {
+    },

Force the collection to re-sort itself. You don't need to call this under +normal circumstances, as the set will maintain sort order as each item +is added.

    sort: function(options) {
       options || (options = {});
       if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
-      this.models = this.sortBy(this.comparator);
+      var boundComparator = _.bind(this.comparator, this);
+      if (this.comparator.length == 1) {
+        this.models = this.sortBy(boundComparator);
+      } else {
+        this.models.sort(boundComparator);
+      }
       if (!options.silent) this.trigger('reset', this, options);
       return this;
-    },

Pluck an attribute from each model in the collection.

    pluck : function(attr) {
+    },

Pluck an attribute from each model in the collection.

    pluck: function(attr) {
       return _.map(this.models, function(model){ return model.get(attr); });
-    },

When you have more items than you want to add or remove individually, + },

When you have more items than you want to add or remove individually, you can reset the entire set with a new list of models, without firing -any added or removed events. Fires reset when finished.

    reset : function(models, options) {
+any add or remove events. Fires reset when finished.

    reset: function(models, options) {
       models  || (models = []);
       options || (options = {});
-      this.each(this._removeReference);
+      for (var i = 0, l = this.models.length; i < l; i++) {
+        this._removeReference(this.models[i]);
+      }
       this._reset();
-      this.add(models, {silent: true});
+      this.add(models, {silent: true, parse: options.parse});
       if (!options.silent) this.trigger('reset', this, options);
       return this;
-    },

Fetch the default set of models for this collection, resetting the + },

Fetch the default set of models for this collection, resetting the collection when they arrive. If add: true is passed, appends the -models to the collection instead of resetting.

    fetch : function(options) {
-      options || (options = {});
+models to the collection instead of resetting.

    fetch: function(options) {
+      options = options ? _.clone(options) : {};
+      if (options.parse === undefined) options.parse = true;
       var collection = this;
       var success = options.success;
       options.success = function(resp, status, xhr) {
         collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
         if (success) success(collection, resp);
       };
-      options.error = wrapError(options.error, collection, options);
+      options.error = Backbone.wrapError(options.error, collection, options);
       return (this.sync || Backbone.sync).call(this, 'read', this, options);
-    },

Create a new instance of a model in this collection. After the model -has been created on the server, it will be added to the collection. -Returns the model, or 'false' if validation on a new model fails.

    create : function(model, options) {
+    },

Create a new instance of a model in this collection. Add the model to the +collection immediately, unless wait: true is passed, in which case we +wait for the server to agree.

    create: function(model, options) {
       var coll = this;
-      options || (options = {});
+      options = options ? _.clone(options) : {};
       model = this._prepareModel(model, options);
       if (!model) return false;
+      if (!options.wait) coll.add(model, options);
       var success = options.success;
       options.success = function(nextModel, resp, xhr) {
-        coll.add(nextModel, options);
-        if (success) success(nextModel, resp, xhr);
+        if (options.wait) coll.add(nextModel, options);
+        if (success) {
+          success(nextModel, resp);
+        } else {
+          nextModel.trigger('sync', model, resp, options);
+        }
       };
       model.save(null, options);
       return model;
-    },

parse converts a response into a list of models to be added to the -collection. The default implementation is just to pass it through.

    parse : function(resp, xhr) {
+    },

parse converts a response into a list of models to be added to the +collection. The default implementation is just to pass it through.

    parse: function(resp, xhr) {
       return resp;
-    },

Proxy to _'s chain. Can't be proxied the same way the rest of the + },

Proxy to _'s chain. Can't be proxied the same way the rest of the underscore methods are proxied because it relies on the underscore -constructor.

    chain : function () {
+constructor.

    chain: function () {
       return _(this.models).chain();
-    },

Reset all internal state. Called when the collection is reset.

    _reset : function(options) {
+    },

Reset all internal state. Called when the collection is reset.

    _reset: function(options) {
       this.length = 0;
       this.models = [];
       this._byId  = {};
       this._byCid = {};
-    },

Prepare a model to be added to this collection

    _prepareModel : function(model, options) {
+    },

Prepare a model or hash of attributes to be added to this collection.

    _prepareModel: function(model, options) {
       if (!(model instanceof Backbone.Model)) {
         var attrs = model;
-        model = new this.model(attrs, {collection: this});
+        options.collection = this;
+        model = new this.model(attrs, options);
         if (model.validate && !model._performValidation(model.attributes, options)) model = false;
       } else if (!model.collection) {
         model.collection = this;
       }
       return model;
-    },

Internal implementation of adding a single model to the set, updating -hash indexes for id and cid lookups. -Returns the model, or 'false' if validation on a new model fails.

    _add : function(model, options) {
-      options || (options = {});
-      model = this._prepareModel(model, options);
-      if (!model) return false;
-      var already = this.getByCid(model);
-      if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
-      this._byId[model.id] = model;
-      this._byCid[model.cid] = model;
-      var index = options.at != null ? options.at :
-                  this.comparator ? this.sortedIndex(model, this.comparator) :
-                  this.length;
-      this.models.splice(index, 0, model);
-      model.bind('all', this._onModelEvent);
-      this.length++;
-      options.index = index;
-      if (!options.silent) model.trigger('add', model, this, options);
-      return model;
-    },

Internal implementation of removing a single model from the set, updating -hash indexes for id and cid lookups.

    _remove : function(model, options) {
-      options || (options = {});
-      model = this.getByCid(model) || this.get(model);
-      if (!model) return null;
-      delete this._byId[model.id];
-      delete this._byCid[model.cid];
-      var index = this.indexOf(model);
-      this.models.splice(index, 1);
-      this.length--;
-      options.index = index;
-      if (!options.silent) model.trigger('remove', model, this, options);
-      this._removeReference(model);
-      return model;
-    },

Internal method to remove a model's ties to a collection.

    _removeReference : function(model) {
+    },

Internal method to remove a model's ties to a collection.

    _removeReference: function(model) {
       if (this == model.collection) {
         delete model.collection;
       }
-      model.unbind('all', this._onModelEvent);
+      model.off('all', this._onModelEvent, this);
     },

Internal method called every time a model in the set fires an event. Sets need to update their indexes when models change ids. All other events simply proxy through. "add" and "remove" events that originate -in other collections are ignored.

    _onModelEvent : function(ev, model, collection, options) {
+in other collections are ignored.

    _onModelEvent: function(ev, model, collection, options) {
       if ((ev == 'add' || ev == 'remove') && collection != this) return;
       if (ev == 'destroy') {
-        this._remove(model, options);
+        this.remove(model, options);
       }
       if (model && ev === 'change:' + model.idAttribute) {
         delete this._byId[model.previous(model.idAttribute)];
@@ -414,10 +456,11 @@
       this.trigger.apply(this, arguments);
     }
 
-  });

Underscore methods that we want to implement on the Collection.

  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
-    'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
-    'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
-    'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];

Mix in each Underscore method as a proxy to Collection#models.

  _.each(methods, function(method) {
+  });

Underscore methods that we want to implement on the Collection.

  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
+    'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
+    'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
+    'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
+    'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];

Mix in each Underscore method as a proxy to Collection#models.

  _.each(methods, function(method) {
     Backbone.Collection.prototype[method] = function() {
       return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
     };
@@ -428,27 +471,30 @@
     this._bindRoutes();
     this.initialize.apply(this, arguments);
   };

Cached regular expressions for matching named param parts and splatted -parts of route strings.

  var namedParam    = /:([\w\d]+)/g;
-  var splatParam    = /\*([\w\d]+)/g;
+parts of route strings.

  var namedParam    = /:\w+/g;
+  var splatParam    = /\*\w+/g;
   var escapeRegExp  = /[-[\]{}()+?.,\\^$|#\s]/g;

Set up all inheritable Backbone.Router properties and methods.

  _.extend(Backbone.Router.prototype, Backbone.Events, {

Initialize is an empty function by default. Override it with your own -initialization logic.

    initialize : function(){},

Manually bind a single named route to a callback. For example:

+initialization logic.

    initialize: function(){},

Manually bind a single named route to a callback. For example:

this.route('search/:query/p:num', 'search', function(query, num) {
   ...
 });
-
    route : function(route, name, callback) {
+
    route: function(route, name, callback) {
       Backbone.history || (Backbone.history = new Backbone.History);
       if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+      if (!callback) callback = this[name];
       Backbone.history.route(route, _.bind(function(fragment) {
         var args = this._extractParameters(route, fragment);
         callback && callback.apply(this, args);
         this.trigger.apply(this, ['route:' + name].concat(args));
+        Backbone.history.trigger('route', this, name, args);
       }, this));
-    },

Simple proxy to Backbone.history to save a fragment into the history.

    navigate : function(fragment, options) {
+      return this;
+    },

Simple proxy to Backbone.history to save a fragment into the history.

    navigate: function(fragment, options) {
       Backbone.history.navigate(fragment, options);
     },

Bind all defined routes to Backbone.history. We have to reverse the order of the routes here to support behavior where the most general -routes can be defined at the bottom of the route map.

    _bindRoutes : function() {
+routes can be defined at the bottom of the route map.

    _bindRoutes: function() {
       if (!this.routes) return;
       var routes = [];
       for (var route in this.routes) {
@@ -458,13 +504,13 @@
         this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
       }
     },

Convert a route string into a regular expression, suitable for matching -against the current location hash.

    _routeToRegExp : function(route) {
-      route = route.replace(escapeRegExp, "\\$&")
-                   .replace(namedParam, "([^\/]*)")
-                   .replace(splatParam, "(.*?)");
+against the current location hash.

    _routeToRegExp: function(route) {
+      route = route.replace(escapeRegExp, '\\$&')
+                   .replace(namedParam, '([^\/]+)')
+                   .replace(splatParam, '(.*?)');
       return new RegExp('^' + route + '$');
     },

Given a route, and a URL fragment that it matches, return the array of -extracted parameters.

    _extractParameters : function(route, fragment) {
+extracted parameters.

    _extractParameters: function(route, fragment) {
       return route.exec(fragment).slice(1);
     }
 
@@ -472,9 +518,9 @@
 browser does not support onhashchange, falls back to polling.

  Backbone.History = function() {
     this.handlers = [];
     _.bindAll(this, 'checkUrl');
-  };

Cached regex for cleaning hashes.

  var hashStrip = /^#*/;

Cached regex for detecting MSIE.

  var isExplorer = /msie [\w.]+/;

Has the history handling already been started?

  var historyStarted = false;

Set up all inheritable Backbone.History properties and methods.

  _.extend(Backbone.History.prototype, {

The default interval to poll for hash changes, if necessary, is + };

Cached regex for cleaning leading hashes and slashes .

  var routeStripper = /^[#\/]/;

Cached regex for detecting MSIE.

  var isExplorer = /msie [\w.]+/;

Has the history handling already been started?

  var historyStarted = false;

Set up all inheritable Backbone.History properties and methods.

  _.extend(Backbone.History.prototype, Backbone.Events, {

The default interval to poll for hash changes, if necessary, is twenty times a second.

    interval: 50,

Get the cross-browser normalized URL fragment, either from the URL, -the hash, or the override.

    getFragment : function(fragment, forcePushState) {
+the hash, or the override.

    getFragment: function(fragment, forcePushState) {
       if (fragment == null) {
         if (this._hasPushState || forcePushState) {
           fragment = window.location.pathname;
@@ -484,13 +530,14 @@
           fragment = window.location.hash;
         }
       }
-      fragment = decodeURIComponent(fragment.replace(hashStrip, ''));
+      fragment = decodeURIComponent(fragment.replace(routeStripper, ''));
       if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
       return fragment;
     },

Start the hash change handling, returning true if the current URL matches -an existing route, and false otherwise.

    start : function(options) {

Figure out the initial configuration. Do we need an iframe? +an existing route, and false otherwise.

    start: function(options) {

Figure out the initial configuration. Do we need an iframe? Is pushState desired ... is it available?

      if (historyStarted) throw new Error("Backbone.history has already been started");
       this.options          = _.extend({}, {root: '/'}, this.options, options);
+      this._wantsHashChange = this.options.hashChange !== false;
       this._wantsPushState  = !!this.options.pushState;
       this._hasPushState    = !!(this.options.pushState && window.history && window.history.pushState);
       var fragment          = this.getFragment();
@@ -502,39 +549,44 @@
       }

Depending on whether we're using pushState or hashes, and whether 'onhashchange' is supported, determine how we check the URL state.

      if (this._hasPushState) {
         $(window).bind('popstate', this.checkUrl);
-      } else if ('onhashchange' in window && !oldIE) {
+      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
         $(window).bind('hashchange', this.checkUrl);
-      } else {
-        setInterval(this.checkUrl, this.interval);
+      } else if (this._wantsHashChange) {
+        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
       }

Determine if we need to change the base url, for a pushState link opened by a non-pushState browser.

      this.fragment = fragment;
       historyStarted = true;
       var loc = window.location;
-      var atRoot  = loc.pathname == this.options.root;
-      if (this._wantsPushState && !this._hasPushState && !atRoot) {
+      var atRoot  = loc.pathname == this.options.root;

If we've started off with a route from a pushState-enabled browser, +but we're currently in a browser that doesn't support it...

      if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
         this.fragment = this.getFragment(null, true);
-        window.location.replace(this.options.root + '#' + this.fragment);

Return immediately as browser will do redirect to new url

        return true;
-      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
-        this.fragment = loc.hash.replace(hashStrip, '');
+        window.location.replace(this.options.root + '#' + this.fragment);

Return immediately as browser will do redirect to new url

        return true;

Or if we've started out with a hash-based route, but we're currently +in a browser where it could be pushState-based instead...

      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
+        this.fragment = loc.hash.replace(routeStripper, '');
         window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
       }
 
       if (!this.options.silent) {
         return this.loadUrl();
       }
-    },

Add a route to be tested when the fragment changes. Routes added later may -override previous routes.

    route : function(route, callback) {
-      this.handlers.unshift({route : route, callback : callback});
-    },

Checks the current URL to see if it has changed, and if it has, -calls loadUrl, normalizing across the hidden iframe.

    checkUrl : function(e) {
+    },

Disable Backbone.history, perhaps temporarily. Not useful in a real app, +but possibly useful for unit testing Routers.

    stop: function() {
+      $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
+      clearInterval(this._checkUrlInterval);
+      historyStarted = false;
+    },

Add a route to be tested when the fragment changes. Routes added later +may override previous routes.

    route: function(route, callback) {
+      this.handlers.unshift({route: route, callback: callback});
+    },

Checks the current URL to see if it has changed, and if it has, +calls loadUrl, normalizing across the hidden iframe.

    checkUrl: function(e) {
       var current = this.getFragment();
       if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
       if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
       if (this.iframe) this.navigate(current);
       this.loadUrl() || this.loadUrl(window.location.hash);
-    },

Attempt to load the current URL fragment. If a route succeeds with a + },

Attempt to load the current URL fragment. If a route succeeds with a match, returns true. If no defined routes matches the fragment, -returns false.

    loadUrl : function(fragmentOverride) {
+returns false.

    loadUrl: function(fragmentOverride) {
       var fragment = this.fragment = this.getFragment(fragmentOverride);
       var matched = _.any(this.handlers, function(handler) {
         if (handler.route.test(fragment)) {
@@ -543,87 +595,80 @@
         }
       });
       return matched;
-    },

Save a fragment into the hash history, or replace the URL state -if the 'replace' option is passed. You are responsible for properly -URL-encoding the fragment in advance. This does not trigger -a hashchange event. -parameters:

+ },

Save a fragment into the hash history, or replace the URL state if the +'replace' option is passed. You are responsible for properly URL-encoding +the fragment in advance.

-
    -
  • fragment: the URL fragment to navigate to (the portion after the '#')
  • -
  • options: An object with the following parameters: - - trigger: call the route corresponding to the provided fragment - - replace: Navigate such that the back button will - not return to this current state.

    - -
       To comply with earlier API specifications, passing
    -   true/false for options will be interpretted as
    -   {options: trigger: true/false}
    -
  • -
    navigate : function(fragment, options) {
-      if (!options || typeof options === 'boolean') options = {trigger: options};
-      var frag = (fragment || '').replace(hashStrip, '');
-      if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
-      if (this._hasPushState) {
+

The options object can contain trigger: true if you wish to have the +route callback be fired (not usually desirable), or replace: true, if +you which to modify the current URL without adding an entry to the history.

    navigate: function(fragment, options) {
+      if (!historyStarted) return false;
+      if (!options || options === true) options = {trigger: options};
+      var frag = (fragment || '').replace(routeStripper, '');
+      if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;

If pushState is available, we use it to set the fragment as a real URL.

      if (this._hasPushState) {
         if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
         this.fragment = frag;
-        if (options.replace) {
-          window.history.replaceState({}, document.title, frag);
-        } else {
-          window.history.pushState({}, document.title, frag);
-        }
-      } else {
+        window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);

If hash changes haven't been explicitly disabled, update the hash +fragment to store history.

      } else if (this._wantsHashChange) {
         this.fragment = frag;
-        this._updateLocationHash(window.location, frag, options.replace);
-        if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {

Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change. + this._updateHash(window.location, frag, options.replace); + if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {

Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change. When replace is true, we don't want this.

          if(!options.replace) this.iframe.document.open().close();
-          this._updateLocationHash(this.iframe.location, frag, options.replace);
-        }
+          this._updateHash(this.iframe.location, frag, options.replace);
+        }

If you've told us that you explicitly don't want fallback hashchange- +based history, then navigate becomes a page refresh.

      } else {
+        window.location.assign(this.options.root + fragment);
       }
       if (options.trigger) this.loadUrl(fragment);
-    },

Since you can't run location.replace on a hash fragment, this -helper function provides an effective work-around.

    _updateLocationHash: function(location, new_fragment, replace) {
-      if (replace)
-        location.replace(location.toString().replace(/#.*$/, "") + "#" + new_fragment);
-      else
-        location.hash = new_fragment;
+    },

Update the hash location, either replacing the current entry, or adding +a new one to the browser history.

    _updateHash: function(location, fragment, replace) {
+      if (replace) {
+        location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
+      } else {
+        location.hash = fragment;
+      }
     }
-  });

Backbone.View

Creating a Backbone.View creates its initial element outside of the DOM, + });

Backbone.View

Creating a Backbone.View creates its initial element outside of the DOM, if an existing element is not provided...

  Backbone.View = function(options) {
     this.cid = _.uniqueId('view');
     this._configure(options || {});
     this._ensureElement();
-    this.delegateEvents();
     this.initialize.apply(this, arguments);
-  };

Element lookup, scoped to DOM elements within the current view. -This should be prefered to global lookups, if you're dealing with -a specific view.

  var selectorDelegate = function(selector) {
-    return $(selector, this.el);
-  };

Cached regex to split keys for delegate.

  var eventSplitter = /^(\S+)\s*(.*)$/;

List of view options to be merged as properties.

  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];

Set up all inheritable Backbone.View properties and methods.

  _.extend(Backbone.View.prototype, Backbone.Events, {

The default tagName of a View's element is "div".

    tagName : 'div',

Attach the selectorDelegate function as the $ property.

    $       : selectorDelegate,

Initialize is an empty function by default. Override it with your own -initialization logic.

    initialize : function(){},

render is the core function that your view should override, in order + this.delegateEvents(); + };

Cached regex to split keys for delegate.

  var eventSplitter = /^(\S+)\s*(.*)$/;

List of view options to be merged as properties.

  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];

Set up all inheritable Backbone.View properties and methods.

  _.extend(Backbone.View.prototype, Backbone.Events, {

The default tagName of a View's element is "div".

    tagName: 'div',

jQuery delegate for element lookup, scoped to DOM elements within the +current view. This should be prefered to global lookups where possible.

    $: function(selector) {
+      return this.$el.find(selector);
+    },

Initialize is an empty function by default. Override it with your own +initialization logic.

    initialize: function(){},

render is the core function that your view should override, in order to populate its element (this.el), with the appropriate HTML. The -convention is for render to always return this.

    render : function() {
+convention is for render to always return this.

    render: function() {
       return this;
-    },

Remove this view from the DOM. Note that the view isn't present in the -DOM by default, so calling this method may be a no-op.

    remove : function() {
-      $(this.el).remove();
+    },

Remove this view from the DOM. Note that the view isn't present in the +DOM by default, so calling this method may be a no-op.

    remove: function() {
+      this.$el.remove();
       return this;
-    },

For small amounts of DOM Elements, where a full-blown template isn't + },

For small amounts of DOM Elements, where a full-blown template isn't needed, use make to manufacture elements, one at a time.

var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
-
    make : function(tagName, attributes, content) {
+
    make: function(tagName, attributes, content) {
       var el = document.createElement(tagName);
       if (attributes) $(el).attr(attributes);
       if (content) $(el).html(content);
       return el;
-    },

Set callbacks, where this.events is a hash of

+ },

Change the view's element (this.el property), including event +re-delegation.

    setElement: function(element, delegate) {
+      this.$el = $(element);
+      this.el = this.$el[0];
+      if (delegate !== false) this.delegateEvents();
+    },

Set callbacks, where this.events is a hash of

{"event selector": "callback"}

{
   'mousedown .title':  'edit',
   'click .button':     'save'
+  'click .open':       function(e) { ... }
 }
 
@@ -631,59 +676,61 @@ Uses event delegation for efficiency. Omitting the selector binds the event to this.el. This only works for delegate-able events: not focus, blur, and -not change, submit, and reset in Internet Explorer.

    delegateEvents : function(events) {
-      if (!(events || (events = this.events))) return;
-      if (_.isFunction(events)) events = events.call(this);
+not change, submit, and reset in Internet Explorer.

    delegateEvents: function(events) {
+      if (!(events || (events = getValue(this, 'events')))) return;
       this.undelegateEvents();
       for (var key in events) {
-        var method = this[events[key]];
+        var method = events[key];
+        if (!_.isFunction(method)) method = this[events[key]];
         if (!method) throw new Error('Event "' + events[key] + '" does not exist');
         var match = key.match(eventSplitter);
         var eventName = match[1], selector = match[2];
         method = _.bind(method, this);
         eventName += '.delegateEvents' + this.cid;
         if (selector === '') {
-          $(this.el).bind(eventName, method);
+          this.$el.bind(eventName, method);
         } else {
-          $(this.el).delegate(selector, eventName, method);
+          this.$el.delegate(selector, eventName, method);
         }
       }
-    },

Clears all callbacks previously bound to the view with delegateEvents.

    undelegateEvents : function() {
-      $(this.el).unbind('.delegateEvents' + this.cid);
-    },

Performs the initial configuration of a View with a set of options. + },

Clears all callbacks previously bound to the view with delegateEvents. +You usually don't need to use this, but may wish to if you have multiple +Backbone views attached to the same DOM element.

    undelegateEvents: function() {
+      this.$el.unbind('.delegateEvents' + this.cid);
+    },

Performs the initial configuration of a View with a set of options. Keys with special meaning (model, collection, id, className), are -attached directly to the view.

    _configure : function(options) {
+attached directly to the view.

    _configure: function(options) {
       if (this.options) options = _.extend({}, this.options, options);
       for (var i = 0, l = viewOptions.length; i < l; i++) {
         var attr = viewOptions[i];
         if (options[attr]) this[attr] = options[attr];
       }
       this.options = options;
-    },

Ensure that the View has a DOM element to render into. + },

Ensure that the View has a DOM element to render into. If this.el is a string, pass it through $(), take the first matching element, and re-assign it to el. Otherwise, create -an element from the id, className and tagName properties.

    _ensureElement : function() {
+an element from the id, className and tagName properties.

    _ensureElement: function() {
       if (!this.el) {
-        var attrs = this.attributes || {};
+        var attrs = getValue(this, 'attributes') || {};
         if (this.id) attrs.id = this.id;
         if (this.className) attrs['class'] = this.className;
-        this.el = this.make(this.tagName, attrs);
-      } else if (_.isString(this.el)) {
-        this.el = $(this.el).get(0);
+        this.setElement(this.make(this.tagName, attrs), false);
+      } else {
+        this.setElement(this.el, false);
       }
     }
 
-  });

The self-propagating extend function that Backbone classes use.

  var extend = function (protoProps, classProps) {
+  });

The self-propagating extend function that Backbone classes use.

  var extend = function (protoProps, classProps) {
     var child = inherits(this, protoProps, classProps);
     child.extend = this.extend;
     return child;
-  };

Set up inheritance for the model, collection, and view.

  Backbone.Model.extend = Backbone.Collection.extend =
-    Backbone.Router.extend = Backbone.View.extend = extend;

Map from CRUD to HTTP for our default Backbone.sync implementation.

  var methodMap = {
+  };

Set up inheritance for the model, collection, and view.

  Backbone.Model.extend = Backbone.Collection.extend =
+    Backbone.Router.extend = Backbone.View.extend = extend;

Backbone.sync

Map from CRUD to HTTP for our default Backbone.sync implementation.

  var methodMap = {
     'create': 'POST',
     'update': 'PUT',
     'delete': 'DELETE',
-    'read'  : 'GET'
-  };

Backbone.sync

Override this function to change the manner in which Backbone persists + 'read': 'GET' + };

Override this function to change the manner in which Backbone persists models to the server. You will be passed the type of request, and the model in question. By default, makes a RESTful Ajax request to the model's url(). Some possible customizations could be:

@@ -696,19 +743,19 @@

Turn on Backbone.emulateHTTP in order to send PUT and DELETE requests as POST, with a _method parameter containing the true HTTP method, -as well as all requests with the body as application/x-www-form-urlencoded instead of -application/json with the model in a param named model. +as well as all requests with the body as application/x-www-form-urlencoded +instead of application/json with the model in a param named model. Useful when interfacing with server-side languages like PHP that make it difficult to read the body of PUT requests.

  Backbone.sync = function(method, model, options) {
-    var type = methodMap[method];

Default JSON-request options.

    var params = {type : type, dataType : 'json'};

Ensure that we have a URL.

    if (!options.url) {
-      params.url = getUrl(model) || urlError();
-    }

Ensure that we have the appropriate request data.

    if (!options.data && model && (method == 'create' || method == 'update')) {
+    var type = methodMap[method];

Default JSON-request options.

    var params = {type: type, dataType: 'json'};

Ensure that we have a URL.

    if (!options.url) {
+      params.url = getValue(model, 'url') || urlError();
+    }

Ensure that we have the appropriate request data.

    if (!options.data && model && (method == 'create' || method == 'update')) {
       params.contentType = 'application/json';
       params.data = JSON.stringify(model.toJSON());
-    }

For older servers, emulate JSON by encoding the request into an HTML-form.

    if (Backbone.emulateJSON) {
+    }

For older servers, emulate JSON by encoding the request into an HTML-form.

    if (Backbone.emulateJSON) {
       params.contentType = 'application/x-www-form-urlencoded';
-      params.data = params.data ? {model : params.data} : {};
-    }

For older servers, emulate HTTP by mimicking the HTTP method with _method + params.data = params.data ? {model: params.data} : {}; + }

For older servers, emulate HTTP by mimicking the HTTP method with _method And an X-HTTP-Method-Override header.

    if (Backbone.emulateHTTP) {
       if (type === 'PUT' || type === 'DELETE') {
         if (Backbone.emulateJSON) params.data._method = type;
@@ -717,39 +764,39 @@
           xhr.setRequestHeader('X-HTTP-Method-Override', type);
         };
       }
-    }

Don't process data on a non-GET request.

    if (params.type !== 'GET' && !Backbone.emulateJSON) {
+    }

Don't process data on a non-GET request.

    if (params.type !== 'GET' && !Backbone.emulateJSON) {
       params.processData = false;
-    }

Make the request, allowing the user to override any Ajax options.

    return $.ajax(_.extend(params, options));
-  };

Helpers

Shared empty constructor function to aid in prototype-chain creation.

  var ctor = function(){};

Helper function to correctly set up the prototype chain, for subclasses. + }

Make the request, allowing the user to override any Ajax options.

    return $.ajax(_.extend(params, options));
+  };

Wrap an optional error callback with a fallback error event.

  Backbone.wrapError = function(onError, originalModel, options) {
+    return function(model, resp) {
+      var resp = model === originalModel ? resp : model;
+      if (onError) {
+        onError(model, resp, options);
+      } else {
+        originalModel.trigger('error', model, resp, options);
+      }
+    };
+  };

Helpers

Shared empty constructor function to aid in prototype-chain creation.

  var ctor = function(){};

Helper function to correctly set up the prototype chain, for subclasses. Similar to goog.inherits, but uses a hash of prototype properties and class properties to be extended.

  var inherits = function(parent, protoProps, staticProps) {
-    var child;

The constructor function for the new subclass is either defined by you + var child;

The constructor function for the new subclass is either defined by you (the "constructor" property in your extend definition), or defaulted -by us to simply call super().

    if (protoProps && protoProps.hasOwnProperty('constructor')) {
+by us to simply call the parent's constructor.

    if (protoProps && protoProps.hasOwnProperty('constructor')) {
       child = protoProps.constructor;
     } else {
-      child = function(){ return parent.apply(this, arguments); };
-    }

Inherit class (static) properties from parent.

    _.extend(child, parent);

Set the prototype chain to inherit from parent, without calling + child = function(){ parent.apply(this, arguments); }; + }

Inherit class (static) properties from parent.

    _.extend(child, parent);

Set the prototype chain to inherit from parent, without calling parent's constructor function.

    ctor.prototype = parent.prototype;
-    child.prototype = new ctor();

Add prototype properties (instance properties) to the subclass, -if supplied.

    if (protoProps) _.extend(child.prototype, protoProps);

Add static properties to the constructor function, if supplied.

    if (staticProps) _.extend(child, staticProps);

Correctly set child's prototype.constructor.

    child.prototype.constructor = child;

Set a convenience property in case the parent's prototype is needed later.

    child.__super__ = parent.prototype;
+    child.prototype = new ctor();

Add prototype properties (instance properties) to the subclass, +if supplied.

    if (protoProps) _.extend(child.prototype, protoProps);

Add static properties to the constructor function, if supplied.

    if (staticProps) _.extend(child, staticProps);

Correctly set child's prototype.constructor.

    child.prototype.constructor = child;

Set a convenience property in case the parent's prototype is needed later.

    child.__super__ = parent.prototype;
 
     return child;
-  };

Helper function to get a URL from a Model or Collection as a property -or as a function.

  var getUrl = function(object) {
-    if (!(object && object.url)) return null;
-    return _.isFunction(object.url) ? object.url() : object.url;
-  };

Throw an error when a URL is needed, and none is supplied.

  var urlError = function() {
+  };

Helper function to get a value from a Backbone object as a property +or as a function.

  var getValue = function(object, prop) {
+    if (!(object && object[prop])) return null;
+    return _.isFunction(object[prop]) ? object[prop]() : object[prop];
+  };

Throw an error when a URL is needed, and none is supplied.

  var urlError = function() {
     throw new Error('A "url" property or function must be specified');
-  };

Wrap an optional error callback with a fallback error event.

  var wrapError = function(onError, originalModel, options) {
-    return function(model, resp) {
-      var resp = model === originalModel ? resp : model;
-      if (onError) {
-        onError(model, resp, options);
-      } else {
-        originalModel.trigger('error', model, resp, options);
-      }
-    };
   };
 
 }).call(this);
diff --git a/docs/todos.html b/docs/todos.html
index 281da8453..50a5940c8 100644
--- a/docs/todos.html
+++ b/docs/todos.html
@@ -82,7 +82,7 @@
     },

Add a single todo item to the list by creating a view for it, and appending its element to the <ul>.

    addOne: function(todo) {
       var view = new TodoView({model: todo});
-      this.$("#todo-list").append(view.render().el);
+      $("#todo-list").append(view.render().el);
     },

Add all items in the Todos collection at once.

    addAll: function() {
       Todos.each(this.addOne);
     },

If you hit return in the main input field, and there is text to save, diff --git a/index.html b/index.html index d5ce9dcb5..dd5535c43 100644 --- a/index.html +++ b/index.html @@ -83,9 +83,16 @@ } div.container ul { list-style: circle; - font-size: 12px; padding-left: 15px; + font-size: 13px; + line-height: 18px; } + div.container ul li { + margin-bottom: 10px; + } + div.container ul.small { + font-size: 12px; + } a, a:visited { color: #444; } @@ -191,19 +198,23 @@