vendor/assets/javascripts/backbone.js in character-1.0.2 vs vendor/assets/javascripts/backbone.js in character-1.0.3

- old
+ new

@@ -1,51 +1,53 @@ -// Backbone.js 1.1.0 +// Backbone.js 1.1.2 -// (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc. -// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org -(function(){ +(function(root, factory) { + // Set up Backbone appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + + // Next for Node.js or CommonJS. jQuery may not be needed as a module. + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'); + factory(root, exports, _); + + // Finally, as a browser global. + } else { + root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); + } + +}(this, function(root, Backbone, _, $) { + // Initial Setup // ------------- - // Save a reference to the global object (`window` in the browser, `exports` - // 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 local references to array methods we'll want to use later. var array = []; var push = array.push; var slice = array.slice; var splice = array.splice; - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both the browser and the server. - 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 = '1.1.0'; + Backbone.VERSION = '1.1.2'; - // 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, Ender, or My Library (kidding) owns // the `$` variable. - Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + Backbone.$ = $; // 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; @@ -107,11 +109,11 @@ // callbacks for all events. off: function(name, callback, context) { var retain, ev, events, names, i, l, j, k; if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; if (!name && !callback && !context) { - this._events = {}; + this._events = void 0; return this; } names = name ? [name] : _.keys(this._events); for (i = 0, l = names.length; i < l; i++) { name = names[i]; @@ -203,11 +205,11 @@ switch (args.length) { case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; } }; var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; @@ -348,21 +350,22 @@ unset ? delete current[attr] : current[attr] = val; } // Trigger all relevant attribute changes. if (!silent) { - if (changes.length) this._pending = true; + if (changes.length) this._pending = options; for (var i = 0, l = changes.length; i < l; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. if (changing) return this; if (!silent) { while (this._pending) { + options = this._pending; this._pending = false; this.trigger('change', this, options); } } this._pending = false; @@ -526,13 +529,16 @@ // 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 = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); + var base = + _.result(this, 'urlRoot') || + _.result(this.collection, 'url') || + urlError(); if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); + return base.replace(/([^\/])$/, '$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, options) { @@ -544,11 +550,11 @@ 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; + return !this.has(this.idAttribute); }, // Check if the model is currently in a valid state. isValid: function(options) { return this._validate({}, _.extend(options || {}, { validate: true })); @@ -648,11 +654,11 @@ this.length--; if (!options.silent) { options.index = index; model.trigger('remove', model, this, options); } - this._removeReference(model); + this._removeReference(model, options); } return singular ? models[0] : models; }, // Update a collection by `set`-ing a new list of models, adding new ones, @@ -674,15 +680,15 @@ var order = !sortable && add && remove ? [] : false; // Turn bare objects into model references, and prevent invalid models // from being added. for (i = 0, l = models.length; i < l; i++) { - attrs = models[i]; + attrs = models[i] || {}; if (attrs instanceof Model) { id = model = attrs; } else { - id = attrs[targetModel.prototype.idAttribute]; + id = attrs[targetModel.prototype.idAttribute || 'id']; } // If a duplicate is found, prevent it from being added and // optionally merge it into the existing model. if (existing = this.get(id)) { @@ -698,18 +704,17 @@ // If this is a new, valid model, push it to the `toAdd` list. } else if (add) { model = models[i] = this._prepareModel(attrs, options); if (!model) continue; toAdd.push(model); - - // Listen to added models' events, and index models for lookup by - // `id` and by `cid`. - model.on('all', this._onModelEvent, this); - this._byId[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; + this._addReference(model, options); } - if (order) order.push(existing || model); + + // Do not add multiple models with the same `id`. + model = existing || model; + if (order && (model.isNew() || !modelMap[model.id])) order.push(model); + modelMap[model.id] = true; } // Remove nonexistent models if appropriate. if (remove) { for (i = 0, l = this.length; i < l; ++i) { @@ -743,11 +748,11 @@ for (i = 0, l = toAdd.length; i < l; i++) { (model = toAdd[i]).trigger('add', model, this, options); } if (sort || (order && order.length)) this.trigger('sort', this, options); } - + // Return the added (or merged) model (or models). return singular ? models[0] : models; }, // When you have more items than you want to add or remove individually, @@ -755,11 +760,11 @@ // any granular `add` or `remove` events. Fires `reset` when finished. // Useful for bulk operations and optimizations. reset: function(models, options) { options || (options = {}); for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i]); + this._removeReference(this.models[i], options); } options.previousModels = this.models; this._reset(); models = this.add(models, _.extend({silent: true}, options)); if (!options.silent) this.trigger('reset', this, options); @@ -796,11 +801,11 @@ }, // Get a model from the set by id. get: function(obj) { if (obj == null) return void 0; - return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; + return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; }, // Get the model at the given index. at: function(index) { return this.models[index]; @@ -872,11 +877,11 @@ options = options ? _.clone(options) : {}; if (!(model = this._prepareModel(model, options))) return false; if (!options.wait) this.add(model, options); var collection = this; var success = options.success; - options.success = function(model, resp, options) { + options.success = function(model, resp) { if (options.wait) collection.add(model, options); if (success) success(model, resp, options); }; model.save(null, options); return model; @@ -902,24 +907,29 @@ }, // Prepare a hash of attributes (or other model) to be added to this // collection. _prepareModel: function(attrs, options) { - if (attrs instanceof Model) { - if (!attrs.collection) attrs.collection = this; - return attrs; - } + if (attrs instanceof Model) return attrs; options = options ? _.clone(options) : {}; options.collection = this; var model = new this.model(attrs, options); if (!model.validationError) return model; this.trigger('invalid', this, model.validationError, options); return false; }, + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + if (!model.collection) model.collection = this; + model.on('all', this._onModelEvent, this); + }, + // Internal method to sever a model's ties to a collection. - _removeReference: function(model) { + _removeReference: function(model, options) { if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); }, // Internal method called every time a model in the set fires an event. @@ -944,11 +954,11 @@ var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', - 'lastIndexOf', 'isEmpty', 'chain']; + 'lastIndexOf', 'isEmpty', 'chain', 'sample']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { Collection.prototype[method] = function() { var args = slice.call(arguments); @@ -956,11 +966,11 @@ return _[method].apply(_, args); }; }); // Underscore methods that take a property name as an argument. - var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; // Use attributes instead of properties. _.each(attributeMethods, function(method) { Collection.prototype[method] = function(value, context) { var iterator = _.isFunction(value) ? value : function(model) { @@ -1178,11 +1188,13 @@ var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); model.trigger('request', model, xhr, options); return xhr; }; - var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); + var noXhrPatch = + typeof window !== 'undefined' && !!window.ActiveXObject && + !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); // Map from CRUD to HTTP for our default `Backbone.sync` implementation. var methodMap = { 'create': 'POST', 'update': 'PUT', @@ -1237,18 +1249,24 @@ } if (!callback) callback = this[name]; var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); - callback && callback.apply(router, args); + router.execute(callback, args); router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger('route', name, args); Backbone.history.trigger('route', router, name, args); }); return this; }, + // Execute a route handler with the provided parameters. This is an + // excellent place to do pre-route setup or post-route cleanup. + execute: function(callback, args) { + if (callback) callback.apply(this, args); + }, + // Simple proxy to `Backbone.history` to save a fragment into the history. navigate: function(fragment, options) { Backbone.history.navigate(fragment, options); return this; }, @@ -1269,22 +1287,24 @@ // against the current location hash. _routeToRegExp: function(route) { route = route.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { - return optional ? match : '([^\/]+)'; + return optional ? match : '([^/?]+)'; }) - .replace(splatParam, '(.*?)'); - return new RegExp('^' + route + '$'); + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); }, // Given a route, and a URL fragment that it matches, return the array of // extracted decoded parameters. Empty or unmatched parameters will be // treated as `null` to normalize cross-browser behavior. _extractParameters: function(route, fragment) { var params = route.exec(fragment).slice(1); - return _.map(params, function(param) { + return _.map(params, function(param, i) { + // Don't decode the search params. + if (i === params.length - 1) return param || null; return param ? decodeURIComponent(param) : null; }); } }); @@ -1318,12 +1338,12 @@ var isExplorer = /msie [\w.]+/; // Cached regex for removing a trailing slash. var trailingSlash = /\/$/; - // Cached regex for stripping urls of hash and query. - var pathStripper = /[?#].*$/; + // Cached regex for stripping urls of hash. + var pathStripper = /#.*$/; // Has the history handling already been started? History.started = false; // Set up all inheritable **Backbone.History** properties and methods. @@ -1331,10 +1351,15 @@ // The default interval to poll for hash changes, if necessary, is // twenty times a second. interval: 50, + // Are we at the app root? + atRoot: function() { + return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; + }, + // Gets the true hash value. Cannot use location.hash directly due to bug // in Firefox where location.hash will always be decoded. getHash: function(window) { var match = (window || this).location.href.match(/#(.*)$/); return match ? match[1] : ''; @@ -1343,11 +1368,11 @@ // 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 || !this._wantsHashChange || forcePushState) { - fragment = this.location.pathname; + fragment = decodeURI(this.location.pathname + this.location.search); var root = this.root.replace(trailingSlash, ''); if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); } else { fragment = this.getHash(); } @@ -1374,11 +1399,12 @@ // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); if (oldIE && this._wantsHashChange) { - this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; + var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">'); + this.iframe = frame.hide().appendTo('body')[0].contentWindow; this.navigate(fragment); } // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. @@ -1392,29 +1418,28 @@ // Determine if we need to change the base url, for a pushState link // opened by a non-pushState browser. this.fragment = fragment; var loc = this.location; - var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; // Transition from hashChange to pushState or vice versa if both are // requested. if (this._wantsHashChange && this._wantsPushState) { // 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._hasPushState && !atRoot) { + if (!this._hasPushState && !this.atRoot()) { this.fragment = this.getFragment(null, true); - this.location.replace(this.root + this.location.search + '#' + this.fragment); + this.location.replace(this.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._hasPushState && atRoot && loc.hash) { + } else if (this._hasPushState && this.atRoot() && loc.hash) { this.fragment = this.getHash().replace(routeStripper, ''); - this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); + this.history.replaceState({}, document.title, this.root + this.fragment); } } if (!this.options.silent) return this.loadUrl(); @@ -1422,11 +1447,11 @@ // Disable Backbone.history, perhaps temporarily. Not useful in a real app, // but possibly useful for unit testing Routers. stop: function() { Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); - clearInterval(this._checkUrlInterval); + if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); History.started = false; }, // Add a route to be tested when the fragment changes. Routes added later // may override previous routes. @@ -1470,11 +1495,11 @@ if (!History.started) return false; if (!options || options === true) options = {trigger: !!options}; var url = this.root + (fragment = this.getFragment(fragment || '')); - // Strip the fragment of the query and hash for matching. + // Strip the hash for matching. fragment = fragment.replace(pathStripper, ''); if (this.fragment === fragment) return; this.fragment = fragment; @@ -1576,6 +1601,8 @@ if (error) error(model, resp, options); model.trigger('error', model, resp, options); }; }; -}).call(this); + return Backbone; + +})); \ No newline at end of file