diff --git a/backbone.js b/backbone.js index 74bde79b0..c2e561892 100644 --- a/backbone.js +++ b/backbone.js @@ -1,4 +1,4 @@ -// Backbone.js 0.3.3 +// Backbone.js 0.5.0-pre // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: @@ -25,7 +25,7 @@ } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.3.3'; + Backbone.VERSION = '0.5.0-pre'; // Require Underscore, if we're on the server, and it's not already present. var _ = root._; @@ -70,7 +70,7 @@ // Passing `"all"` will bind the callback to all events fired. bind : function(ev, callback) { var calls = this._callbacks || (this._callbacks = {}); - var list = this._callbacks[ev] || (this._callbacks[ev] = []); + var list = calls[ev] || (calls[ev] = []); list.push(callback); return this; }, @@ -410,13 +410,10 @@ // 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; - delete options.comparator; - } + if (options.comparator) this.comparator = options.comparator; _.bindAll(this, '_onModelEvent', '_removeReference'); this._reset(); - if (models) this.refresh(models, {silent: true}); + if (models) this.reset(models, {silent: true}); this.initialize(models, options); }; @@ -485,7 +482,7 @@ options || (options = {}); if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh', this, options); + if (!options.silent) this.trigger('reset', this, options); return this; }, @@ -495,27 +492,27 @@ }, // When you have more items than you want to add or remove individually, - // you can refresh the entire set with a new list of models, without firing - // any `added` or `removed` events. Fires `refresh` when finished. - refresh : function(models, options) { + // 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) { models || (models = []); options || (options = {}); this.each(this._removeReference); this._reset(); this.add(models, {silent: true}); - if (!options.silent) this.trigger('refresh', this, options); + if (!options.silent) this.trigger('reset', this, options); return this; }, - // Fetch the default set of models for this collection, refreshing 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 refreshing. + // models to the collection instead of resetting. fetch : function(options) { options || (options = {}); var collection = this; var success = options.success; options.success = function(resp, status, xhr) { - collection[options.add ? 'add' : 'refresh'](collection.parse(resp, xhr), options); + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); if (success) success(collection, resp); }; options.error = wrapError(options.error, collection, options); @@ -532,7 +529,7 @@ if (!model) return false; var success = options.success; options.success = function(nextModel, resp, xhr) { - coll.add(nextModel); + coll.add(nextModel, options); if (success) success(nextModel, resp, xhr); }; model.save(null, options); @@ -584,7 +581,9 @@ 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 = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; + 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++; @@ -646,12 +645,12 @@ }; }); - // Backbone.Controller + // Backbone.Router // ------------------- - // Controllers map faux-URLs to actions, and fire events when routes are + // Routers map faux-URLs to actions, and fire events when routes are // matched. Creating a new one sets its `routes` hash, if not set statically. - Backbone.Controller = function(options) { + Backbone.Router = function(options) { options || (options = {}); if (options.routes) this.routes = options.routes; this._bindRoutes(); @@ -664,8 +663,8 @@ var splatParam = /\*([\w\d]+)/g; var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; - // Set up all inheritable **Backbone.Controller** properties and methods. - _.extend(Backbone.Controller.prototype, Backbone.Events, { + // 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. @@ -680,8 +679,8 @@ route : function(route, name, callback) { Backbone.history || (Backbone.history = new Backbone.History); if (!_.isRegExp(route)) route = this._routeToRegExp(route); - Backbone.history.route(route, _.bind(function(hash) { - var args = this._extractParameters(route, hash); + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); callback.apply(this, args); this.trigger.apply(this, ['route:' + name].concat(args)); }, this)); @@ -689,8 +688,15 @@ // Simple proxy to `Backbone.history` to save a fragment into the history, // without triggering routes. - saveLocation : function(hash) { - Backbone.history.saveLocation(hash); + saveLocation : function(fragment) { + Backbone.history.saveLocation(fragment); + }, + + // Simple proxy to `Backbone.history` to both save a fragment into the + // history and to then load the route at that fragment. + setLocation : function(fragment) { + Backbone.history.saveLocation(fragment); + Backbone.history.loadUrl(fragment); }, // Bind all defined routes to `Backbone.history`. We have to reverse the @@ -718,8 +724,8 @@ // Given a route, and a URL fragment that it matches, return the array of // extracted parameters. - _extractParameters : function(route, hash) { - return route.exec(hash).slice(1); + _extractParameters : function(route, fragment) { + return route.exec(fragment).slice(1); } }); @@ -727,7 +733,7 @@ // Backbone.History // ---------------- - // Handles cross-browser history management, based on URL hashes. If the + // Handles cross-browser history management, based on URL fragments. If the // browser does not support `onhashchange`, falls back to polling. Backbone.History = function() { this.handlers = []; @@ -750,33 +756,64 @@ // twenty times a second. interval: 50, - // Get the cross-browser normalized URL fragment. - getHash : function(loc) { - return (loc || window.location).hash.replace(hashStrip, ''); + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment : function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); + } else { + fragment = window.location.hash; + } + } + return fragment.replace(hashStrip, ''); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. - start : function() { + 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"); - var hash = this.getHash(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); if (oldIE) { this.iframe = $('