// Backbone.js 1.0.023 // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.4 // Backbone may be freely distributed under the MIT license.5 // For all details and documentation:6 // http://backbonejs.org78 (function(){910 // Initial Setup11 // -------------1213 // Save a reference to the global object (`window` in the browser, `exports`14 // on the server).15 var root = this;1617 // Save the previous value of the `Backbone` variable, so that it can be18 // restored later on, if `noConflict` is used.19 var previousBackbone = root.Backbone;2021 // Create local references to array methods we'll want to use later.22 var array = [];23 var push = array.push;24 var slice = array.slice;25 var splice = array.splice;2627 // The top-level namespace. All public Backbone classes and modules will28 // be attached to this. Exported for both the browser and the server.29 var Backbone;30 if (typeof exports !== 'undefined') {31 Backbone = exports;32 } else {33 Backbone = root.Backbone = {};34 }3536 // Current version of the library. Keep in sync with `package.json`.37 Backbone.VERSION = '1.0.0';3839 // Require Underscore, if we're on the server, and it's not already present.40 var _ = root._;41 if (!_ && (typeof require !== 'undefined')) _ = require('underscore');4243 // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns44 // the `$` variable.45 Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;4647 // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable48 // to its previous owner. Returns a reference to this Backbone object.49 Backbone.noConflict = function() {50 root.Backbone = previousBackbone;51 return this;52 };5354 // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option55 // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and56 // set a `X-Http-Method-Override` header.57 Backbone.emulateHTTP = false;5859 // Turn on `emulateJSON` to support legacy servers that can't deal with direct60 // `application/json` requests ... will encode the body as61 // `application/x-www-form-urlencoded` instead and will send the model in a62 // form param named `model`.63 Backbone.emulateJSON = false;6465 // Backbone.Events66 // ---------------6768 // A module that can be mixed in to *any object* in order to provide it with69 // custom events. You may bind with `on` or remove with `off` callback70 // functions to an event; `trigger`-ing an event fires all callbacks in71 // succession.72 //73 // var object = {};74 // _.extend(object, Backbone.Events);75 // object.on('expand', function(){ alert('expanded'); });76 // object.trigger('expand');77 //78 var Events = Backbone.Events = {7980 // Bind an event to a `callback` function. Passing `"all"` will bind81 // the callback to all events fired.82 on: function(name, callback, context) {83 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;84 this._events || (this._events = {});85 var events = this._events[name] || (this._events[name] = []);86 events.push({callback: callback, context: context, ctx: context || this});87 return this;88 },8990 // Bind an event to only be triggered a single time. After the first time91 // the callback is invoked, it will be removed.92 once: function(name, callback, context) {93 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;94 var self = this;95 var once = _.once(function() {96 self.off(name, once);97 callback.apply(this, arguments);98 });99 once._callback = callback;100 return this.on(name, once, context);101 },102103 // Remove one or many callbacks. If `context` is null, removes all104 // callbacks with that function. If `callback` is null, removes all105 // callbacks for the event. If `name` is null, removes all bound106 // callbacks for all events.107 off: function(name, callback, context) {108 var retain, ev, events, names, i, l, j, k;109 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;110 if (!name && !callback && !context) {111 this._events = {};112 return this;113 }114115 names = name ? [name] : _.keys(this._events);116 for (i = 0, l = names.length; i < l; i++) {117 name = names[i];118 if (events = this._events[name]) {119 this._events[name] = retain = [];120 if (callback || context) {121 for (j = 0, k = events.length; j < k; j++) {122 ev = events[j];123 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||124 (context && context !== ev.context)) {125 retain.push(ev);126 }127 }128 }129 if (!retain.length) delete this._events[name];130 }131 }132133 return this;134 },135136 // Trigger one or many events, firing all bound callbacks. Callbacks are137 // passed the same arguments as `trigger` is, apart from the event name138 // (unless you're listening on `"all"`, which will cause your callback to139 // receive the true name of the event as the first argument).140 trigger: function(name) {141 if (!this._events) return this;142 var args = slice.call(arguments, 1);143 if (!eventsApi(this, 'trigger', name, args)) return this;144 var events = this._events[name];145 var allEvents = this._events.all;146 if (events) triggerEvents(events, args);147 if (allEvents) triggerEvents(allEvents, arguments);148 return this;149 },150151 // Tell this object to stop listening to either specific events ... or152 // to every object it's currently listening to.153 stopListening: function(obj, name, callback) {154 var listeners = this._listeners;155 if (!listeners) return this;156 var deleteListener = !name && !callback;157 if (typeof name === 'object') callback = this;158 if (obj) (listeners = {})[obj._listenerId] = obj;159 for (var id in listeners) {160 listeners[id].off(name, callback, this);161 if (deleteListener) delete this._listeners[id];162 }163 return this;164 }165166 };167168 // Regular expression used to split event strings.169 var eventSplitter = /\s+/;170171 // Implement fancy features of the Events API such as multiple event172 // names `"change blur"` and jQuery-style event maps `{change: action}`173 // in terms of the existing API.174 var eventsApi = function(obj, action, name, rest) {175 if (!name) return true;176177 // Handle event maps.178 if (typeof name === 'object') {179 for (var key in name) {180 obj[action].apply(obj, [key, name[key]].concat(rest));181 }182 return false;183 }184185 // Handle space separated event names.186 if (eventSplitter.test(name)) {187 var names = name.split(eventSplitter);188 for (var i = 0, l = names.length; i < l; i++) {189 obj[action].apply(obj, [names[i]].concat(rest));190 }191 return false;192 }193194 return true;195 };196197 // A difficult-to-believe, but optimized internal dispatch function for198 // triggering events. Tries to keep the usual cases speedy (most internal199 // Backbone events have 3 arguments).200 var triggerEvents = function(events, args) {201 var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];202 switch (args.length) {203 case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;204 case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;205 case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;206 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;207 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);208 }209 };210211 var listenMethods = {listenTo: 'on', listenToOnce: 'once'};212213 // Inversion-of-control versions of `on` and `once`. Tell *this* object to214 // listen to an event in another object ... keeping track of what it's215 // listening to.216 _.each(listenMethods, function(implementation, method) {217 Events[method] = function(obj, name, callback) {218 var listeners = this._listeners || (this._listeners = {});219 var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));220 listeners[id] = obj;221 if (typeof name === 'object') callback = this;222 obj[implementation](name, callback, this);223 return this;224 };225 });226227 // Aliases for backwards compatibility.228 Events.bind = Events.on;229 Events.unbind = Events.off;230231 // Allow the `Backbone` object to serve as a global event bus, for folks who232 // want global "pubsub" in a convenient place.233 _.extend(Backbone, Events);234235 // Backbone.Model236 // --------------237238 // Backbone **Models** are the basic data object in the framework --239 // frequently representing a row in a table in a database on your server.240 // A discrete chunk of data and a bunch of useful, related methods for241 // performing computations and transformations on that data.242243 // Create a new model with the specified attributes. A client id (`cid`)244 // is automatically generated and assigned for you.245 var Model = Backbone.Model = function(attributes, options) {246 var defaults;247 var attrs = attributes || {};248 options || (options = {});249 this.cid = _.uniqueId('c');250 this.attributes = {};251 _.extend(this, _.pick(options, modelOptions));252 if (options.parse) attrs = this.parse(attrs, options) || {};253 if (defaults = _.result(this, 'defaults')) {254 attrs = _.defaults({}, attrs, defaults);255 }256 this.set(attrs, options);257 this.changed = {};258 this.initialize.apply(this, arguments);259 };260261 // A list of options to be attached directly to the model, if provided.262 var modelOptions = ['url', 'urlRoot', 'collection'];263264 // Attach all inheritable methods to the Model prototype.265 _.extend(Model.prototype, Events, {266267 // A hash of attributes whose current and previous value differ.268 changed: null,269270 // The value returned during the last failed validation.271 validationError: null,272273 // The default name for the JSON `id` attribute is `"id"`. MongoDB and274 // CouchDB users may want to set this to `"_id"`.275 idAttribute: 'id',276277 // Initialize is an empty function by default. Override it with your own278 // initialization logic.279 initialize: function(){},280281 // Return a copy of the model's `attributes` object.282 toJSON: function(options) {283 return _.clone(this.attributes);284 },285286 // Proxy `Backbone.sync` by default -- but override this if you need287 // custom syncing semantics for *this* particular model.288 sync: function() {289 return Backbone.sync.apply(this, arguments);290 },291292 // Get the value of an attribute.293 get: function(attr) {294 return this.attributes[attr];295 },296297 // Get the HTML-escaped value of an attribute.298 escape: function(attr) {299 return _.escape(this.get(attr));300 },301302 // Returns `true` if the attribute contains a value that is not null303 // or undefined.304 has: function(attr) {305 return this.get(attr) != null;306 },307308 // Set a hash of model attributes on the object, firing `"change"`. This is309 // the core primitive operation of a model, updating the data and notifying310 // anyone who needs to know about the change in state. The heart of the beast.311 set: function(key, val, options) {312 var attr, attrs, unset, changes, silent, changing, prev, current;313 if (key == null) return this;314315 // Handle both `"key", value` and `{key: value}` -style arguments.316 if (typeof key === 'object') {317 attrs = key;318 options = val;319 } else {320 (attrs = {})[key] = val;321 }322323 options || (options = {});324325 // Run validation.326 if (!this._validate(attrs, options)) return false;327328 // Extract attributes and options.329 unset = options.unset;330 silent = options.silent;331 changes = [];332 changing = this._changing;333 this._changing = true;334335 if (!changing) {336 this._previousAttributes = _.clone(this.attributes);337 this.changed = {};338 }339 current = this.attributes, prev = this._previousAttributes;340341 // Check for changes of `id`.342 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];343344 // For each `set` attribute, update or delete the current value.345 for (attr in attrs) {346 val = attrs[attr];347 if (!_.isEqual(current[attr], val)) changes.push(attr);348 if (!_.isEqual(prev[attr], val)) {349 this.changed[attr] = val;350 } else {351 delete this.changed[attr];352 }353 unset ? delete current[attr] : current[attr] = val;354 }355356 // Trigger all relevant attribute changes.357 if (!silent) {358 if (changes.length) this._pending = true;359 for (var i = 0, l = changes.length; i < l; i++) {360 this.trigger('change:' + changes[i], this, current[changes[i]], options);361 }362 }363364 // You might be wondering why there's a `while` loop here. Changes can365 // be recursively nested within `"change"` events.366 if (changing) return this;367 if (!silent) {368 while (this._pending) {369 this._pending = false;370 this.trigger('change', this, options);371 }372 }373 this._pending = false;374 this._changing = false;375 return this;376 },377378 // Remove an attribute from the model, firing `"change"`. `unset` is a noop379 // if the attribute doesn't exist.380 unset: function(attr, options) {381 return this.set(attr, void 0, _.extend({}, options, {unset: true}));382 },383384 // Clear all attributes on the model, firing `"change"`.385 clear: function(options) {386 var attrs = {};387 for (var key in this.attributes) attrs[key] = void 0;388 return this.set(attrs, _.extend({}, options, {unset: true}));389 },390391 // Determine if the model has changed since the last `"change"` event.392 // If you specify an attribute name, determine if that attribute has changed.393 hasChanged: function(attr) {394 if (attr == null) return !_.isEmpty(this.changed);395 return _.has(this.changed, attr);396 },397398 // Return an object containing all the attributes that have changed, or399 // false if there are no changed attributes. Useful for determining what400 // parts of a view need to be updated and/or what attributes need to be401 // persisted to the server. Unset attributes will be set to undefined.402 // You can also pass an attributes object to diff against the model,403 // determining if there *would be* a change.404 changedAttributes: function(diff) {405 if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;406 var val, changed = false;407 var old = this._changing ? this._previousAttributes : this.attributes;408 for (var attr in diff) {409 if (_.isEqual(old[attr], (val = diff[attr]))) continue;410 (changed || (changed = {}))[attr] = val;411 }412 return changed;413 },414415 // Get the previous value of an attribute, recorded at the time the last416 // `"change"` event was fired.417 previous: function(attr) {418 if (attr == null || !this._previousAttributes) return null;419 return this._previousAttributes[attr];420 },421422 // Get all of the attributes of the model at the time of the previous423 // `"change"` event.424 previousAttributes: function() {425 return _.clone(this._previousAttributes);426 },427428 // Fetch the model from the server. If the server's representation of the429 // model differs from its current attributes, they will be overridden,430 // triggering a `"change"` event.431 fetch: function(options) {432 options = options ? _.clone(options) : {};433 if (options.parse === void 0) options.parse = true;434 var model = this;435 var success = options.success;436 options.success = function(resp) {437 if (!model.set(model.parse(resp, options), options)) return false;438 if (success) success(model, resp, options);439 model.trigger('sync', model, resp, options);440 };441 wrapError(this, options);442 return this.sync('read', this, options);443 },444445 // Set a hash of model attributes, and sync the model to the server.446 // If the server returns an attributes hash that differs, the model's447 // state will be `set` again.448 save: function(key, val, options) {449 var attrs, method, xhr, attributes = this.attributes;450451 // Handle both `"key", value` and `{key: value}` -style arguments.452 if (key == null || typeof key === 'object') {453 attrs = key;454 options = val;455 } else {456 (attrs = {})[key] = val;457 }458459 // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.460 if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;461462 options = _.extend({validate: true}, options);463464 // Do not persist invalid models.465 if (!this._validate(attrs, options)) return false;466467 // Set temporary attributes if `{wait: true}`.468 if (attrs && options.wait) {469 this.attributes = _.extend({}, attributes, attrs);470 }471472 // After a successful server-side save, the client is (optionally)473 // updated with the server-side state.474 if (options.parse === void 0) options.parse = true;475 var model = this;476 var success = options.success;477 options.success = function(resp) {478 // Ensure attributes are restored during synchronous saves.479 model.attributes = attributes;480 var serverAttrs = model.parse(resp, options);481 if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);482 if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {483 return false;484 }485 if (success) success(model, resp, options);486 model.trigger('sync', model, resp, options);487 };488 wrapError(this, options);489490 method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');491 if (method === 'patch') options.attrs = attrs;492 xhr = this.sync(method, this, options);493494 // Restore attributes.495 if (attrs && options.wait) this.attributes = attributes;496497 return xhr;498 },499500 // Destroy this model on the server if it was already persisted.501 // Optimistically removes the model from its collection, if it has one.502 // If `wait: true` is passed, waits for the server to respond before removal.503 destroy: function(options) {504 options = options ? _.clone(options) : {};505 var model = this;506 var success = options.success;507508 var destroy = function() {509 model.trigger('destroy', model, model.collection, options);510 };511512 options.success = function(resp) {513 if (options.wait || model.isNew()) destroy();514 if (success) success(model, resp, options);515 if (!model.isNew()) model.trigger('sync', model, resp, options);516 };517518 if (this.isNew()) {519 options.success();520 return false;521 }522 wrapError(this, options);523524 var xhr = this.sync('delete', this, options);525 if (!options.wait) destroy();526 return xhr;527 },528529 // Default URL for the model's representation on the server -- if you're530 // using Backbone's restful methods, override this to change the endpoint531 // that will be called.532 url: function() {533 var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();534 if (this.isNew()) return base;535 return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);536 },537538 // **parse** converts a response into the hash of attributes to be `set` on539 // the model. The default implementation is just to pass the response along.540 parse: function(resp, options) {541 return resp;542 },543544 // Create a new model with identical attributes to this one.545 clone: function() {546 return new this.constructor(this.attributes);547 },548549 // A model is new if it has never been saved to the server, and lacks an id.550 isNew: function() {551 return this.id == null;552 },553554 // Check if the model is currently in a valid state.555 isValid: function(options) {556 return this._validate({}, _.extend(options || {}, { validate: true }));557 },558559 // Run validation against the next complete set of model attributes,560 // returning `true` if all is well. Otherwise, fire an `"invalid"` event.561 _validate: function(attrs, options) {562 if (!options.validate || !this.validate) return true;563 attrs = _.extend({}, this.attributes, attrs);564 var error = this.validationError = this.validate(attrs, options) || null;565 if (!error) return true;566 this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));567 return false;568 }569570 });571572 // Underscore methods that we want to implement on the Model.573 var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];574575 // Mix in each Underscore method as a proxy to `Model#attributes`.576 _.each(modelMethods, function(method) {577 Model.prototype[method] = function() {578 var args = slice.call(arguments);579 args.unshift(this.attributes);580 return _[method].apply(_, args);581 };582 });583584 // Backbone.Collection585 // -------------------586587 // If models tend to represent a single row of data, a Backbone Collection is588 // more analagous to a table full of data ... or a small slice or page of that589 // table, or a collection of rows that belong together for a particular reason590 // -- all of the messages in this particular folder, all of the documents591 // belonging to this particular author, and so on. Collections maintain592 // indexes of their models, both in order, and for lookup by `id`.593594 // Create a new **Collection**, perhaps to contain a specific type of `model`.595 // If a `comparator` is specified, the Collection will maintain596 // its models in sort order, as they're added and removed.597 var Collection = Backbone.Collection = function(models, options) {598 options || (options = {});599 if (options.url) this.url = options.url;600 if (options.model) this.model = options.model;601 if (options.comparator !== void 0) this.comparator = options.comparator;602 this._reset();603 this.initialize.apply(this, arguments);604 if (models) this.reset(models, _.extend({silent: true}, options));605 };606607 // Default options for `Collection#set`.608 var setOptions = {add: true, remove: true, merge: true};609 var addOptions = {add: true, merge: false, remove: false};610611 // Define the Collection's inheritable methods.612 _.extend(Collection.prototype, Events, {613614 // The default model for a collection is just a **Backbone.Model**.615 // This should be overridden in most cases.616 model: Model,617618 // Initialize is an empty function by default. Override it with your own619 // initialization logic.620 initialize: function(){},621622 // The JSON representation of a Collection is an array of the623 // models' attributes.624 toJSON: function(options) {625 return this.map(function(model){ return model.toJSON(options); });626 },627628 // Proxy `Backbone.sync` by default.629 sync: function() {630 return Backbone.sync.apply(this, arguments);631 },632633 // Add a model, or list of models to the set.634 add: function(models, options) {635 return this.set(models, _.defaults(options || {}, addOptions));636 },637638 // Remove a model, or a list of models from the set.639 remove: function(models, options) {640 models = _.isArray(models) ? models.slice() : [models];641 options || (options = {});642 var i, l, index, model;643 for (i = 0, l = models.length; i < l; i++) {644 model = this.get(models[i]);645 if (!model) continue;646 delete this._byId[model.id];647 delete this._byId[model.cid];648 index = this.indexOf(model);649 this.models.splice(index, 1);650 this.length--;651 if (!options.silent) {652 options.index = index;653 model.trigger('remove', model, this, options);654 }655 this._removeReference(model);656 }657 return this;658 },659660 // Update a collection by `set`-ing a new list of models, adding new ones,661 // removing models that are no longer present, and merging models that662 // already exist in the collection, as necessary. Similar to **Model#set**,663 // the core operation for updating the data contained by the collection.664 set: function(models, options) {665 options = _.defaults(options || {}, setOptions);666 if (options.parse) models = this.parse(models, options);667 if (!_.isArray(models)) models = models ? [models] : [];668 var i, l, model, attrs, existing, sort;669 var at = options.at;670 var sortable = this.comparator && (at == null) && options.sort !== false;671 var sortAttr = _.isString(this.comparator) ? this.comparator : null;672 var toAdd = [], toRemove = [], modelMap = {};673674 // Turn bare objects into model references, and prevent invalid models675 // from being added.676 for (i = 0, l = models.length; i < l; i++) {677 if (!(model = this._prepareModel(models[i], options))) continue;678679 // If a duplicate is found, prevent it from being added and680 // optionally merge it into the existing model.681 if (existing = this.get(model)) {682 if (options.remove) modelMap[existing.cid] = true;683 if (options.merge) {684 existing.set(model.attributes, options);685 if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;686 }687688 // This is a new model, push it to the `toAdd` list.689 } else if (options.add) {690 toAdd.push(model);691692 // Listen to added models' events, and index models for lookup by693 // `id` and by `cid`.694 model.on('all', this._onModelEvent, this);695 this._byId[model.cid] = model;696 if (model.id != null) this._byId[model.id] = model;697 }698 }699700 // Remove nonexistent models if appropriate.701 if (options.remove) {702 for (i = 0, l = this.length; i < l; ++i) {703 if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);704 }705 if (toRemove.length) this.remove(toRemove, options);706 }707708 // See if sorting is needed, update `length` and splice in new models.709 if (toAdd.length) {710 if (sortable) sort = true;711 this.length += toAdd.length;712 if (at != null) {713 splice.apply(this.models, [at, 0].concat(toAdd));714 } else {715 push.apply(this.models, toAdd);716 }717 }718719 // Silently sort the collection if appropriate.720 if (sort) this.sort({silent: true});721722 if (options.silent) return this;723724 // Trigger `add` events.725 for (i = 0, l = toAdd.length; i < l; i++) {726 (model = toAdd[i]).trigger('add', model, this, options);727 }728729 // Trigger `sort` if the collection was sorted.730 if (sort) this.trigger('sort', this, options);731 return this;732 },733734 // When you have more items than you want to add or remove individually,735 // you can reset the entire set with a new list of models, without firing736 // any granular `add` or `remove` events. Fires `reset` when finished.737 // Useful for bulk operations and optimizations.738 reset: function(models, options) {739 options || (options = {});740 for (var i = 0, l = this.models.length; i < l; i++) {741 this._removeReference(this.models[i]);742 }743 options.previousModels = this.models;744 this._reset();745 this.add(models, _.extend({silent: true}, options));746 if (!options.silent) this.trigger('reset', this, options);747 return this;748 },749750 // Add a model to the end of the collection.751 push: function(model, options) {752 model = this._prepareModel(model, options);753 this.add(model, _.extend({at: this.length}, options));754 return model;755 },756757 // Remove a model from the end of the collection.758 pop: function(options) {759 var model = this.at(this.length - 1);760 this.remove(model, options);761 return model;762 },763764 // Add a model to the beginning of the collection.765 unshift: function(model, options) {766 model = this._prepareModel(model, options);767 this.add(model, _.extend({at: 0}, options));768 return model;769 },770771 // Remove a model from the beginning of the collection.772 shift: function(options) {773 var model = this.at(0);774 this.remove(model, options);775 return model;776 },777778 // Slice out a sub-array of models from the collection.779 slice: function(begin, end) {780 return this.models.slice(begin, end);781 },782783 // Get a model from the set by id.784 get: function(obj) {785 if (obj == null) return void 0;786 return this._byId[obj.id != null ? obj.id : obj.cid || obj];787 },788789 // Get the model at the given index.790 at: function(index) {791 return this.models[index];792 },793794 // Return models with matching attributes. Useful for simple cases of795 // `filter`.796 where: function(attrs, first) {797 if (_.isEmpty(attrs)) return first ? void 0 : [];798 return this[first ? 'find' : 'filter'](function(model) {799 for (var key in attrs) {800 if (attrs[key] !== model.get(key)) return false;801 }802 return true;803 });804 },805806 // Return the first model with matching attributes. Useful for simple cases807 // of `find`.808 findWhere: function(attrs) {809 return this.where(attrs, true);810 },811812 // Force the collection to re-sort itself. You don't need to call this under813 // normal circumstances, as the set will maintain sort order as each item814 // is added.815 sort: function(options) {816 if (!this.comparator) throw new Error('Cannot sort a set without a comparator');817 options || (options = {});818819 // Run sort based on type of `comparator`.820 if (_.isString(this.comparator) || this.comparator.length === 1) {821 this.models = this.sortBy(this.comparator, this);822 } else {823 this.models.sort(_.bind(this.comparator, this));824 }825826 if (!options.silent) this.trigger('sort', this, options);827 return this;828 },829830 // Figure out the smallest index at which a model should be inserted so as831 // to maintain order.832 sortedIndex: function(model, value, context) {833 value || (value = this.comparator);834 var iterator = _.isFunction(value) ? value : function(model) {835 return model.get(value);836 };837 return _.sortedIndex(this.models, model, iterator, context);838 },839840 // Pluck an attribute from each model in the collection.841 pluck: function(attr) {842 return _.invoke(this.models, 'get', attr);843 },844845 // Fetch the default set of models for this collection, resetting the846 // collection when they arrive. If `reset: true` is passed, the response847 // data will be passed through the `reset` method instead of `set`.848 fetch: function(options) {849 options = options ? _.clone(options) : {};850 if (options.parse === void 0) options.parse = true;851 var success = options.success;852 var collection = this;853 options.success = function(resp) {854 var method = options.reset ? 'reset' : 'set';855 collection[method](resp, options);856 if (success) success(collection, resp, options);857 collection.trigger('sync', collection, resp, options);858 };859 wrapError(this, options);860 return this.sync('read', this, options);861 },862863 // Create a new instance of a model in this collection. Add the model to the864 // collection immediately, unless `wait: true` is passed, in which case we865 // wait for the server to agree.866 create: function(model, options) {867 options = options ? _.clone(options) : {};868 if (!(model = this._prepareModel(model, options))) return false;869 if (!options.wait) this.add(model, options);870 var collection = this;871 var success = options.success;872 options.success = function(resp) {873 if (options.wait) collection.add(model, options);874 if (success) success(model, resp, options);875 };876 model.save(null, options);877 return model;878 },879880 // **parse** converts a response into a list of models to be added to the881 // collection. The default implementation is just to pass it through.882 parse: function(resp, options) {883 return resp;884 },885886 // Create a new collection with an identical list of models as this one.887 clone: function() {888 return new this.constructor(this.models);889 },890891 // Private method to reset all internal state. Called when the collection892 // is first initialized or reset.893 _reset: function() {894 this.length = 0;895 this.models = [];896 this._byId = {};897 },898899 // Prepare a hash of attributes (or other model) to be added to this900 // collection.901 _prepareModel: function(attrs, options) {902 if (attrs instanceof Model) {903 if (!attrs.collection) attrs.collection = this;904 return attrs;905 }906 options || (options = {});907 options.collection = this;908 var model = new this.model(attrs, options);909 if (!model._validate(attrs, options)) {910 this.trigger('invalid', this, attrs, options);911 return false;912 }913 return model;914 },915916 // Internal method to sever a model's ties to a collection.917 _removeReference: function(model) {918 if (this === model.collection) delete model.collection;919 model.off('all', this._onModelEvent, this);920 },921922 // Internal method called every time a model in the set fires an event.923 // Sets need to update their indexes when models change ids. All other924 // events simply proxy through. "add" and "remove" events that originate925 // in other collections are ignored.926 _onModelEvent: function(event, model, collection, options) {927 if ((event === 'add' || event === 'remove') && collection !== this) return;928 if (event === 'destroy') this.remove(model, options);929 if (model && event === 'change:' + model.idAttribute) {930 delete this._byId[model.previous(model.idAttribute)];931 if (model.id != null) this._byId[model.id] = model;932 }933 this.trigger.apply(this, arguments);934 }935936 });937938 // Underscore methods that we want to implement on the Collection.939 // 90% of the core usefulness of Backbone Collections is actually implemented940 // right here:941 var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',942 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',943 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',944 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',945 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',946 'isEmpty', 'chain'];947948 // Mix in each Underscore method as a proxy to `Collection#models`.949 _.each(methods, function(method) {950 Collection.prototype[method] = function() {951 var args = slice.call(arguments);952 args.unshift(this.models);953 return _[method].apply(_, args);954 };955 });956957 // Underscore methods that take a property name as an argument.958 var attributeMethods = ['groupBy', 'countBy', 'sortBy'];959960 // Use attributes instead of properties.961 _.each(attributeMethods, function(method) {962 Collection.prototype[method] = function(value, context) {963 var iterator = _.isFunction(value) ? value : function(model) {964 return model.get(value);965 };966 return _[method](this.models, iterator, context);967 };968 });969970 // Backbone.View971 // -------------972973 // Backbone Views are almost more convention than they are actual code. A View974 // is simply a JavaScript object that represents a logical chunk of UI in the975 // DOM. This might be a single item, an entire list, a sidebar or panel, or976 // even the surrounding frame which wraps your whole app. Defining a chunk of977 // UI as a **View** allows you to define your DOM events declaratively, without978 // having to worry about render order ... and makes it easy for the view to979 // react to specific changes in the state of your models.980981 // Creating a Backbone.View creates its initial element outside of the DOM,982 // if an existing element is not provided...983 var View = Backbone.View = function(options) {984 this.cid = _.uniqueId('view');985 this._configure(options || {});986 this._ensureElement();987 this.initialize.apply(this, arguments);988 this.delegateEvents();989 };990991 // Cached regex to split keys for `delegate`.992 var delegateEventSplitter = /^(\S+)\s*(.*)$/;993994 // List of view options to be merged as properties.995 var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];996997 // Set up all inheritable **Backbone.View** properties and methods.998 _.extend(View.prototype, Events, {9991000 // The default `tagName` of a View's element is `"div"`.1001 tagName: 'div',10021003 // jQuery delegate for element lookup, scoped to DOM elements within the1004 // current view. This should be prefered to global lookups where possible.1005 $: function(selector) {1006 return this.$el.find(selector);1007 },10081009 // Initialize is an empty function by default. Override it with your own1010 // initialization logic.1011 initialize: function(){},10121013 // **render** is the core function that your view should override, in order1014 // to populate its element (`this.el`), with the appropriate HTML. The1015 // convention is for **render** to always return `this`.1016 render: function() {1017 return this;1018 },10191020 // Remove this view by taking the element out of the DOM, and removing any1021 // applicable Backbone.Events listeners.1022 remove: function() {1023 this.$el.remove();1024 this.stopListening();1025 return this;1026 },10271028 // Change the view's element (`this.el` property), including event1029 // re-delegation.1030 setElement: function(element, delegate) {1031 if (this.$el) this.undelegateEvents();1032 this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);1033 this.el = this.$el[0];1034 if (delegate !== false) this.delegateEvents();1035 return this;1036 },10371038 // Set callbacks, where `this.events` is a hash of1039 //1040 // *{"event selector": "callback"}*1041 //1042 // {1043 // 'mousedown .title': 'edit',1044 // 'click .button': 'save'1045 // 'click .open': function(e) { ... }1046 // }1047 //1048 // pairs. Callbacks will be bound to the view, with `this` set properly.1049 // Uses event delegation for efficiency.1050 // Omitting the selector binds the event to `this.el`.1051 // This only works for delegate-able events: not `focus`, `blur`, and1052 // not `change`, `submit`, and `reset` in Internet Explorer.1053 delegateEvents: function(events) {1054 if (!(events || (events = _.result(this, 'events')))) return this;1055 this.undelegateEvents();1056 for (var key in events) {1057 var method = events[key];1058 if (!_.isFunction(method)) method = this[events[key]];1059 if (!method) continue;10601061 var match = key.match(delegateEventSplitter);1062 var eventName = match[1], selector = match[2];1063 method = _.bind(method, this);1064 eventName += '.delegateEvents' + this.cid;1065 if (selector === '') {1066 this.$el.on(eventName, method);1067 } else {1068 this.$el.on(eventName, selector, method);1069 }1070 }1071 return this;1072 },10731074 // Clears all callbacks previously bound to the view with `delegateEvents`.1075 // You usually don't need to use this, but may wish to if you have multiple1076 // Backbone views attached to the same DOM element.1077 undelegateEvents: function() {1078 this.$el.off('.delegateEvents' + this.cid);1079 return this;1080 },10811082 // Performs the initial configuration of a View with a set of options.1083 // Keys with special meaning *(e.g. model, collection, id, className)* are1084 // attached directly to the view. See `viewOptions` for an exhaustive1085 // list.1086 _configure: function(options) {1087 if (this.options) options = _.extend({}, _.result(this, 'options'), options);1088 _.extend(this, _.pick(options, viewOptions));1089 this.options = options;1090 },10911092 // Ensure that the View has a DOM element to render into.1093 // If `this.el` is a string, pass it through `$()`, take the first1094 // matching element, and re-assign it to `el`. Otherwise, create1095 // an element from the `id`, `className` and `tagName` properties.1096 _ensureElement: function() {1097 if (!this.el) {1098 var attrs = _.extend({}, _.result(this, 'attributes'));1099 if (this.id) attrs.id = _.result(this, 'id');1100 if (this.className) attrs['class'] = _.result(this, 'className');1101 var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);1102 this.setElement($el, false);1103 } else {1104 this.setElement(_.result(this, 'el'), false);1105 }1106 }11071108 });11091110 // Backbone.sync1111 // -------------11121113 // Override this function to change the manner in which Backbone persists1114 // models to the server. You will be passed the type of request, and the1115 // model in question. By default, makes a RESTful Ajax request1116 // to the model's `url()`. Some possible customizations could be:1117 //1118 // * Use `setTimeout` to batch rapid-fire updates into a single request.1119 // * Send up the models as XML instead of JSON.1120 // * Persist models via WebSockets instead of Ajax.1121 //1122 // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests1123 // as `POST`, with a `_method` parameter containing the true HTTP method,1124 // as well as all requests with the body as `application/x-www-form-urlencoded`1125 // instead of `application/json` with the model in a param named `model`.1126 // Useful when interfacing with server-side languages like **PHP** that make1127 // it difficult to read the body of `PUT` requests.1128 Backbone.sync = function(method, model, options) {1129 var type = methodMap[method];11301131 // Default options, unless specified.1132 _.defaults(options || (options = {}), {1133 emulateHTTP: Backbone.emulateHTTP,1134 emulateJSON: Backbone.emulateJSON1135 });11361137 // Default JSON-request options.1138 var params = {type: type, dataType: 'json'};11391140 // Ensure that we have a URL.1141 if (!options.url) {1142 params.url = _.result(model, 'url') || urlError();1143 }11441145 // Ensure that we have the appropriate request data.1146 if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {1147 params.contentType = 'application/json';1148 params.data = JSON.stringify(options.attrs || model.toJSON(options));1149 }11501151 // For older servers, emulate JSON by encoding the request into an HTML-form.1152 if (options.emulateJSON) {1153 params.contentType = 'application/x-www-form-urlencoded';1154 params.data = params.data ? {model: params.data} : {};1155 }11561157 // For older servers, emulate HTTP by mimicking the HTTP method with `_method`1158 // And an `X-HTTP-Method-Override` header.1159 if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {1160 params.type = 'POST';1161 if (options.emulateJSON) params.data._method = type;1162 var beforeSend = options.beforeSend;1163 options.beforeSend = function(xhr) {1164 xhr.setRequestHeader('X-HTTP-Method-Override', type);1165 if (beforeSend) return beforeSend.apply(this, arguments);1166 };1167 }11681169 // Don't process data on a non-GET request.1170 if (params.type !== 'GET' && !options.emulateJSON) {1171 params.processData = false;1172 }11731174 // If we're sending a `PATCH` request, and we're in an old Internet Explorer1175 // that still has ActiveX enabled by default, override jQuery to use that1176 // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.1177 if (params.type === 'PATCH' && window.ActiveXObject &&1178 !(window.external && window.external.msActiveXFilteringEnabled)) {1179 params.xhr = function() {1180 return new ActiveXObject("Microsoft.XMLHTTP");1181 };1182 }11831184 // Make the request, allowing the user to override any Ajax options.1185 var xhr = options.xhr = Backbone.ajax(_.extend(params, options));1186 model.trigger('request', model, xhr, options);1187 return xhr;1188 };11891190 // Map from CRUD to HTTP for our default `Backbone.sync` implementation.1191 var methodMap = {1192 'create': 'POST',1193 'update': 'PUT',1194 'patch': 'PATCH',1195 'delete': 'DELETE',1196 'read': 'GET'1197 };11981199 // Set the default implementation of `Backbone.ajax` to proxy through to `$`.1200 // Override this if you'd like to use a different library.1201 Backbone.ajax = function() {1202 return Backbone.$.ajax.apply(Backbone.$, arguments);1203 };12041205 // Backbone.Router1206 // ---------------12071208 // Routers map faux-URLs to actions, and fire events when routes are1209 // matched. Creating a new one sets its `routes` hash, if not set statically.1210 var Router = Backbone.Router = function(options) {1211 options || (options = {});1212 if (options.routes) this.routes = options.routes;1213 this._bindRoutes();1214 this.initialize.apply(this, arguments);1215 };12161217 // Cached regular expressions for matching named param parts and splatted1218 // parts of route strings.1219 var optionalParam = /\((.*?)\)/g;1220 var namedParam = /(\(\?)?:\w+/g;1221 var splatParam = /\*\w+/g;1222 var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;12231224 // Set up all inheritable **Backbone.Router** properties and methods.1225 _.extend(Router.prototype, Events, {12261227 // Initialize is an empty function by default. Override it with your own1228 // initialization logic.1229 initialize: function(){},12301231 // Manually bind a single named route to a callback. For example:1232 //1233 // this.route('search/:query/p:num', 'search', function(query, num) {1234 // ...1235 // });1236 //1237 route: function(route, name, callback) {1238 if (!_.isRegExp(route)) route = this._routeToRegExp(route);1239 if (_.isFunction(name)) {1240 callback = name;1241 name = '';1242 }1243 if (!callback) callback = this[name];1244 var router = this;1245 Backbone.history.route(route, function(fragment) {1246 var args = router._extractParameters(route, fragment);1247 callback && callback.apply(router, args);1248 router.trigger.apply(router, ['route:' + name].concat(args));1249 router.trigger('route', name, args);1250 Backbone.history.trigger('route', router, name, args);1251 });1252 return this;1253 },12541255 // Simple proxy to `Backbone.history` to save a fragment into the history.1256 navigate: function(fragment, options) {1257 Backbone.history.navigate(fragment, options);1258 return this;1259 },12601261 // Bind all defined routes to `Backbone.history`. We have to reverse the1262 // order of the routes here to support behavior where the most general1263 // routes can be defined at the bottom of the route map.1264 _bindRoutes: function() {1265 if (!this.routes) return;1266 this.routes = _.result(this, 'routes');1267 var route, routes = _.keys(this.routes);1268 while ((route = routes.pop()) != null) {1269 this.route(route, this.routes[route]);1270 }1271 },12721273 // Convert a route string into a regular expression, suitable for matching1274 // against the current location hash.1275 _routeToRegExp: function(route) {1276 route = route.replace(escapeRegExp, '\\$&')1277 .replace(optionalParam, '(?:$1)?')1278 .replace(namedParam, function(match, optional){1279 return optional ? match : '([^\/]+)';1280 })1281 .replace(splatParam, '(.*?)');1282 return new RegExp('^' + route + '$');1283 },12841285 // Given a route, and a URL fragment that it matches, return the array of1286 // extracted decoded parameters. Empty or unmatched parameters will be1287 // treated as `null` to normalize cross-browser behavior.1288 _extractParameters: function(route, fragment) {1289 var params = route.exec(fragment).slice(1);1290 return _.map(params, function(param) {1291 return param ? decodeURIComponent(param) : null;1292 });1293 }12941295 });12961297 // Backbone.History1298 // ----------------12991300 // Handles cross-browser history management, based on either1301 // [pushState](http://diveintohtml5.info/history.html) and real URLs, or1302 // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)1303 // and URL fragments. If the browser supports neither (old IE, natch),1304 // falls back to polling.1305 var History = Backbone.History = function() {1306 this.handlers = [];1307 _.bindAll(this, 'checkUrl');13081309 // Ensure that `History` can be used outside of the browser.1310 if (typeof window !== 'undefined') {1311 this.location = window.location;1312 this.history = window.history;1313 }1314 };13151316 // Cached regex for stripping a leading hash/slash and trailing space.1317 var routeStripper = /^[#\/]|\s+$/g;13181319 // Cached regex for stripping leading and trailing slashes.1320 var rootStripper = /^\/+|\/+$/g;13211322 // Cached regex for detecting MSIE.1323 var isExplorer = /msie [\w.]+/;13241325 // Cached regex for removing a trailing slash.1326 var trailingSlash = /\/$/;13271328 // Has the history handling already been started?1329 History.started = false;13301331 // Set up all inheritable **Backbone.History** properties and methods.1332 _.extend(History.prototype, Events, {13331334 // The default interval to poll for hash changes, if necessary, is1335 // twenty times a second.1336 interval: 50,13371338 // Gets the true hash value. Cannot use location.hash directly due to bug1339 // in Firefox where location.hash will always be decoded.1340 getHash: function(window) {1341 var match = (window || this).location.href.match(/#(.*)$/);1342 return match ? match[1] : '';1343 },13441345 // Get the cross-browser normalized URL fragment, either from the URL,1346 // the hash, or the override.1347 getFragment: function(fragment, forcePushState) {1348 if (fragment == null) {1349 if (this._hasPushState || !this._wantsHashChange || forcePushState) {1350 fragment = this.location.pathname;1351 var root = this.root.replace(trailingSlash, '');1352 if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);1353 } else {1354 fragment = this.getHash();1355 }1356 }1357 return fragment.replace(routeStripper, '');1358 },13591360 // Start the hash change handling, returning `true` if the current URL matches1361 // an existing route, and `false` otherwise.1362 start: function(options) {1363 if (History.started) throw new Error("Backbone.history has already been started");1364 History.started = true;13651366 // Figure out the initial configuration. Do we need an iframe?1367 // Is pushState desired ... is it available?1368 this.options = _.extend({}, {root: '/'}, this.options, options);1369 this.root = this.options.root;1370 this._wantsHashChange = this.options.hashChange !== false;1371 this._wantsPushState = !!this.options.pushState;1372 this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);1373 var fragment = this.getFragment();1374 var docMode = document.documentMode;1375 var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));13761377 // Normalize root to always include a leading and trailing slash.1378 this.root = ('/' + this.root + '/').replace(rootStripper, '/');13791380 if (oldIE && this._wantsHashChange) {1381 this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;1382 this.navigate(fragment);1383 }13841385 // Depending on whether we're using pushState or hashes, and whether1386 // 'onhashchange' is supported, determine how we check the URL state.1387 if (this._hasPushState) {1388 Backbone.$(window).on('popstate', this.checkUrl);1389 } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {1390 Backbone.$(window).on('hashchange', this.checkUrl);1391 } else if (this._wantsHashChange) {1392 this._checkUrlInterval = setInterval(this.checkUrl, this.interval);1393 }13941395 // Determine if we need to change the base url, for a pushState link1396 // opened by a non-pushState browser.1397 this.fragment = fragment;1398 var loc = this.location;1399 var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;14001401 // If we've started off with a route from a `pushState`-enabled browser,1402 // but we're currently in a browser that doesn't support it...1403 if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {1404 this.fragment = this.getFragment(null, true);1405 this.location.replace(this.root + this.location.search + '#' + this.fragment);1406 // Return immediately as browser will do redirect to new url1407 return true;14081409 // Or if we've started out with a hash-based route, but we're currently1410 // in a browser where it could be `pushState`-based instead...1411 } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {1412 this.fragment = this.getHash().replace(routeStripper, '');1413 this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);1414 }14151416 if (!this.options.silent) return this.loadUrl();1417 },14181419 // Disable Backbone.history, perhaps temporarily. Not useful in a real app,1420 // but possibly useful for unit testing Routers.1421 stop: function() {1422 Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);1423 clearInterval(this._checkUrlInterval);1424 History.started = false;1425 },14261427 // Add a route to be tested when the fragment changes. Routes added later1428 // may override previous routes.1429 route: function(route, callback) {1430 this.handlers.unshift({route: route, callback: callback});1431 },14321433 // Checks the current URL to see if it has changed, and if it has,1434 // calls `loadUrl`, normalizing across the hidden iframe.1435 checkUrl: function(e) {1436 var current = this.getFragment();1437 if (current === this.fragment && this.iframe) {1438 current = this.getFragment(this.getHash(this.iframe));1439 }1440 if (current === this.fragment) return false;1441 if (this.iframe) this.navigate(current);1442 this.loadUrl() || this.loadUrl(this.getHash());1443 },14441445 // Attempt to load the current URL fragment. If a route succeeds with a1446 // match, returns `true`. If no defined routes matches the fragment,1447 // returns `false`.1448 loadUrl: function(fragmentOverride) {1449 var fragment = this.fragment = this.getFragment(fragmentOverride);1450 var matched = _.any(this.handlers, function(handler) {1451 if (handler.route.test(fragment)) {1452 handler.callback(fragment);1453 return true;1454 }1455 });1456 return matched;1457 },14581459 // Save a fragment into the hash history, or replace the URL state if the1460 // 'replace' option is passed. You are responsible for properly URL-encoding1461 // the fragment in advance.1462 //1463 // The options object can contain `trigger: true` if you wish to have the1464 // route callback be fired (not usually desirable), or `replace: true`, if1465 // you wish to modify the current URL without adding an entry to the history.1466 navigate: function(fragment, options) {1467 if (!History.started) return false;1468 if (!options || options === true) options = {trigger: options};1469 fragment = this.getFragment(fragment || '');1470 if (this.fragment === fragment) return;1471 this.fragment = fragment;1472 var url = this.root + fragment;14731474 // If pushState is available, we use it to set the fragment as a real URL.1475 if (this._hasPushState) {1476 this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);14771478 // If hash changes haven't been explicitly disabled, update the hash1479 // fragment to store history.1480 } else if (this._wantsHashChange) {1481 this._updateHash(this.location, fragment, options.replace);1482 if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {1483 // Opening and closing the iframe tricks IE7 and earlier to push a1484 // history entry on hash-tag change. When replace is true, we don't1485 // want this.1486 if(!options.replace) this.iframe.document.open().close();1487 this._updateHash(this.iframe.location, fragment, options.replace);1488 }14891490 // If you've told us that you explicitly don't want fallback hashchange-1491 // based history, then `navigate` becomes a page refresh.1492 } else {1493 return this.location.assign(url);1494 }1495 if (options.trigger) this.loadUrl(fragment);1496 },14971498 // Update the hash location, either replacing the current entry, or adding1499 // a new one to the browser history.1500 _updateHash: function(location, fragment, replace) {1501 if (replace) {1502 var href = location.href.replace(/(javascript:|#).*$/, '');1503 location.replace(href + '#' + fragment);1504 } else {1505 // Some browsers require that `hash` contains a leading #.1506 location.hash = '#' + fragment;1507 }1508 }15091510 });15111512 // Create the default Backbone.history.1513 Backbone.history = new History;15141515 // Helpers1516 // -------15171518 // Helper function to correctly set up the prototype chain, for subclasses.1519 // Similar to `goog.inherits`, but uses a hash of prototype properties and1520 // class properties to be extended.1521 var extend = function(protoProps, staticProps) {1522 var parent = this;1523 var child;15241525 // The constructor function for the new subclass is either defined by you1526 // (the "constructor" property in your `extend` definition), or defaulted1527 // by us to simply call the parent's constructor.1528 if (protoProps && _.has(protoProps, 'constructor')) {1529 child = protoProps.constructor;1530 } else {1531 child = function(){ return parent.apply(this, arguments); };1532 }15331534 // Add static properties to the constructor function, if supplied.1535 _.extend(child, parent, staticProps);15361537 // Set the prototype chain to inherit from `parent`, without calling1538 // `parent`'s constructor function.1539 var Surrogate = function(){ this.constructor = child; };1540 Surrogate.prototype = parent.prototype;1541 child.prototype = new Surrogate;15421543 // Add prototype properties (instance properties) to the subclass,1544 // if supplied.1545 if (protoProps) _.extend(child.prototype, protoProps);15461547 // Set a convenience property in case the parent's prototype is needed1548 // later.1549 child.__super__ = parent.prototype;15501551 return child;1552 };15531554 // Set up inheritance for the model, collection, router, view and history.1555 Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;15561557 // Throw an error when a URL is needed, and none is supplied.1558 var urlError = function() {1559 throw new Error('A "url" property or function must be specified');1560 };15611562 // Wrap an optional error callback with a fallback error event.1563 var wrapError = function (model, options) {1564 var error = options.error;1565 options.error = function(resp) {1566 if (error) error(model, resp, options);1567 model.trigger('error', model, resp, options);1568 };1569 };15701571 }).call(this);// Backbone.js 1.0.0 2 3 // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. 4 // Backbone may be freely distributed under the MIT license. 5 // For all details and documentation: 6 // http://backbonejs.org 7 8 (function(){ 9 10 // Initial Setup 11 // ------------- 12 13 // Save a reference to the global object (`window` in the browser, `exports` 14 // on the server). 15 var root = this; 16 17 // Save the previous value of the `Backbone` variable, so that it can be 18 // restored later on, if `noConflict` is used. 19 var previousBackbone = root.Backbone; 20 21 // Create local references to array methods we'll want to use later. 22 var array = []; 23 var push = array.push; 24 var slice = array.slice; 25 var splice = array.splice; 26 27 // The top-level namespace. All public Backbone classes and modules will 28 // be attached to this. Exported for both the browser and the server. 29 var Backbone; 30 if (typeof exports !== 'undefined') { 31 Backbone = exports; 32 } else { 33 Backbone = root.Backbone = {}; 34 } 35 36 // Current version of the library. Keep in sync with `package.json`. 37 Backbone.VERSION = '1.0.0'; 38 39 // Require Underscore, if we're on the server, and it's not already present. 40 var _ = root._; 41 if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 42 43 // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 44 // the `$` variable. 45 Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; 46 47 // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 48 // to its previous owner. Returns a reference to this Backbone object. 49 Backbone.noConflict = function() { 50 root.Backbone = previousBackbone; 51 return this; 52 }; 53 54 // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 55 // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 56 // set a `X-Http-Method-Override` header. 57 Backbone.emulateHTTP = false; 58 59 // Turn on `emulateJSON` to support legacy servers that can't deal with direct 60 // `application/json` requests ... will encode the body as 61 // `application/x-www-form-urlencoded` instead and will send the model in a 62 // form param named `model`. 63 Backbone.emulateJSON = false; 64 65 // Backbone.Events 66 // --------------- 67 68 // A module that can be mixed in to *any object* in order to provide it with 69 // custom events. You may bind with `on` or remove with `off` callback 70 // functions to an event; `trigger`-ing an event fires all callbacks in 71 // succession. 72 // 73 // var object = {}; 74 // _.extend(object, Backbone.Events); 75 // object.on('expand', function(){ alert('expanded'); }); 76 // object.trigger('expand'); 77 // 78 var Events = Backbone.Events = { 79 80 // Bind an event to a `callback` function. Passing `"all"` will bind 81 // the callback to all events fired. 82 on: function(name, callback, context) { 83 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 84 this._events || (this._events = {}); 85 var events = this._events[name] || (this._events[name] = []); 86 events.push({callback: callback, context: context, ctx: context || this}); 87 return this; 88 }, 89 90 // Bind an event to only be triggered a single time. After the first time 91 // the callback is invoked, it will be removed. 92 once: function(name, callback, context) { 93 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 94 var self = this; 95 var once = _.once(function() { 96 self.off(name, once); 97 callback.apply(this, arguments); 98 }); 99 once._callback = callback; 100 return this.on(name, once, context); 101 }, 102 103 // Remove one or many callbacks. If `context` is null, removes all 104 // callbacks with that function. If `callback` is null, removes all 105 // callbacks for the event. If `name` is null, removes all bound 106 // callbacks for all events. 107 off: function(name, callback, context) { 108 var retain, ev, events, names, i, l, j, k; 109 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 110 if (!name && !callback && !context) { 111 this._events = {}; 112 return this; 113 } 114 115 names = name ? [name] : _.keys(this._events); 116 for (i = 0, l = names.length; i < l; i++) { 117 name = names[i]; 118 if (events = this._events[name]) { 119 this._events[name] = retain = []; 120 if (callback || context) { 121 for (j = 0, k = events.length; j < k; j++) { 122 ev = events[j]; 123 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 124 (context && context !== ev.context)) { 125 retain.push(ev); 126 } 127 } 128 } 129 if (!retain.length) delete this._events[name]; 130 } 131 } 132 133 return this; 134 }, 135 136 // Trigger one or many events, firing all bound callbacks. Callbacks are 137 // passed the same arguments as `trigger` is, apart from the event name 138 // (unless you're listening on `"all"`, which will cause your callback to 139 // receive the true name of the event as the first argument). 140 trigger: function(name) { 141 if (!this._events) return this; 142 var args = slice.call(arguments, 1); 143 if (!eventsApi(this, 'trigger', name, args)) return this; 144 var events = this._events[name]; 145 var allEvents = this._events.all; 146 if (events) triggerEvents(events, args); 147 if (allEvents) triggerEvents(allEvents, arguments); 148 return this; 149 }, 150 151 // Tell this object to stop listening to either specific events ... or 152 // to every object it's currently listening to. 153 stopListening: function(obj, name, callback) { 154 var listeners = this._listeners; 155 if (!listeners) return this; 156 var deleteListener = !name && !callback; 157 if (typeof name === 'object') callback = this; 158 if (obj) (listeners = {})[obj._listenerId] = obj; 159 for (var id in listeners) { 160 listeners[id].off(name, callback, this); 161 if (deleteListener) delete this._listeners[id]; 162 } 163 return this; 164 } 165 166 }; 167 168 // Regular expression used to split event strings. 169 var eventSplitter = /\s+/; 170 171 // Implement fancy features of the Events API such as multiple event 172 // names `"change blur"` and jQuery-style event maps `{change: action}` 173 // in terms of the existing API. 174 var eventsApi = function(obj, action, name, rest) { 175 if (!name) return true; 176 177 // Handle event maps. 178 if (typeof name === 'object') { 179 for (var key in name) { 180 obj[action].apply(obj, [key, name[key]].concat(rest)); 181 } 182 return false; 183 } 184 185 // Handle space separated event names. 186 if (eventSplitter.test(name)) { 187 var names = name.split(eventSplitter); 188 for (var i = 0, l = names.length; i < l; i++) { 189 obj[action].apply(obj, [names[i]].concat(rest)); 190 } 191 return false; 192 } 193 194 return true; 195 }; 196 197 // A difficult-to-believe, but optimized internal dispatch function for 198 // triggering events. Tries to keep the usual cases speedy (most internal 199 // Backbone events have 3 arguments). 200 var triggerEvents = function(events, args) { 201 var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 202 switch (args.length) { 203 case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 204 case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 205 case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 206 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 207 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); 208 } 209 }; 210 211 var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 212 213 // Inversion-of-control versions of `on` and `once`. Tell *this* object to 214 // listen to an event in another object ... keeping track of what it's 215 // listening to. 216 _.each(listenMethods, function(implementation, method) { 217 Events[method] = function(obj, name, callback) { 218 var listeners = this._listeners || (this._listeners = {}); 219 var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 220 listeners[id] = obj; 221 if (typeof name === 'object') callback = this; 222 obj[implementation](name, callback, this); 223 return this; 224 }; 225 }); 226 227 // Aliases for backwards compatibility. 228 Events.bind = Events.on; 229 Events.unbind = Events.off; 230 231 // Allow the `Backbone` object to serve as a global event bus, for folks who 232 // want global "pubsub" in a convenient place. 233 _.extend(Backbone, Events); 234 235 // Backbone.Model 236 // -------------- 237 238 // Backbone **Models** are the basic data object in the framework -- 239 // frequently representing a row in a table in a database on your server. 240 // A discrete chunk of data and a bunch of useful, related methods for 241 // performing computations and transformations on that data. 242 243 // Create a new model with the specified attributes. A client id (`cid`) 244 // is automatically generated and assigned for you. 245 var Model = Backbone.Model = function(attributes, options) { 246 var defaults; 247 var attrs = attributes || {}; 248 options || (options = {}); 249 this.cid = _.uniqueId('c'); 250 this.attributes = {}; 251 _.extend(this, _.pick(options, modelOptions)); 252 if (options.parse) attrs = this.parse(attrs, options) || {}; 253 if (defaults = _.result(this, 'defaults')) { 254 attrs = _.defaults({}, attrs, defaults); 255 } 256 this.set(attrs, options); 257 this.changed = {}; 258 this.initialize.apply(this, arguments); 259 }; 260 261 // A list of options to be attached directly to the model, if provided. 262 var modelOptions = ['url', 'urlRoot', 'collection']; 263 264 // Attach all inheritable methods to the Model prototype. 265 _.extend(Model.prototype, Events, { 266 267 // A hash of attributes whose current and previous value differ. 268 changed: null, 269 270 // The value returned during the last failed validation. 271 validationError: null, 272 273 // The default name for the JSON `id` attribute is `"id"`. MongoDB and 274 // CouchDB users may want to set this to `"_id"`. 275 idAttribute: 'id', 276 277 // Initialize is an empty function by default. Override it with your own 278 // initialization logic. 279 initialize: function(){}, 280 281 // Return a copy of the model's `attributes` object. 282 toJSON: function(options) { 283 return _.clone(this.attributes); 284 }, 285 286 // Proxy `Backbone.sync` by default -- but override this if you need 287 // custom syncing semantics for *this* particular model. 288 sync: function() { 289 return Backbone.sync.apply(this, arguments); 290 }, 291 292 // Get the value of an attribute. 293 get: function(attr) { 294 return this.attributes[attr]; 295 }, 296 297 // Get the HTML-escaped value of an attribute. 298 escape: function(attr) { 299 return _.escape(this.get(attr)); 300 }, 301 302 // Returns `true` if the attribute contains a value that is not null 303 // or undefined. 304 has: function(attr) { 305 return this.get(attr) != null; 306 }, 307 308 // Set a hash of model attributes on the object, firing `"change"`. This is 309 // the core primitive operation of a model, updating the data and notifying 310 // anyone who needs to know about the change in state. The heart of the beast. 311 set: function(key, val, options) { 312 var attr, attrs, unset, changes, silent, changing, prev, current; 313 if (key == null) return this; 314 315 // Handle both `"key", value` and `{key: value}` -style arguments. 316 if (typeof key === 'object') { 317 attrs = key; 318 options = val; 319 } else { 320 (attrs = {})[key] = val; 321 } 322 323 options || (options = {}); 324 325 // Run validation. 326 if (!this._validate(attrs, options)) return false; 327 328 // Extract attributes and options. 329 unset = options.unset; 330 silent = options.silent; 331 changes = []; 332 changing = this._changing; 333 this._changing = true; 334 335 if (!changing) { 336 this._previousAttributes = _.clone(this.attributes); 337 this.changed = {}; 338 } 339 current = this.attributes, prev = this._previousAttributes; 340 341 // Check for changes of `id`. 342 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 343 344 // For each `set` attribute, update or delete the current value. 345 for (attr in attrs) { 346 val = attrs[attr]; 347 if (!_.isEqual(current[attr], val)) changes.push(attr); 348 if (!_.isEqual(prev[attr], val)) { 349 this.changed[attr] = val; 350 } else { 351 delete this.changed[attr]; 352 } 353 unset ? delete current[attr] : current[attr] = val; 354 } 355 356 // Trigger all relevant attribute changes. 357 if (!silent) { 358 if (changes.length) this._pending = true; 359 for (var i = 0, l = changes.length; i < l; i++) { 360 this.trigger('change:' + changes[i], this, current[changes[i]], options); 361 } 362 } 363 364 // You might be wondering why there's a `while` loop here. Changes can 365 // be recursively nested within `"change"` events. 366 if (changing) return this; 367 if (!silent) { 368 while (this._pending) { 369 this._pending = false; 370 this.trigger('change', this, options); 371 } 372 } 373 this._pending = false; 374 this._changing = false; 375 return this; 376 }, 377 378 // Remove an attribute from the model, firing `"change"`. `unset` is a noop 379 // if the attribute doesn't exist. 380 unset: function(attr, options) { 381 return this.set(attr, void 0, _.extend({}, options, {unset: true})); 382 }, 383 384 // Clear all attributes on the model, firing `"change"`. 385 clear: function(options) { 386 var attrs = {}; 387 for (var key in this.attributes) attrs[key] = void 0; 388 return this.set(attrs, _.extend({}, options, {unset: true})); 389 }, 390 391 // Determine if the model has changed since the last `"change"` event. 392 // If you specify an attribute name, determine if that attribute has changed. 393 hasChanged: function(attr) { 394 if (attr == null) return !_.isEmpty(this.changed); 395 return _.has(this.changed, attr); 396 }, 397 398 // Return an object containing all the attributes that have changed, or 399 // false if there are no changed attributes. Useful for determining what 400 // parts of a view need to be updated and/or what attributes need to be 401 // persisted to the server. Unset attributes will be set to undefined. 402 // You can also pass an attributes object to diff against the model, 403 // determining if there *would be* a change. 404 changedAttributes: function(diff) { 405 if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 406 var val, changed = false; 407 var old = this._changing ? this._previousAttributes : this.attributes; 408 for (var attr in diff) { 409 if (_.isEqual(old[attr], (val = diff[attr]))) continue; 410 (changed || (changed = {}))[attr] = val; 411 } 412 return changed; 413 }, 414 415 // Get the previous value of an attribute, recorded at the time the last 416 // `"change"` event was fired. 417 previous: function(attr) { 418 if (attr == null || !this._previousAttributes) return null; 419 return this._previousAttributes[attr]; 420 }, 421 422 // Get all of the attributes of the model at the time of the previous 423 // `"change"` event. 424 previousAttributes: function() { 425 return _.clone(this._previousAttributes); 426 }, 427 428 // Fetch the model from the server. If the server's representation of the 429 // model differs from its current attributes, they will be overridden, 430 // triggering a `"change"` event. 431 fetch: function(options) { 432 options = options ? _.clone(options) : {}; 433 if (options.parse === void 0) options.parse = true; 434 var model = this; 435 var success = options.success; 436 options.success = function(resp) { 437 if (!model.set(model.parse(resp, options), options)) return false; 438 if (success) success(model, resp, options); 439 model.trigger('sync', model, resp, options); 440 }; 441 wrapError(this, options); 442 return this.sync('read', this, options); 443 }, 444 445 // Set a hash of model attributes, and sync the model to the server. 446 // If the server returns an attributes hash that differs, the model's 447 // state will be `set` again. 448 save: function(key, val, options) { 449 var attrs, method, xhr, attributes = this.attributes; 450 451 // Handle both `"key", value` and `{key: value}` -style arguments. 452 if (key == null || typeof key === 'object') { 453 attrs = key; 454 options = val; 455 } else { 456 (attrs = {})[key] = val; 457 } 458 459 // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. 460 if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; 461 462 options = _.extend({validate: true}, options); 463 464 // Do not persist invalid models. 465 if (!this._validate(attrs, options)) return false; 466 467 // Set temporary attributes if `{wait: true}`. 468 if (attrs && options.wait) { 469 this.attributes = _.extend({}, attributes, attrs); 470 } 471 472 // After a successful server-side save, the client is (optionally) 473 // updated with the server-side state. 474 if (options.parse === void 0) options.parse = true; 475 var model = this; 476 var success = options.success; 477 options.success = function(resp) { 478 // Ensure attributes are restored during synchronous saves. 479 model.attributes = attributes; 480 var serverAttrs = model.parse(resp, options); 481 if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 482 if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 483 return false; 484 } 485 if (success) success(model, resp, options); 486 model.trigger('sync', model, resp, options); 487 }; 488 wrapError(this, options); 489 490 method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 491 if (method === 'patch') options.attrs = attrs; 492 xhr = this.sync(method, this, options); 493 494 // Restore attributes. 495 if (attrs && options.wait) this.attributes = attributes; 496 497 return xhr; 498 }, 499 500 // Destroy this model on the server if it was already persisted. 501 // Optimistically removes the model from its collection, if it has one. 502 // If `wait: true` is passed, waits for the server to respond before removal. 503 destroy: function(options) { 504 options = options ? _.clone(options) : {}; 505 var model = this; 506 var success = options.success; 507 508 var destroy = function() { 509 model.trigger('destroy', model, model.collection, options); 510 }; 511 512 options.success = function(resp) { 513 if (options.wait || model.isNew()) destroy(); 514 if (success) success(model, resp, options); 515 if (!model.isNew()) model.trigger('sync', model, resp, options); 516 }; 517 518 if (this.isNew()) { 519 options.success(); 520 return false; 521 } 522 wrapError(this, options); 523 524 var xhr = this.sync('delete', this, options); 525 if (!options.wait) destroy(); 526 return xhr; 527 }, 528 529 // Default URL for the model's representation on the server -- if you're 530 // using Backbone's restful methods, override this to change the endpoint 531 // that will be called. 532 url: function() { 533 var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); 534 if (this.isNew()) return base; 535 return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); 536 }, 537 538 // **parse** converts a response into the hash of attributes to be `set` on 539 // the model. The default implementation is just to pass the response along. 540 parse: function(resp, options) { 541 return resp; 542 }, 543 544 // Create a new model with identical attributes to this one. 545 clone: function() { 546 return new this.constructor(this.attributes); 547 }, 548 549 // A model is new if it has never been saved to the server, and lacks an id. 550 isNew: function() { 551 return this.id == null; 552 }, 553 554 // Check if the model is currently in a valid state. 555 isValid: function(options) { 556 return this._validate({}, _.extend(options || {}, { validate: true })); 557 }, 558 559 // Run validation against the next complete set of model attributes, 560 // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 561 _validate: function(attrs, options) { 562 if (!options.validate || !this.validate) return true; 563 attrs = _.extend({}, this.attributes, attrs); 564 var error = this.validationError = this.validate(attrs, options) || null; 565 if (!error) return true; 566 this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); 567 return false; 568 } 569 570 }); 571 572 // Underscore methods that we want to implement on the Model. 573 var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 574 575 // Mix in each Underscore method as a proxy to `Model#attributes`. 576 _.each(modelMethods, function(method) { 577 Model.prototype[method] = function() { 578 var args = slice.call(arguments); 579 args.unshift(this.attributes); 580 return _[method].apply(_, args); 581 }; 582 }); 583 584 // Backbone.Collection 585 // ------------------- 586 587 // If models tend to represent a single row of data, a Backbone Collection is 588 // more analagous to a table full of data ... or a small slice or page of that 589 // table, or a collection of rows that belong together for a particular reason 590 // -- all of the messages in this particular folder, all of the documents 591 // belonging to this particular author, and so on. Collections maintain 592 // indexes of their models, both in order, and for lookup by `id`. 593 594 // Create a new **Collection**, perhaps to contain a specific type of `model`. 595 // If a `comparator` is specified, the Collection will maintain 596 // its models in sort order, as they're added and removed. 597 var Collection = Backbone.Collection = function(models, options) { 598 options || (options = {}); 599 if (options.url) this.url = options.url; 600 if (options.model) this.model = options.model; 601 if (options.comparator !== void 0) this.comparator = options.comparator; 602 this._reset(); 603 this.initialize.apply(this, arguments); 604 if (models) this.reset(models, _.extend({silent: true}, options)); 605 }; 606 607 // Default options for `Collection#set`. 608 var setOptions = {add: true, remove: true, merge: true}; 609 var addOptions = {add: true, merge: false, remove: false}; 610 611 // Define the Collection's inheritable methods. 612 _.extend(Collection.prototype, Events, { 613 614 // The default model for a collection is just a **Backbone.Model**. 615 // This should be overridden in most cases. 616 model: Model, 617 618 // Initialize is an empty function by default. Override it with your own 619 // initialization logic. 620 initialize: function(){}, 621 622 // The JSON representation of a Collection is an array of the 623 // models' attributes. 624 toJSON: function(options) { 625 return this.map(function(model){ return model.toJSON(options); }); 626 }, 627 628 // Proxy `Backbone.sync` by default. 629 sync: function() { 630 return Backbone.sync.apply(this, arguments); 631 }, 632 633 // Add a model, or list of models to the set. 634 add: function(models, options) { 635 return this.set(models, _.defaults(options || {}, addOptions)); 636 }, 637 638 // Remove a model, or a list of models from the set. 639 remove: function(models, options) { 640 models = _.isArray(models) ? models.slice() : [models]; 641 options || (options = {}); 642 var i, l, index, model; 643 for (i = 0, l = models.length; i < l; i++) { 644 model = this.get(models[i]); 645 if (!model) continue; 646 delete this._byId[model.id]; 647 delete this._byId[model.cid]; 648 index = this.indexOf(model); 649 this.models.splice(index, 1); 650 this.length--; 651 if (!options.silent) { 652 options.index = index; 653 model.trigger('remove', model, this, options); 654 } 655 this._removeReference(model); 656 } 657 return this; 658 }, 659 660 // Update a collection by `set`-ing a new list of models, adding new ones, 661 // removing models that are no longer present, and merging models that 662 // already exist in the collection, as necessary. Similar to **Model#set**, 663 // the core operation for updating the data contained by the collection. 664 set: function(models, options) { 665 options = _.defaults(options || {}, setOptions); 666 if (options.parse) models = this.parse(models, options); 667 if (!_.isArray(models)) models = models ? [models] : []; 668 var i, l, model, attrs, existing, sort; 669 var at = options.at; 670 var sortable = this.comparator && (at == null) && options.sort !== false; 671 var sortAttr = _.isString(this.comparator) ? this.comparator : null; 672 var toAdd = [], toRemove = [], modelMap = {}; 673 674 // Turn bare objects into model references, and prevent invalid models 675 // from being added. 676 for (i = 0, l = models.length; i < l; i++) { 677 if (!(model = this._prepareModel(models[i], options))) continue; 678 679 // If a duplicate is found, prevent it from being added and 680 // optionally merge it into the existing model. 681 if (existing = this.get(model)) { 682 if (options.remove) modelMap[existing.cid] = true; 683 if (options.merge) { 684 existing.set(model.attributes, options); 685 if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 686 } 687 688 // This is a new model, push it to the `toAdd` list. 689 } else if (options.add) { 690 toAdd.push(model); 691 692 // Listen to added models' events, and index models for lookup by 693 // `id` and by `cid`. 694 model.on('all', this._onModelEvent, this); 695 this._byId[model.cid] = model; 696 if (model.id != null) this._byId[model.id] = model; 697 } 698 } 699 700 // Remove nonexistent models if appropriate. 701 if (options.remove) { 702 for (i = 0, l = this.length; i < l; ++i) { 703 if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 704 } 705 if (toRemove.length) this.remove(toRemove, options); 706 } 707 708 // See if sorting is needed, update `length` and splice in new models. 709 if (toAdd.length) { 710 if (sortable) sort = true; 711 this.length += toAdd.length; 712 if (at != null) { 713 splice.apply(this.models, [at, 0].concat(toAdd)); 714 } else { 715 push.apply(this.models, toAdd); 716 } 717 } 718 719 // Silently sort the collection if appropriate. 720 if (sort) this.sort({silent: true}); 721 722 if (options.silent) return this; 723 724 // Trigger `add` events. 725 for (i = 0, l = toAdd.length; i < l; i++) { 726 (model = toAdd[i]).trigger('add', model, this, options); 727 } 728 729 // Trigger `sort` if the collection was sorted. 730 if (sort) this.trigger('sort', this, options); 731 return this; 732 }, 733 734 // When you have more items than you want to add or remove individually, 735 // you can reset the entire set with a new list of models, without firing 736 // any granular `add` or `remove` events. Fires `reset` when finished. 737 // Useful for bulk operations and optimizations. 738 reset: function(models, options) { 739 options || (options = {}); 740 for (var i = 0, l = this.models.length; i < l; i++) { 741 this._removeReference(this.models[i]); 742 } 743 options.previousModels = this.models; 744 this._reset(); 745 this.add(models, _.extend({silent: true}, options)); 746 if (!options.silent) this.trigger('reset', this, options); 747 return this; 748 }, 749 750 // Add a model to the end of the collection. 751 push: function(model, options) { 752 model = this._prepareModel(model, options); 753 this.add(model, _.extend({at: this.length}, options)); 754 return model; 755 }, 756 757 // Remove a model from the end of the collection. 758 pop: function(options) { 759 var model = this.at(this.length - 1); 760 this.remove(model, options); 761 return model; 762 }, 763 764 // Add a model to the beginning of the collection. 765 unshift: function(model, options) { 766 model = this._prepareModel(model, options); 767 this.add(model, _.extend({at: 0}, options)); 768 return model; 769 }, 770 771 // Remove a model from the beginning of the collection. 772 shift: function(options) { 773 var model = this.at(0); 774 this.remove(model, options); 775 return model; 776 }, 777 778 // Slice out a sub-array of models from the collection. 779 slice: function(begin, end) { 780 return this.models.slice(begin, end); 781 }, 782 783 // Get a model from the set by id. 784 get: function(obj) { 785 if (obj == null) return void 0; 786 return this._byId[obj.id != null ? obj.id : obj.cid || obj]; 787 }, 788 789 // Get the model at the given index. 790 at: function(index) { 791 return this.models[index]; 792 }, 793 794 // Return models with matching attributes. Useful for simple cases of 795 // `filter`. 796 where: function(attrs, first) { 797 if (_.isEmpty(attrs)) return first ? void 0 : []; 798 return this[first ? 'find' : 'filter'](function(model) { 799 for (var key in attrs) { 800 if (attrs[key] !== model.get(key)) return false; 801 } 802 return true; 803 }); 804 }, 805 806 // Return the first model with matching attributes. Useful for simple cases 807 // of `find`. 808 findWhere: function(attrs) { 809 return this.where(attrs, true); 810 }, 811 812 // Force the collection to re-sort itself. You don't need to call this under 813 // normal circumstances, as the set will maintain sort order as each item 814 // is added. 815 sort: function(options) { 816 if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 817 options || (options = {}); 818 819 // Run sort based on type of `comparator`. 820 if (_.isString(this.comparator) || this.comparator.length === 1) { 821 this.models = this.sortBy(this.comparator, this); 822 } else { 823 this.models.sort(_.bind(this.comparator, this)); 824 } 825 826 if (!options.silent) this.trigger('sort', this, options); 827 return this; 828 }, 829 830 // Figure out the smallest index at which a model should be inserted so as 831 // to maintain order. 832 sortedIndex: function(model, value, context) { 833 value || (value = this.comparator); 834 var iterator = _.isFunction(value) ? value : function(model) { 835 return model.get(value); 836 }; 837 return _.sortedIndex(this.models, model, iterator, context); 838 }, 839 840 // Pluck an attribute from each model in the collection. 841 pluck: function(attr) { 842 return _.invoke(this.models, 'get', attr); 843 }, 844 845 // Fetch the default set of models for this collection, resetting the 846 // collection when they arrive. If `reset: true` is passed, the response 847 // data will be passed through the `reset` method instead of `set`. 848 fetch: function(options) { 849 options = options ? _.clone(options) : {}; 850 if (options.parse === void 0) options.parse = true; 851 var success = options.success; 852 var collection = this; 853 options.success = function(resp) { 854 var method = options.reset ? 'reset' : 'set'; 855 collection[method](resp, options); 856 if (success) success(collection, resp, options); 857 collection.trigger('sync', collection, resp, options); 858 }; 859 wrapError(this, options); 860 return this.sync('read', this, options); 861 }, 862 863 // Create a new instance of a model in this collection. Add the model to the 864 // collection immediately, unless `wait: true` is passed, in which case we 865 // wait for the server to agree. 866 create: function(model, options) { 867 options = options ? _.clone(options) : {}; 868 if (!(model = this._prepareModel(model, options))) return false; 869 if (!options.wait) this.add(model, options); 870 var collection = this; 871 var success = options.success; 872 options.success = function(resp) { 873 if (options.wait) collection.add(model, options); 874 if (success) success(model, resp, options); 875 }; 876 model.save(null, options); 877 return model; 878 }, 879 880 // **parse** converts a response into a list of models to be added to the 881 // collection. The default implementation is just to pass it through. 882 parse: function(resp, options) { 883 return resp; 884 }, 885 886 // Create a new collection with an identical list of models as this one. 887 clone: function() { 888 return new this.constructor(this.models); 889 }, 890 891 // Private method to reset all internal state. Called when the collection 892 // is first initialized or reset. 893 _reset: function() { 894 this.length = 0; 895 this.models = []; 896 this._byId = {}; 897 }, 898 899 // Prepare a hash of attributes (or other model) to be added to this 900 // collection. 901 _prepareModel: function(attrs, options) { 902 if (attrs instanceof Model) { 903 if (!attrs.collection) attrs.collection = this; 904 return attrs; 905 } 906 options || (options = {}); 907 options.collection = this; 908 var model = new this.model(attrs, options); 909 if (!model._validate(attrs, options)) { 910 this.trigger('invalid', this, attrs, options); 911 return false; 912 } 913 return model; 914 }, 915 916 // Internal method to sever a model's ties to a collection. 917 _removeReference: function(model) { 918 if (this === model.collection) delete model.collection; 919 model.off('all', this._onModelEvent, this); 920 }, 921 922 // Internal method called every time a model in the set fires an event. 923 // Sets need to update their indexes when models change ids. All other 924 // events simply proxy through. "add" and "remove" events that originate 925 // in other collections are ignored. 926 _onModelEvent: function(event, model, collection, options) { 927 if ((event === 'add' || event === 'remove') && collection !== this) return; 928 if (event === 'destroy') this.remove(model, options); 929 if (model && event === 'change:' + model.idAttribute) { 930 delete this._byId[model.previous(model.idAttribute)]; 931 if (model.id != null) this._byId[model.id] = model; 932 } 933 this.trigger.apply(this, arguments); 934 } 935 936 }); 937 938 // Underscore methods that we want to implement on the Collection. 939 // 90% of the core usefulness of Backbone Collections is actually implemented 940 // right here: 941 var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 942 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 943 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 944 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 945 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 946 'isEmpty', 'chain']; 947 948 // Mix in each Underscore method as a proxy to `Collection#models`. 949 _.each(methods, function(method) { 950 Collection.prototype[method] = function() { 951 var args = slice.call(arguments); 952 args.unshift(this.models); 953 return _[method].apply(_, args); 954 }; 955 }); 956 957 // Underscore methods that take a property name as an argument. 958 var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 959 960 // Use attributes instead of properties. 961 _.each(attributeMethods, function(method) { 962 Collection.prototype[method] = function(value, context) { 963 var iterator = _.isFunction(value) ? value : function(model) { 964 return model.get(value); 965 }; 966 return _[method](this.models, iterator, context); 967 }; 968 }); 969 970 // Backbone.View 971 // ------------- 972 973 // Backbone Views are almost more convention than they are actual code. A View 974 // is simply a JavaScript object that represents a logical chunk of UI in the 975 // DOM. This might be a single item, an entire list, a sidebar or panel, or 976 // even the surrounding frame which wraps your whole app. Defining a chunk of 977 // UI as a **View** allows you to define your DOM events declaratively, without 978 // having to worry about render order ... and makes it easy for the view to 979 // react to specific changes in the state of your models. 980 981 // Creating a Backbone.View creates its initial element outside of the DOM, 982 // if an existing element is not provided... 983 var View = Backbone.View = function(options) { 984 this.cid = _.uniqueId('view'); 985 this._configure(options || {}); 986 this._ensureElement(); 987 this.initialize.apply(this, arguments); 988 this.delegateEvents(); 989 }; 990 991 // Cached regex to split keys for `delegate`. 992 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 993 994 // List of view options to be merged as properties. 995 var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 996 997 // Set up all inheritable **Backbone.View** properties and methods. 998 _.extend(View.prototype, Events, { 999 1000 // The default `tagName` of a View's element is `"div"`. 1001 tagName: 'div', 1002 1003 // jQuery delegate for element lookup, scoped to DOM elements within the 1004 // current view. This should be prefered to global lookups where possible. 1005 $: function(selector) { 1006 return this.$el.find(selector); 1007 }, 1008 1009 // Initialize is an empty function by default. Override it with your own 1010 // initialization logic. 1011 initialize: function(){}, 1012 1013 // **render** is the core function that your view should override, in order 1014 // to populate its element (`this.el`), with the appropriate HTML. The 1015 // convention is for **render** to always return `this`. 1016 render: function() { 1017 return this; 1018 }, 1019 1020 // Remove this view by taking the element out of the DOM, and removing any 1021 // applicable Backbone.Events listeners. 1022 remove: function() { 1023 this.$el.remove(); 1024 this.stopListening(); 1025 return this; 1026 }, 1027 1028 // Change the view's element (`this.el` property), including event 1029 // re-delegation. 1030 setElement: function(element, delegate) { 1031 if (this.$el) this.undelegateEvents(); 1032 this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1033 this.el = this.$el[0]; 1034 if (delegate !== false) this.delegateEvents(); 1035 return this; 1036 }, 1037 1038 // Set callbacks, where `this.events` is a hash of 1039 // 1040 // *{"event selector": "callback"}* 1041 // 1042 // { 1043 // 'mousedown .title': 'edit', 1044 // 'click .button': 'save' 1045 // 'click .open': function(e) { ... } 1046 // } 1047 // 1048 // pairs. Callbacks will be bound to the view, with `this` set properly. 1049 // Uses event delegation for efficiency. 1050 // Omitting the selector binds the event to `this.el`. 1051 // This only works for delegate-able events: not `focus`, `blur`, and 1052 // not `change`, `submit`, and `reset` in Internet Explorer. 1053 delegateEvents: function(events) { 1054 if (!(events || (events = _.result(this, 'events')))) return this; 1055 this.undelegateEvents(); 1056 for (var key in events) { 1057 var method = events[key]; 1058 if (!_.isFunction(method)) method = this[events[key]]; 1059 if (!method) continue; 1060 1061 var match = key.match(delegateEventSplitter); 1062 var eventName = match[1], selector = match[2]; 1063 method = _.bind(method, this); 1064 eventName += '.delegateEvents' + this.cid; 1065 if (selector === '') { 1066 this.$el.on(eventName, method); 1067 } else { 1068 this.$el.on(eventName, selector, method); 1069 } 1070 } 1071 return this; 1072 }, 1073 1074 // Clears all callbacks previously bound to the view with `delegateEvents`. 1075 // You usually don't need to use this, but may wish to if you have multiple 1076 // Backbone views attached to the same DOM element. 1077 undelegateEvents: function() { 1078 this.$el.off('.delegateEvents' + this.cid); 1079 return this; 1080 }, 1081 1082 // Performs the initial configuration of a View with a set of options. 1083 // Keys with special meaning *(e.g. model, collection, id, className)* are 1084 // attached directly to the view. See `viewOptions` for an exhaustive 1085 // list. 1086 _configure: function(options) { 1087 if (this.options) options = _.extend({}, _.result(this, 'options'), options); 1088 _.extend(this, _.pick(options, viewOptions)); 1089 this.options = options; 1090 }, 1091 1092 // Ensure that the View has a DOM element to render into. 1093 // If `this.el` is a string, pass it through `$()`, take the first 1094 // matching element, and re-assign it to `el`. Otherwise, create 1095 // an element from the `id`, `className` and `tagName` properties. 1096 _ensureElement: function() { 1097 if (!this.el) { 1098 var attrs = _.extend({}, _.result(this, 'attributes')); 1099 if (this.id) attrs.id = _.result(this, 'id'); 1100 if (this.className) attrs['class'] = _.result(this, 'className'); 1101 var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1102 this.setElement($el, false); 1103 } else { 1104 this.setElement(_.result(this, 'el'), false); 1105 } 1106 } 1107 1108 }); 1109 1110 // Backbone.sync 1111 // ------------- 1112 1113 // Override this function to change the manner in which Backbone persists 1114 // models to the server. You will be passed the type of request, and the 1115 // model in question. By default, makes a RESTful Ajax request 1116 // to the model's `url()`. Some possible customizations could be: 1117 // 1118 // * Use `setTimeout` to batch rapid-fire updates into a single request. 1119 // * Send up the models as XML instead of JSON. 1120 // * Persist models via WebSockets instead of Ajax. 1121 // 1122 // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1123 // as `POST`, with a `_method` parameter containing the true HTTP method, 1124 // as well as all requests with the body as `application/x-www-form-urlencoded` 1125 // instead of `application/json` with the model in a param named `model`. 1126 // Useful when interfacing with server-side languages like **PHP** that make 1127 // it difficult to read the body of `PUT` requests. 1128 Backbone.sync = function(method, model, options) { 1129 var type = methodMap[method]; 1130 1131 // Default options, unless specified. 1132 _.defaults(options || (options = {}), { 1133 emulateHTTP: Backbone.emulateHTTP, 1134 emulateJSON: Backbone.emulateJSON 1135 }); 1136 1137 // Default JSON-request options. 1138 var params = {type: type, dataType: 'json'}; 1139 1140 // Ensure that we have a URL. 1141 if (!options.url) { 1142 params.url = _.result(model, 'url') || urlError(); 1143 } 1144 1145 // Ensure that we have the appropriate request data. 1146 if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1147 params.contentType = 'application/json'; 1148 params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1149 } 1150 1151 // For older servers, emulate JSON by encoding the request into an HTML-form. 1152 if (options.emulateJSON) { 1153 params.contentType = 'application/x-www-form-urlencoded'; 1154 params.data = params.data ? {model: params.data} : {}; 1155 } 1156 1157 // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1158 // And an `X-HTTP-Method-Override` header. 1159 if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1160 params.type = 'POST'; 1161 if (options.emulateJSON) params.data._method = type; 1162 var beforeSend = options.beforeSend; 1163 options.beforeSend = function(xhr) { 1164 xhr.setRequestHeader('X-HTTP-Method-Override', type); 1165 if (beforeSend) return beforeSend.apply(this, arguments); 1166 }; 1167 } 1168 1169 // Don't process data on a non-GET request. 1170 if (params.type !== 'GET' && !options.emulateJSON) { 1171 params.processData = false; 1172 } 1173 1174 // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1175 // that still has ActiveX enabled by default, override jQuery to use that 1176 // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1177 if (params.type === 'PATCH' && window.ActiveXObject && 1178 !(window.external && window.external.msActiveXFilteringEnabled)) { 1179 params.xhr = function() { 1180 return new ActiveXObject("Microsoft.XMLHTTP"); 1181 }; 1182 } 1183 1184 // Make the request, allowing the user to override any Ajax options. 1185 var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1186 model.trigger('request', model, xhr, options); 1187 return xhr; 1188 }; 1189 1190 // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1191 var methodMap = { 1192 'create': 'POST', 1193 'update': 'PUT', 1194 'patch': 'PATCH', 1195 'delete': 'DELETE', 1196 'read': 'GET' 1197 }; 1198 1199 // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1200 // Override this if you'd like to use a different library. 1201 Backbone.ajax = function() { 1202 return Backbone.$.ajax.apply(Backbone.$, arguments); 1203 }; 1204 1205 // Backbone.Router 1206 // --------------- 1207 1208 // Routers map faux-URLs to actions, and fire events when routes are 1209 // matched. Creating a new one sets its `routes` hash, if not set statically. 1210 var Router = Backbone.Router = function(options) { 1211 options || (options = {}); 1212 if (options.routes) this.routes = options.routes; 1213 this._bindRoutes(); 1214 this.initialize.apply(this, arguments); 1215 }; 1216 1217 // Cached regular expressions for matching named param parts and splatted 1218 // parts of route strings. 1219 var optionalParam = /\((.*?)\)/g; 1220 var namedParam = /(\(\?)?:\w+/g; 1221 var splatParam = /\*\w+/g; 1222 var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1223 1224 // Set up all inheritable **Backbone.Router** properties and methods. 1225 _.extend(Router.prototype, Events, { 1226 1227 // Initialize is an empty function by default. Override it with your own 1228 // initialization logic. 1229 initialize: function(){}, 1230 1231 // Manually bind a single named route to a callback. For example: 1232 // 1233 // this.route('search/:query/p:num', 'search', function(query, num) { 1234 // ... 1235 // }); 1236 // 1237 route: function(route, name, callback) { 1238 if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1239 if (_.isFunction(name)) { 1240 callback = name; 1241 name = ''; 1242 } 1243 if (!callback) callback = this[name]; 1244 var router = this; 1245 Backbone.history.route(route, function(fragment) { 1246 var args = router._extractParameters(route, fragment); 1247 callback && callback.apply(router, args); 1248 router.trigger.apply(router, ['route:' + name].concat(args)); 1249 router.trigger('route', name, args); 1250 Backbone.history.trigger('route', router, name, args); 1251 }); 1252 return this; 1253 }, 1254 1255 // Simple proxy to `Backbone.history` to save a fragment into the history. 1256 navigate: function(fragment, options) { 1257 Backbone.history.navigate(fragment, options); 1258 return this; 1259 }, 1260 1261 // Bind all defined routes to `Backbone.history`. We have to reverse the 1262 // order of the routes here to support behavior where the most general 1263 // routes can be defined at the bottom of the route map. 1264 _bindRoutes: function() { 1265 if (!this.routes) return; 1266 this.routes = _.result(this, 'routes'); 1267 var route, routes = _.keys(this.routes); 1268 while ((route = routes.pop()) != null) { 1269 this.route(route, this.routes[route]); 1270 } 1271 }, 1272 1273 // Convert a route string into a regular expression, suitable for matching 1274 // against the current location hash. 1275 _routeToRegExp: function(route) { 1276 route = route.replace(escapeRegExp, '\\$&') 1277 .replace(optionalParam, '(?:$1)?') 1278 .replace(namedParam, function(match, optional){ 1279 return optional ? match : '([^\/]+)'; 1280 }) 1281 .replace(splatParam, '(.*?)'); 1282 return new RegExp('^' + route + '$'); 1283 }, 1284 1285 // Given a route, and a URL fragment that it matches, return the array of 1286 // extracted decoded parameters. Empty or unmatched parameters will be 1287 // treated as `null` to normalize cross-browser behavior. 1288 _extractParameters: function(route, fragment) { 1289 var params = route.exec(fragment).slice(1); 1290 return _.map(params, function(param) { 1291 return param ? decodeURIComponent(param) : null; 1292 }); 1293 } 1294 1295 }); 1296 1297 // Backbone.History 1298 // ---------------- 1299 1300 // Handles cross-browser history management, based on either 1301 // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1302 // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1303 // and URL fragments. If the browser supports neither (old IE, natch), 1304 // falls back to polling. 1305 var History = Backbone.History = function() { 1306 this.handlers = []; 1307 _.bindAll(this, 'checkUrl'); 1308 1309 // Ensure that `History` can be used outside of the browser. 1310 if (typeof window !== 'undefined') { 1311 this.location = window.location; 1312 this.history = window.history; 1313 } 1314 }; 1315 1316 // Cached regex for stripping a leading hash/slash and trailing space. 1317 var routeStripper = /^[#\/]|\s+$/g; 1318 1319 // Cached regex for stripping leading and trailing slashes. 1320 var rootStripper = /^\/+|\/+$/g; 1321 1322 // Cached regex for detecting MSIE. 1323 var isExplorer = /msie [\w.]+/; 1324 1325 // Cached regex for removing a trailing slash. 1326 var trailingSlash = /\/$/; 1327 1328 // Has the history handling already been started? 1329 History.started = false; 1330 1331 // Set up all inheritable **Backbone.History** properties and methods. 1332 _.extend(History.prototype, Events, { 1333 1334 // The default interval to poll for hash changes, if necessary, is 1335 // twenty times a second. 1336 interval: 50, 1337 1338 // Gets the true hash value. Cannot use location.hash directly due to bug 1339 // in Firefox where location.hash will always be decoded. 1340 getHash: function(window) { 1341 var match = (window || this).location.href.match(/#(.*)$/); 1342 return match ? match[1] : ''; 1343 }, 1344 1345 // Get the cross-browser normalized URL fragment, either from the URL, 1346 // the hash, or the override. 1347 getFragment: function(fragment, forcePushState) { 1348 if (fragment == null) { 1349 if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1350 fragment = this.location.pathname; 1351 var root = this.root.replace(trailingSlash, ''); 1352 if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); 1353 } else { 1354 fragment = this.getHash(); 1355 } 1356 } 1357 return fragment.replace(routeStripper, ''); 1358 }, 1359 1360 // Start the hash change handling, returning `true` if the current URL matches 1361 // an existing route, and `false` otherwise. 1362 start: function(options) { 1363 if (History.started) throw new Error("Backbone.history has already been started"); 1364 History.started = true; 1365 1366 // Figure out the initial configuration. Do we need an iframe? 1367 // Is pushState desired ... is it available? 1368 this.options = _.extend({}, {root: '/'}, this.options, options); 1369 this.root = this.options.root; 1370 this._wantsHashChange = this.options.hashChange !== false; 1371 this._wantsPushState = !!this.options.pushState; 1372 this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1373 var fragment = this.getFragment(); 1374 var docMode = document.documentMode; 1375 var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1376 1377 // Normalize root to always include a leading and trailing slash. 1378 this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1379 1380 if (oldIE && this._wantsHashChange) { 1381 this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; 1382 this.navigate(fragment); 1383 } 1384 1385 // Depending on whether we're using pushState or hashes, and whether 1386 // 'onhashchange' is supported, determine how we check the URL state. 1387 if (this._hasPushState) { 1388 Backbone.$(window).on('popstate', this.checkUrl); 1389 } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { 1390 Backbone.$(window).on('hashchange', this.checkUrl); 1391 } else if (this._wantsHashChange) { 1392 this._checkUrlInterval = setInterval(this.checkUrl, this.interval); 1393 } 1394 1395 // Determine if we need to change the base url, for a pushState link 1396 // opened by a non-pushState browser. 1397 this.fragment = fragment; 1398 var loc = this.location; 1399 var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; 1400 1401 // If we've started off with a route from a `pushState`-enabled browser, 1402 // but we're currently in a browser that doesn't support it... 1403 if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { 1404 this.fragment = this.getFragment(null, true); 1405 this.location.replace(this.root + this.location.search + '#' + this.fragment); 1406 // Return immediately as browser will do redirect to new url 1407 return true; 1408 1409 // Or if we've started out with a hash-based route, but we're currently 1410 // in a browser where it could be `pushState`-based instead... 1411 } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { 1412 this.fragment = this.getHash().replace(routeStripper, ''); 1413 this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); 1414 } 1415 1416 if (!this.options.silent) return this.loadUrl(); 1417 }, 1418 1419 // Disable Backbone.history, perhaps temporarily. Not useful in a real app, 1420 // but possibly useful for unit testing Routers. 1421 stop: function() { 1422 Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); 1423 clearInterval(this._checkUrlInterval); 1424 History.started = false; 1425 }, 1426 1427 // Add a route to be tested when the fragment changes. Routes added later 1428 // may override previous routes. 1429 route: function(route, callback) { 1430 this.handlers.unshift({route: route, callback: callback}); 1431 }, 1432 1433 // Checks the current URL to see if it has changed, and if it has, 1434 // calls `loadUrl`, normalizing across the hidden iframe. 1435 checkUrl: function(e) { 1436 var current = this.getFragment(); 1437 if (current === this.fragment && this.iframe) { 1438 current = this.getFragment(this.getHash(this.iframe)); 1439 } 1440 if (current === this.fragment) return false; 1441 if (this.iframe) this.navigate(current); 1442 this.loadUrl() || this.loadUrl(this.getHash()); 1443 }, 1444 1445 // Attempt to load the current URL fragment. If a route succeeds with a 1446 // match, returns `true`. If no defined routes matches the fragment, 1447 // returns `false`. 1448 loadUrl: function(fragmentOverride) { 1449 var fragment = this.fragment = this.getFragment(fragmentOverride); 1450 var matched = _.any(this.handlers, function(handler) { 1451 if (handler.route.test(fragment)) { 1452 handler.callback(fragment); 1453 return true; 1454 } 1455 }); 1456 return matched; 1457 }, 1458 1459 // Save a fragment into the hash history, or replace the URL state if the 1460 // 'replace' option is passed. You are responsible for properly URL-encoding 1461 // the fragment in advance. 1462 // 1463 // The options object can contain `trigger: true` if you wish to have the 1464 // route callback be fired (not usually desirable), or `replace: true`, if 1465 // you wish to modify the current URL without adding an entry to the history. 1466 navigate: function(fragment, options) { 1467 if (!History.started) return false; 1468 if (!options || options === true) options = {trigger: options}; 1469 fragment = this.getFragment(fragment || ''); 1470 if (this.fragment === fragment) return; 1471 this.fragment = fragment; 1472 var url = this.root + fragment; 1473 1474 // If pushState is available, we use it to set the fragment as a real URL. 1475 if (this._hasPushState) { 1476 this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 1477 1478 // If hash changes haven't been explicitly disabled, update the hash 1479 // fragment to store history. 1480 } else if (this._wantsHashChange) { 1481 this._updateHash(this.location, fragment, options.replace); 1482 if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { 1483 // Opening and closing the iframe tricks IE7 and earlier to push a 1484 // history entry on hash-tag change. When replace is true, we don't 1485 // want this. 1486 if(!options.replace) this.iframe.document.open().close(); 1487 this._updateHash(this.iframe.location, fragment, options.replace); 1488 } 1489 1490 // If you've told us that you explicitly don't want fallback hashchange- 1491 // based history, then `navigate` becomes a page refresh. 1492 } else { 1493 return this.location.assign(url); 1494 } 1495 if (options.trigger) this.loadUrl(fragment); 1496 }, 1497 1498 // Update the hash location, either replacing the current entry, or adding 1499 // a new one to the browser history. 1500 _updateHash: function(location, fragment, replace) { 1501 if (replace) { 1502 var href = location.href.replace(/(javascript:|#).*$/, ''); 1503 location.replace(href + '#' + fragment); 1504 } else { 1505 // Some browsers require that `hash` contains a leading #. 1506 location.hash = '#' + fragment; 1507 } 1508 } 1509 1510 }); 1511 1512 // Create the default Backbone.history. 1513 Backbone.history = new History; 1514 1515 // Helpers 1516 // ------- 1517 1518 // Helper function to correctly set up the prototype chain, for subclasses. 1519 // Similar to `goog.inherits`, but uses a hash of prototype properties and 1520 // class properties to be extended. 1521 var extend = function(protoProps, staticProps) { 1522 var parent = this; 1523 var child; 1524 1525 // The constructor function for the new subclass is either defined by you 1526 // (the "constructor" property in your `extend` definition), or defaulted 1527 // by us to simply call the parent's constructor. 1528 if (protoProps && _.has(protoProps, 'constructor')) { 1529 child = protoProps.constructor; 1530 } else { 1531 child = function(){ return parent.apply(this, arguments); }; 1532 } 1533 1534 // Add static properties to the constructor function, if supplied. 1535 _.extend(child, parent, staticProps); 1536 1537 // Set the prototype chain to inherit from `parent`, without calling 1538 // `parent`'s constructor function. 1539 var Surrogate = function(){ this.constructor = child; }; 1540 Surrogate.prototype = parent.prototype; 1541 child.prototype = new Surrogate; 1542 1543 // Add prototype properties (instance properties) to the subclass, 1544 // if supplied. 1545 if (protoProps) _.extend(child.prototype, protoProps); 1546 1547 // Set a convenience property in case the parent's prototype is needed 1548 // later. 1549 child.__super__ = parent.prototype; 1550 1551 return child; 1552 }; 1553 1554 // Set up inheritance for the model, collection, router, view and history. 1555 Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; 1556 1557 // Throw an error when a URL is needed, and none is supplied. 1558 var urlError = function() { 1559 throw new Error('A "url" property or function must be specified'); 1560 }; 1561 1562 // Wrap an optional error callback with a fallback error event. 1563 var wrapError = function (model, options) { 1564 var error = options.error; 1565 options.error = function(resp) { 1566 if (error) error(model, resp, options); 1567 model.trigger('error', model, resp, options); 1568 }; 1569 }; 1570 1571 }).call(this);// Backbone.js 1.0.0 2 3 // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. 4 // Backbone may be freely distributed under the MIT license. 5 // For all details and documentation: 6 // http://backbonejs.org 7 8 (function(){ 9 10 // Initial Setup 11 // ------------- 12 13 // Save a reference to the global object (`window` in the browser, `exports` 14 // on the server). 15 var root = this; 16 17 // Save the previous value of the `Backbone` variable, so that it can be 18 // restored later on, if `noConflict` is used. 19 var previousBackbone = root.Backbone; 20 21 // Create local references to array methods we'll want to use later. 22 var array = []; 23 var push = array.push; 24 var slice = array.slice; 25 var splice = array.splice; 26 27 // The top-level namespace. All public Backbone classes and modules will 28 // be attached to this. Exported for both the browser and the server. 29 var Backbone; 30 if (typeof exports !== 'undefined') { 31 Backbone = exports; 32 } else { 33 Backbone = root.Backbone = {}; 34 } 35 36 // Current version of the library. Keep in sync with `package.json`. 37 Backbone.VERSION = '1.0.0'; 38 39 // Require Underscore, if we're on the server, and it's not already present. 40 var _ = root._; 41 if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 42 43 // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 44 // the `$` variable. 45 Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; 46 47 // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 48 // to its previous owner. Returns a reference to this Backbone object. 49 Backbone.noConflict = function() { 50 root.Backbone = previousBackbone; 51 return this; 52 }; 53 54 // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 55 // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 56 // set a `X-Http-Method-Override` header. 57 Backbone.emulateHTTP = false; 58 59 // Turn on `emulateJSON` to support legacy servers that can't deal with direct 60 // `application/json` requests ... will encode the body as 61 // `application/x-www-form-urlencoded` instead and will send the model in a 62 // form param named `model`. 63 Backbone.emulateJSON = false; 64 65 // Backbone.Events 66 // --------------- 67 68 // A module that can be mixed in to *any object* in order to provide it with 69 // custom events. You may bind with `on` or remove with `off` callback 70 // functions to an event; `trigger`-ing an event fires all callbacks in 71 // succession. 72 // 73 // var object = {}; 74 // _.extend(object, Backbone.Events); 75 // object.on('expand', function(){ alert('expanded'); }); 76 // object.trigger('expand'); 77 // 78 var Events = Backbone.Events = { 79 80 // Bind an event to a `callback` function. Passing `"all"` will bind 81 // the callback to all events fired. 82 on: function(name, callback, context) { 83 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 84 this._events || (this._events = {}); 85 var events = this._events[name] || (this._events[name] = []); 86 events.push({callback: callback, context: context, ctx: context || this}); 87 return this; 88 }, 89 90 // Bind an event to only be triggered a single time. After the first time 91 // the callback is invoked, it will be removed. 92 once: function(name, callback, context) { 93 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 94 var self = this; 95 var once = _.once(function() { 96 self.off(name, once); 97 callback.apply(this, arguments); 98 }); 99 once._callback = callback; 100 return this.on(name, once, context); 101 }, 102 103 // Remove one or many callbacks. If `context` is null, removes all 104 // callbacks with that function. If `callback` is null, removes all 105 // callbacks for the event. If `name` is null, removes all bound 106 // callbacks for all events. 107 off: function(name, callback, context) { 108 var retain, ev, events, names, i, l, j, k; 109 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 110 if (!name && !callback && !context) { 111 this._events = {}; 112 return this; 113 } 114 115 names = name ? [name] : _.keys(this._events); 116 for (i = 0, l = names.length; i < l; i++) { 117 name = names[i]; 118 if (events = this._events[name]) { 119 this._events[name] = retain = []; 120 if (callback || context) { 121 for (j = 0, k = events.length; j < k; j++) { 122 ev = events[j]; 123 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 124 (context && context !== ev.context)) { 125 retain.push(ev); 126 } 127 } 128 } 129 if (!retain.length) delete this._events[name]; 130 } 131 } 132 133 return this; 134 }, 135 136 // Trigger one or many events, firing all bound callbacks. Callbacks are 137 // passed the same arguments as `trigger` is, apart from the event name 138 // (unless you're listening on `"all"`, which will cause your callback to 139 // receive the true name of the event as the first argument). 140 trigger: function(name) { 141 if (!this._events) return this; 142 var args = slice.call(arguments, 1); 143 if (!eventsApi(this, 'trigger', name, args)) return this; 144 var events = this._events[name]; 145 var allEvents = this._events.all; 146 if (events) triggerEvents(events, args); 147 if (allEvents) triggerEvents(allEvents, arguments); 148 return this; 149 }, 150 151 // Tell this object to stop listening to either specific events ... or 152 // to every object it's currently listening to. 153 stopListening: function(obj, name, callback) { 154 var listeners = this._listeners; 155 if (!listeners) return this; 156 var deleteListener = !name && !callback; 157 if (typeof name === 'object') callback = this; 158 if (obj) (listeners = {})[obj._listenerId] = obj; 159 for (var id in listeners) { 160 listeners[id].off(name, callback, this); 161 if (deleteListener) delete this._listeners[id]; 162 } 163 return this; 164 } 165 166 }; 167 168 // Regular expression used to split event strings. 169 var eventSplitter = /\s+/; 170 171 // Implement fancy features of the Events API such as multiple event 172 // names `"change blur"` and jQuery-style event maps `{change: action}` 173 // in terms of the existing API. 174 var eventsApi = function(obj, action, name, rest) { 175 if (!name) return true; 176 177 // Handle event maps. 178 if (typeof name === 'object') { 179 for (var key in name) { 180 obj[action].apply(obj, [key, name[key]].concat(rest)); 181 } 182 return false; 183 } 184 185 // Handle space separated event names. 186 if (eventSplitter.test(name)) { 187 var names = name.split(eventSplitter); 188 for (var i = 0, l = names.length; i < l; i++) { 189 obj[action].apply(obj, [names[i]].concat(rest)); 190 } 191 return false; 192 } 193 194 return true; 195 }; 196 197 // A difficult-to-believe, but optimized internal dispatch function for 198 // triggering events. Tries to keep the usual cases speedy (most internal 199 // Backbone events have 3 arguments). 200 var triggerEvents = function(events, args) { 201 var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 202 switch (args.length) { 203 case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 204 case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 205 case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 206 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 207 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); 208 } 209 }; 210 211 var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 212 213 // Inversion-of-control versions of `on` and `once`. Tell *this* object to 214 // listen to an event in another object ... keeping track of what it's 215 // listening to. 216 _.each(listenMethods, function(implementation, method) { 217 Events[method] = function(obj, name, callback) { 218 var listeners = this._listeners || (this._listeners = {}); 219 var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 220 listeners[id] = obj; 221 if (typeof name === 'object') callback = this; 222 obj[implementation](name, callback, this); 223 return this; 224 }; 225 }); 226 227 // Aliases for backwards compatibility. 228 Events.bind = Events.on; 229 Events.unbind = Events.off; 230 231 // Allow the `Backbone` object to serve as a global event bus, for folks who 232 // want global "pubsub" in a convenient place. 233 _.extend(Backbone, Events); 234 235 // Backbone.Model 236 // -------------- 237 238 // Backbone **Models** are the basic data object in the framework -- 239 // frequently representing a row in a table in a database on your server. 240 // A discrete chunk of data and a bunch of useful, related methods for 241 // performing computations and transformations on that data. 242 243 // Create a new model with the specified attributes. A client id (`cid`) 244 // is automatically generated and assigned for you. 245 var Model = Backbone.Model = function(attributes, options) { 246 var defaults; 247 var attrs = attributes || {}; 248 options || (options = {}); 249 this.cid = _.uniqueId('c'); 250 this.attributes = {}; 251 _.extend(this, _.pick(options, modelOptions)); 252 if (options.parse) attrs = this.parse(attrs, options) || {}; 253 if (defaults = _.result(this, 'defaults')) { 254 attrs = _.defaults({}, attrs, defaults); 255 } 256 this.set(attrs, options); 257 this.changed = {}; 258 this.initialize.apply(this, arguments); 259 }; 260 261 // A list of options to be attached directly to the model, if provided. 262 var modelOptions = ['url', 'urlRoot', 'collection']; 263 264 // Attach all inheritable methods to the Model prototype. 265 _.extend(Model.prototype, Events, { 266 267 // A hash of attributes whose current and previous value differ. 268 changed: null, 269 270 // The value returned during the last failed validation. 271 validationError: null, 272 273 // The default name for the JSON `id` attribute is `"id"`. MongoDB and 274 // CouchDB users may want to set this to `"_id"`. 275 idAttribute: 'id', 276 277 // Initialize is an empty function by default. Override it with your own 278 // initialization logic. 279 initialize: function(){}, 280 281 // Return a copy of the model's `attributes` object. 282 toJSON: function(options) { 283 return _.clone(this.attributes); 284 }, 285 286 // Proxy `Backbone.sync` by default -- but override this if you need 287 // custom syncing semantics for *this* particular model. 288 sync: function() { 289 return Backbone.sync.apply(this, arguments); 290 }, 291 292 // Get the value of an attribute. 293 get: function(attr) { 294 return this.attributes[attr]; 295 }, 296 297 // Get the HTML-escaped value of an attribute. 298 escape: function(attr) { 299 return _.escape(this.get(attr)); 300 }, 301 302 // Returns `true` if the attribute contains a value that is not null 303 // or undefined. 304 has: function(attr) { 305 return this.get(attr) != null; 306 }, 307 308 // Set a hash of model attributes on the object, firing `"change"`. This is 309 // the core primitive operation of a model, updating the data and notifying 310 // anyone who needs to know about the change in state. The heart of the beast. 311 set: function(key, val, options) { 312 var attr, attrs, unset, changes, silent, changing, prev, current; 313 if (key == null) return this; 314 315 // Handle both `"key", value` and `{key: value}` -style arguments. 316 if (typeof key === 'object') { 317 attrs = key; 318 options = val; 319 } else { 320 (attrs = {})[key] = val; 321 } 322 323 options || (options = {}); 324 325 // Run validation. 326 if (!this._validate(attrs, options)) return false; 327 328 // Extract attributes and options. 329 unset = options.unset; 330 silent = options.silent; 331 changes = []; 332 changing = this._changing; 333 this._changing = true; 334 335 if (!changing) { 336 this._previousAttributes = _.clone(this.attributes); 337 this.changed = {}; 338 } 339 current = this.attributes, prev = this._previousAttributes; 340 341 // Check for changes of `id`. 342 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 343 344 // For each `set` attribute, update or delete the current value. 345 for (attr in attrs) { 346 val = attrs[attr]; 347 if (!_.isEqual(current[attr], val)) changes.push(attr); 348 if (!_.isEqual(prev[attr], val)) { 349 this.changed[attr] = val; 350 } else { 351 delete this.changed[attr]; 352 } 353 unset ? delete current[attr] : current[attr] = val; 354 } 355 356 // Trigger all relevant attribute changes. 357 if (!silent) { 358 if (changes.length) this._pending = true; 359 for (var i = 0, l = changes.length; i < l; i++) { 360 this.trigger('change:' + changes[i], this, current[changes[i]], options); 361 } 362 } 363 364 // You might be wondering why there's a `while` loop here. Changes can 365 // be recursively nested within `"change"` events. 366 if (changing) return this; 367 if (!silent) { 368 while (this._pending) { 369 this._pending = false; 370 this.trigger('change', this, options); 371 } 372 } 373 this._pending = false; 374 this._changing = false; 375 return this; 376 }, 377 378 // Remove an attribute from the model, firing `"change"`. `unset` is a noop 379 // if the attribute doesn't exist. 380 unset: function(attr, options) { 381 return this.set(attr, void 0, _.extend({}, options, {unset: true})); 382 }, 383 384 // Clear all attributes on the model, firing `"change"`. 385 clear: function(options) { 386 var attrs = {}; 387 for (var key in this.attributes) attrs[key] = void 0; 388 return this.set(attrs, _.extend({}, options, {unset: true})); 389 }, 390 391 // Determine if the model has changed since the last `"change"` event. 392 // If you specify an attribute name, determine if that attribute has changed. 393 hasChanged: function(attr) { 394 if (attr == null) return !_.isEmpty(this.changed); 395 return _.has(this.changed, attr); 396 }, 397 398 // Return an object containing all the attributes that have changed, or 399 // false if there are no changed attributes. Useful for determining what 400 // parts of a view need to be updated and/or what attributes need to be 401 // persisted to the server. Unset attributes will be set to undefined. 402 // You can also pass an attributes object to diff against the model, 403 // determining if there *would be* a change. 404 changedAttributes: function(diff) { 405 if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 406 var val, changed = false; 407 var old = this._changing ? this._previousAttributes : this.attributes; 408 for (var attr in diff) { 409 if (_.isEqual(old[attr], (val = diff[attr]))) continue; 410 (changed || (changed = {}))[attr] = val; 411 } 412 return changed; 413 }, 414 415 // Get the previous value of an attribute, recorded at the time the last 416 // `"change"` event was fired. 417 previous: function(attr) { 418 if (attr == null || !this._previousAttributes) return null; 419 return this._previousAttributes[attr]; 420 }, 421 422 // Get all of the attributes of the model at the time of the previous 423 // `"change"` event. 424 previousAttributes: function() { 425 return _.clone(this._previousAttributes); 426 }, 427 428 // Fetch the model from the server. If the server's representation of the 429 // model differs from its current attributes, they will be overridden, 430 // triggering a `"change"` event. 431 fetch: function(options) { 432 options = options ? _.clone(options) : {}; 433 if (options.parse === void 0) options.parse = true; 434 var model = this; 435 var success = options.success; 436 options.success = function(resp) { 437 if (!model.set(model.parse(resp, options), options)) return false; 438 if (success) success(model, resp, options); 439 model.trigger('sync', model, resp, options); 440 }; 441 wrapError(this, options); 442 return this.sync('read', this, options); 443 }, 444 445 // Set a hash of model attributes, and sync the model to the server. 446 // If the server returns an attributes hash that differs, the model's 447 // state will be `set` again. 448 save: function(key, val, options) { 449 var attrs, method, xhr, attributes = this.attributes; 450 451 // Handle both `"key", value` and `{key: value}` -style arguments. 452 if (key == null || typeof key === 'object') { 453 attrs = key; 454 options = val; 455 } else { 456 (attrs = {})[key] = val; 457 } 458 459 // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. 460 if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; 461 462 options = _.extend({validate: true}, options); 463 464 // Do not persist invalid models. 465 if (!this._validate(attrs, options)) return false; 466 467 // Set temporary attributes if `{wait: true}`. 468 if (attrs && options.wait) { 469 this.attributes = _.extend({}, attributes, attrs); 470 } 471 472 // After a successful server-side save, the client is (optionally) 473 // updated with the server-side state. 474 if (options.parse === void 0) options.parse = true; 475 var model = this; 476 var success = options.success; 477 options.success = function(resp) { 478 // Ensure attributes are restored during synchronous saves. 479 model.attributes = attributes; 480 var serverAttrs = model.parse(resp, options); 481 if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 482 if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 483 return false; 484 } 485 if (success) success(model, resp, options); 486 model.trigger('sync', model, resp, options); 487 }; 488 wrapError(this, options); 489 490 method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 491 if (method === 'patch') options.attrs = attrs; 492 xhr = this.sync(method, this, options); 493 494 // Restore attributes. 495 if (attrs && options.wait) this.attributes = attributes; 496 497 return xhr; 498 }, 499 500 // Destroy this model on the server if it was already persisted. 501 // Optimistically removes the model from its collection, if it has one. 502 // If `wait: true` is passed, waits for the server to respond before removal. 503 destroy: function(options) { 504 options = options ? _.clone(options) : {}; 505 var model = this; 506 var success = options.success; 507 508 var destroy = function() { 509 model.trigger('destroy', model, model.collection, options); 510 }; 511 512 options.success = function(resp) { 513 if (options.wait || model.isNew()) destroy(); 514 if (success) success(model, resp, options); 515 if (!model.isNew()) model.trigger('sync', model, resp, options); 516 }; 517 518 if (this.isNew()) { 519 options.success(); 520 return false; 521 } 522 wrapError(this, options); 523 524 var xhr = this.sync('delete', this, options); 525 if (!options.wait) destroy(); 526 return xhr; 527 }, 528 529 // Default URL for the model's representation on the server -- if you're 530 // using Backbone's restful methods, override this to change the endpoint 531 // that will be called. 532 url: function() { 533 var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); 534 if (this.isNew()) return base; 535 return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); 536 }, 537 538 // **parse** converts a response into the hash of attributes to be `set` on 539 // the model. The default implementation is just to pass the response along. 540 parse: function(resp, options) { 541 return resp; 542 }, 543 544 // Create a new model with identical attributes to this one. 545 clone: function() { 546 return new this.constructor(this.attributes); 547 }, 548 549 // A model is new if it has never been saved to the server, and lacks an id. 550 isNew: function() { 551 return this.id == null; 552 }, 553 554 // Check if the model is currently in a valid state. 555 isValid: function(options) { 556 return this._validate({}, _.extend(options || {}, { validate: true })); 557 }, 558 559 // Run validation against the next complete set of model attributes, 560 // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 561 _validate: function(attrs, options) { 562 if (!options.validate || !this.validate) return true; 563 attrs = _.extend({}, this.attributes, attrs); 564 var error = this.validationError = this.validate(attrs, options) || null; 565 if (!error) return true; 566 this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); 567 return false; 568 } 569 570 }); 571 572 // Underscore methods that we want to implement on the Model. 573 var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 574 575 // Mix in each Underscore method as a proxy to `Model#attributes`. 576 _.each(modelMethods, function(method) { 577 Model.prototype[method] = function() { 578 var args = slice.call(arguments); 579 args.unshift(this.attributes); 580 return _[method].apply(_, args); 581 }; 582 }); 583 584 // Backbone.Collection 585 // ------------------- 586 587 // If models tend to represent a single row of data, a Backbone Collection is 588 // more analagous to a table full of data ... or a small slice or page of that 589 // table, or a collection of rows that belong together for a particular reason 590 // -- all of the messages in this particular folder, all of the documents 591 // belonging to this particular author, and so on. Collections maintain 592 // indexes of their models, both in order, and for lookup by `id`. 593 594 // Create a new **Collection**, perhaps to contain a specific type of `model`. 595 // If a `comparator` is specified, the Collection will maintain 596 // its models in sort order, as they're added and removed. 597 var Collection = Backbone.Collection = function(models, options) { 598 options || (options = {}); 599 if (options.url) this.url = options.url; 600 if (options.model) this.model = options.model; 601 if (options.comparator !== void 0) this.comparator = options.comparator; 602 this._reset(); 603 this.initialize.apply(this, arguments); 604 if (models) this.reset(models, _.extend({silent: true}, options)); 605 }; 606 607 // Default options for `Collection#set`. 608 var setOptions = {add: true, remove: true, merge: true}; 609 var addOptions = {add: true, merge: false, remove: false}; 610 611 // Define the Collection's inheritable methods. 612 _.extend(Collection.prototype, Events, { 613 614 // The default model for a collection is just a **Backbone.Model**. 615 // This should be overridden in most cases. 616 model: Model, 617 618 // Initialize is an empty function by default. Override it with your own 619 // initialization logic. 620 initialize: function(){}, 621 622 // The JSON representation of a Collection is an array of the 623 // models' attributes. 624 toJSON: function(options) { 625 return this.map(function(model){ return model.toJSON(options); }); 626 }, 627 628 // Proxy `Backbone.sync` by default. 629 sync: function() { 630 return Backbone.sync.apply(this, arguments); 631 }, 632 633 // Add a model, or list of models to the set. 634 add: function(models, options) { 635 return this.set(models, _.defaults(options || {}, addOptions)); 636 }, 637 638 // Remove a model, or a list of models from the set. 639 remove: function(models, options) { 640 models = _.isArray(models) ? models.slice() : [models]; 641 options || (options = {}); 642 var i, l, index, model; 643 for (i = 0, l = models.length; i < l; i++) { 644 model = this.get(models[i]); 645 if (!model) continue; 646 delete this._byId[model.id]; 647 delete this._byId[model.cid]; 648 index = this.indexOf(model); 649 this.models.splice(index, 1); 650 this.length--; 651 if (!options.silent) { 652 options.index = index; 653 model.trigger('remove', model, this, options); 654 } 655 this._removeReference(model); 656 } 657 return this; 658 }, 659 660 // Update a collection by `set`-ing a new list of models, adding new ones, 661 // removing models that are no longer present, and merging models that 662 // already exist in the collection, as necessary. Similar to **Model#set**, 663 // the core operation for updating the data contained by the collection. 664 set: function(models, options) { 665 options = _.defaults(options || {}, setOptions); 666 if (options.parse) models = this.parse(models, options); 667 if (!_.isArray(models)) models = models ? [models] : []; 668 var i, l, model, attrs, existing, sort; 669 var at = options.at; 670 var sortable = this.comparator && (at == null) && options.sort !== false; 671 var sortAttr = _.isString(this.comparator) ? this.comparator : null; 672 var toAdd = [], toRemove = [], modelMap = {}; 673 674 // Turn bare objects into model references, and prevent invalid models 675 // from being added. 676 for (i = 0, l = models.length; i < l; i++) { 677 if (!(model = this._prepareModel(models[i], options))) continue; 678 679 // If a duplicate is found, prevent it from being added and 680 // optionally merge it into the existing model. 681 if (existing = this.get(model)) { 682 if (options.remove) modelMap[existing.cid] = true; 683 if (options.merge) { 684 existing.set(model.attributes, options); 685 if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 686 } 687 688 // This is a new model, push it to the `toAdd` list. 689 } else if (options.add) { 690 toAdd.push(model); 691 692 // Listen to added models' events, and index models for lookup by 693 // `id` and by `cid`. 694 model.on('all', this._onModelEvent, this); 695 this._byId[model.cid] = model; 696 if (model.id != null) this._byId[model.id] = model; 697 } 698 } 699 700 // Remove nonexistent models if appropriate. 701 if (options.remove) { 702 for (i = 0, l = this.length; i < l; ++i) { 703 if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 704 } 705 if (toRemove.length) this.remove(toRemove, options); 706 } 707 708 // See if sorting is needed, update `length` and splice in new models. 709 if (toAdd.length) { 710 if (sortable) sort = true; 711 this.length += toAdd.length; 712 if (at != null) { 713 splice.apply(this.models, [at, 0].concat(toAdd)); 714 } else { 715 push.apply(this.models, toAdd); 716 } 717 } 718 719 // Silently sort the collection if appropriate. 720 if (sort) this.sort({silent: true}); 721 722 if (options.silent) return this; 723 724 // Trigger `add` events. 725 for (i = 0, l = toAdd.length; i < l; i++) { 726 (model = toAdd[i]).trigger('add', model, this, options); 727 } 728 729 // Trigger `sort` if the collection was sorted. 730 if (sort) this.trigger('sort', this, options); 731 return this; 732 }, 733 734 // When you have more items than you want to add or remove individually, 735 // you can reset the entire set with a new list of models, without firing 736 // any granular `add` or `remove` events. Fires `reset` when finished. 737 // Useful for bulk operations and optimizations. 738 reset: function(models, options) { 739 options || (options = {}); 740 for (var i = 0, l = this.models.length; i < l; i++) { 741 this._removeReference(this.models[i]); 742 } 743 options.previousModels = this.models; 744 this._reset(); 745 this.add(models, _.extend({silent: true}, options)); 746 if (!options.silent) this.trigger('reset', this, options); 747 return this; 748 }, 749 750 // Add a model to the end of the collection. 751 push: function(model, options) { 752 model = this._prepareModel(model, options); 753 this.add(model, _.extend({at: this.length}, options)); 754 return model; 755 }, 756 757 // Remove a model from the end of the collection. 758 pop: function(options) { 759 var model = this.at(this.length - 1); 760 this.remove(model, options); 761 return model; 762 }, 763 764 // Add a model to the beginning of the collection. 765 unshift: function(model, options) { 766 model = this._prepareModel(model, options); 767 this.add(model, _.extend({at: 0}, options)); 768 return model; 769 }, 770 771 // Remove a model from the beginning of the collection. 772 shift: function(options) { 773 var model = this.at(0); 774 this.remove(model, options); 775 return model; 776 }, 777 778 // Slice out a sub-array of models from the collection. 779 slice: function(begin, end) { 780 return this.models.slice(begin, end); 781 }, 782 783 // Get a model from the set by id. 784 get: function(obj) { 785 if (obj == null) return void 0; 786 return this._byId[obj.id != null ? obj.id : obj.cid || obj]; 787 }, 788 789 // Get the model at the given index. 790 at: function(index) { 791 return this.models[index]; 792 }, 793 794 // Return models with matching attributes. Useful for simple cases of 795 // `filter`. 796 where: function(attrs, first) { 797 if (_.isEmpty(attrs)) return first ? void 0 : []; 798 return this[first ? 'find' : 'filter'](function(model) { 799 for (var key in attrs) { 800 if (attrs[key] !== model.get(key)) return false; 801 } 802 return true; 803 }); 804 }, 805 806 // Return the first model with matching attributes. Useful for simple cases 807 // of `find`. 808 findWhere: function(attrs) { 809 return this.where(attrs, true); 810 }, 811 812 // Force the collection to re-sort itself. You don't need to call this under 813 // normal circumstances, as the set will maintain sort order as each item 814 // is added. 815 sort: function(options) { 816 if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 817 options || (options = {}); 818 819 // Run sort based on type of `comparator`. 820 if (_.isString(this.comparator) || this.comparator.length === 1) { 821 this.models = this.sortBy(this.comparator, this); 822 } else { 823 this.models.sort(_.bind(this.comparator, this)); 824 } 825 826 if (!options.silent) this.trigger('sort', this, options); 827 return this; 828 }, 829 830 // Figure out the smallest index at which a model should be inserted so as 831 // to maintain order. 832 sortedIndex: function(model, value, context) { 833 value || (value = this.comparator); 834 var iterator = _.isFunction(value) ? value : function(model) { 835 return model.get(value); 836 }; 837 return _.sortedIndex(this.models, model, iterator, context); 838 }, 839 840 // Pluck an attribute from each model in the collection. 841 pluck: function(attr) { 842 return _.invoke(this.models, 'get', attr); 843 }, 844 845 // Fetch the default set of models for this collection, resetting the 846 // collection when they arrive. If `reset: true` is passed, the response 847 // data will be passed through the `reset` method instead of `set`. 848 fetch: function(options) { 849 options = options ? _.clone(options) : {}; 850 if (options.parse === void 0) options.parse = true; 851 var success = options.success; 852 var collection = this; 853 options.success = function(resp) { 854 var method = options.reset ? 'reset' : 'set'; 855 collection[method](resp, options); 856 if (success) success(collection, resp, options); 857 collection.trigger('sync', collection, resp, options); 858 }; 859 wrapError(this, options); 860 return this.sync('read', this, options); 861 }, 862 863 // Create a new instance of a model in this collection. Add the model to the 864 // collection immediately, unless `wait: true` is passed, in which case we 865 // wait for the server to agree. 866 create: function(model, options) { 867 options = options ? _.clone(options) : {}; 868 if (!(model = this._prepareModel(model, options))) return false; 869 if (!options.wait) this.add(model, options); 870 var collection = this; 871 var success = options.success; 872 options.success = function(resp) { 873 if (options.wait) collection.add(model, options); 874 if (success) success(model, resp, options); 875 }; 876 model.save(null, options); 877 return model; 878 }, 879 880 // **parse** converts a response into a list of models to be added to the 881 // collection. The default implementation is just to pass it through. 882 parse: function(resp, options) { 883 return resp; 884 }, 885 886 // Create a new collection with an identical list of models as this one. 887 clone: function() { 888 return new this.constructor(this.models); 889 }, 890 891 // Private method to reset all internal state. Called when the collection 892 // is first initialized or reset. 893 _reset: function() { 894 this.length = 0; 895 this.models = []; 896 this._byId = {}; 897 }, 898 899 // Prepare a hash of attributes (or other model) to be added to this 900 // collection. 901 _prepareModel: function(attrs, options) { 902 if (attrs instanceof Model) { 903 if (!attrs.collection) attrs.collection = this; 904 return attrs; 905 } 906 options || (options = {}); 907 options.collection = this; 908 var model = new this.model(attrs, options); 909 if (!model._validate(attrs, options)) { 910 this.trigger('invalid', this, attrs, options); 911 return false; 912 } 913 return model; 914 }, 915 916 // Internal method to sever a model's ties to a collection. 917 _removeReference: function(model) { 918 if (this === model.collection) delete model.collection; 919 model.off('all', this._onModelEvent, this); 920 }, 921 922 // Internal method called every time a model in the set fires an event. 923 // Sets need to update their indexes when models change ids. All other 924 // events simply proxy through. "add" and "remove" events that originate 925 // in other collections are ignored. 926 _onModelEvent: function(event, model, collection, options) { 927 if ((event === 'add' || event === 'remove') && collection !== this) return; 928 if (event === 'destroy') this.remove(model, options); 929 if (model && event === 'change:' + model.idAttribute) { 930 delete this._byId[model.previous(model.idAttribute)]; 931 if (model.id != null) this._byId[model.id] = model; 932 } 933 this.trigger.apply(this, arguments); 934 } 935 936 }); 937 938 // Underscore methods that we want to implement on the Collection. 939 // 90% of the core usefulness of Backbone Collections is actually implemented 940 // right here: 941 var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 942 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 943 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 944 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 945 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 946 'isEmpty', 'chain']; 947 948 // Mix in each Underscore method as a proxy to `Collection#models`. 949 _.each(methods, function(method) { 950 Collection.prototype[method] = function() { 951 var args = slice.call(arguments); 952 args.unshift(this.models); 953 return _[method].apply(_, args); 954 }; 955 }); 956 957 // Underscore methods that take a property name as an argument. 958 var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 959 960 // Use attributes instead of properties. 961 _.each(attributeMethods, function(method) { 962 Collection.prototype[method] = function(value, context) { 963 var iterator = _.isFunction(value) ? value : function(model) { 964 return model.get(value); 965 }; 966 return _[method](this.models, iterator, context); 967 }; 968 }); 969 970 // Backbone.View 971 // ------------- 972 973 // Backbone Views are almost more convention than they are actual code. A View 974 // is simply a JavaScript object that represents a logical chunk of UI in the 975 // DOM. This might be a single item, an entire list, a sidebar or panel, or 976 // even the surrounding frame which wraps your whole app. Defining a chunk of 977 // UI as a **View** allows you to define your DOM events declaratively, without 978 // having to worry about render order ... and makes it easy for the view to 979 // react to specific changes in the state of your models. 980 981 // Creating a Backbone.View creates its initial element outside of the DOM, 982 // if an existing element is not provided... 983 var View = Backbone.View = function(options) { 984 this.cid = _.uniqueId('view'); 985 this._configure(options || {}); 986 this._ensureElement(); 987 this.initialize.apply(this, arguments); 988 this.delegateEvents(); 989 }; 990 991 // Cached regex to split keys for `delegate`. 992 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 993 994 // List of view options to be merged as properties. 995 var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 996 997 // Set up all inheritable **Backbone.View** properties and methods. 998 _.extend(View.prototype, Events, { 999 1000 // The default `tagName` of a View's element is `"div"`. 1001 tagName: 'div', 1002 1003 // jQuery delegate for element lookup, scoped to DOM elements within the 1004 // current view. This should be prefered to global lookups where possible. 1005 $: function(selector) { 1006 return this.$el.find(selector); 1007 }, 1008 1009 // Initialize is an empty function by default. Override it with your own 1010 // initialization logic. 1011 initialize: function(){}, 1012 1013 // **render** is the core function that your view should override, in order 1014 // to populate its element (`this.el`), with the appropriate HTML. The 1015 // convention is for **render** to always return `this`. 1016 render: function() { 1017 return this; 1018 }, 1019 1020 // Remove this view by taking the element out of the DOM, and removing any 1021 // applicable Backbone.Events listeners. 1022 remove: function() { 1023 this.$el.remove(); 1024 this.stopListening(); 1025 return this; 1026 }, 1027 1028 // Change the view's element (`this.el` property), including event 1029 // re-delegation. 1030 setElement: function(element, delegate) { 1031 if (this.$el) this.undelegateEvents(); 1032 this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1033 this.el = this.$el[0]; 1034 if (delegate !== false) this.delegateEvents(); 1035 return this; 1036 }, 1037 1038 // Set callbacks, where `this.events` is a hash of 1039 // 1040 // *{"event selector": "callback"}* 1041 // 1042 // { 1043 // 'mousedown .title': 'edit', 1044 // 'click .button': 'save' 1045 // 'click .open': function(e) { ... } 1046 // } 1047 // 1048 // pairs. Callbacks will be bound to the view, with `this` set properly. 1049 // Uses event delegation for efficiency. 1050 // Omitting the selector binds the event to `this.el`. 1051 // This only works for delegate-able events: not `focus`, `blur`, and 1052 // not `change`, `submit`, and `reset` in Internet Explorer. 1053 delegateEvents: function(events) { 1054 if (!(events || (events = _.result(this, 'events')))) return this; 1055 this.undelegateEvents(); 1056 for (var key in events) { 1057 var method = events[key]; 1058 if (!_.isFunction(method)) method = this[events[key]]; 1059 if (!method) continue; 1060 1061 var match = key.match(delegateEventSplitter); 1062 var eventName = match[1], selector = match[2]; 1063 method = _.bind(method, this); 1064 eventName += '.delegateEvents' + this.cid; 1065 if (selector === '') { 1066 this.$el.on(eventName, method); 1067 } else { 1068 this.$el.on(eventName, selector, method); 1069 } 1070 } 1071 return this; 1072 }, 1073 1074 // Clears all callbacks previously bound to the view with `delegateEvents`. 1075 // You usually don't need to use this, but may wish to if you have multiple 1076 // Backbone views attached to the same DOM element. 1077 undelegateEvents: function() { 1078 this.$el.off('.delegateEvents' + this.cid); 1079 return this; 1080 }, 1081 1082 // Performs the initial configuration of a View with a set of options. 1083 // Keys with special meaning *(e.g. model, collection, id, className)* are 1084 // attached directly to the view. See `viewOptions` for an exhaustive 1085 // list. 1086 _configure: function(options) { 1087 if (this.options) options = _.extend({}, _.result(this, 'options'), options); 1088 _.extend(this, _.pick(options, viewOptions)); 1089 this.options = options; 1090 }, 1091 1092 // Ensure that the View has a DOM element to render into. 1093 // If `this.el` is a string, pass it through `$()`, take the first 1094 // matching element, and re-assign it to `el`. Otherwise, create 1095 // an element from the `id`, `className` and `tagName` properties. 1096 _ensureElement: function() { 1097 if (!this.el) { 1098 var attrs = _.extend({}, _.result(this, 'attributes')); 1099 if (this.id) attrs.id = _.result(this, 'id'); 1100 if (this.className) attrs['class'] = _.result(this, 'className'); 1101 var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1102 this.setElement($el, false); 1103 } else { 1104 this.setElement(_.result(this, 'el'), false); 1105 } 1106 } 1107 1108 }); 1109 1110 // Backbone.sync 1111 // ------------- 1112 1113 // Override this function to change the manner in which Backbone persists 1114 // models to the server. You will be passed the type of request, and the 1115 // model in question. By default, makes a RESTful Ajax request 1116 // to the model's `url()`. Some possible customizations could be: 1117 // 1118 // * Use `setTimeout` to batch rapid-fire updates into a single request. 1119 // * Send up the models as XML instead of JSON. 1120 // * Persist models via WebSockets instead of Ajax. 1121 // 1122 // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1123 // as `POST`, with a `_method` parameter containing the true HTTP method, 1124 // as well as all requests with the body as `application/x-www-form-urlencoded` 1125 // instead of `application/json` with the model in a param named `model`. 1126 // Useful when interfacing with server-side languages like **PHP** that make 1127 // it difficult to read the body of `PUT` requests. 1128 Backbone.sync = function(method, model, options) { 1129 var type = methodMap[method]; 1130 1131 // Default options, unless specified. 1132 _.defaults(options || (options = {}), { 1133 emulateHTTP: Backbone.emulateHTTP, 1134 emulateJSON: Backbone.emulateJSON 1135 }); 1136 1137 // Default JSON-request options. 1138 var params = {type: type, dataType: 'json'}; 1139 1140 // Ensure that we have a URL. 1141 if (!options.url) { 1142 params.url = _.result(model, 'url') || urlError(); 1143 } 1144 1145 // Ensure that we have the appropriate request data. 1146 if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1147 params.contentType = 'application/json'; 1148 params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1149 } 1150 1151 // For older servers, emulate JSON by encoding the request into an HTML-form. 1152 if (options.emulateJSON) { 1153 params.contentType = 'application/x-www-form-urlencoded'; 1154 params.data = params.data ? {model: params.data} : {}; 1155 } 1156 1157 // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1158 // And an `X-HTTP-Method-Override` header. 1159 if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1160 params.type = 'POST'; 1161 if (options.emulateJSON) params.data._method = type; 1162 var beforeSend = options.beforeSend; 1163 options.beforeSend = function(xhr) { 1164 xhr.setRequestHeader('X-HTTP-Method-Override', type); 1165 if (beforeSend) return beforeSend.apply(this, arguments); 1166 }; 1167 } 1168 1169 // Don't process data on a non-GET request. 1170 if (params.type !== 'GET' && !options.emulateJSON) { 1171 params.processData = false; 1172 } 1173 1174 // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1175 // that still has ActiveX enabled by default, override jQuery to use that 1176 // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1177 if (params.type === 'PATCH' && window.ActiveXObject && 1178 !(window.external && window.external.msActiveXFilteringEnabled)) { 1179 params.xhr = function() { 1180 return new ActiveXObject("Microsoft.XMLHTTP"); 1181 }; 1182 } 1183 1184 // Make the request, allowing the user to override any Ajax options. 1185 var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1186 model.trigger('request', model, xhr, options); 1187 return xhr; 1188 }; 1189 1190 // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1191 var methodMap = { 1192 'create': 'POST', 1193 'update': 'PUT', 1194 'patch': 'PATCH', 1195 'delete': 'DELETE', 1196 'read': 'GET' 1197 }; 1198 1199 // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1200 // Override this if you'd like to use a different library. 1201 Backbone.ajax = function() { 1202 return Backbone.$.ajax.apply(Backbone.$, arguments); 1203 }; 1204 1205 // Backbone.Router 1206 // --------------- 1207 1208 // Routers map faux-URLs to actions, and fire events when routes are 1209 // matched. Creating a new one sets its `routes` hash, if not set statically. 1210 var Router = Backbone.Router = function(options) { 1211 options || (options = {}); 1212 if (options.routes) this.routes = options.routes; 1213 this._bindRoutes(); 1214 this.initialize.apply(this, arguments); 1215 }; 1216 1217 // Cached regular expressions for matching named param parts and splatted 1218 // parts of route strings. 1219 var optionalParam = /\((.*?)\)/g; 1220 var namedParam = /(\(\?)?:\w+/g; 1221 var splatParam = /\*\w+/g; 1222 var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1223 1224 // Set up all inheritable **Backbone.Router** properties and methods. 1225 _.extend(Router.prototype, Events, { 1226 1227 // Initialize is an empty function by default. Override it with your own 1228 // initialization logic. 1229 initialize: function(){}, 1230 1231 // Manually bind a single named route to a callback. For example: 1232 // 1233 // this.route('search/:query/p:num', 'search', function(query, num) { 1234 // ... 1235 // }); 1236 // 1237 route: function(route, name, callback) { 1238 if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1239 if (_.isFunction(name)) { 1240 callback = name; 1241 name = ''; 1242 } 1243 if (!callback) callback = this[name]; 1244 var router = this; 1245 Backbone.history.route(route, function(fragment) { 1246 var args = router._extractParameters(route, fragment); 1247 callback && callback.apply(router, args); 1248 router.trigger.apply(router, ['route:' + name].concat(args)); 1249 router.trigger('route', name, args); 1250 Backbone.history.trigger('route', router, name, args); 1251 }); 1252 return this; 1253 }, 1254 1255 // Simple proxy to `Backbone.history` to save a fragment into the history. 1256 navigate: function(fragment, options) { 1257 Backbone.history.navigate(fragment, options); 1258 return this; 1259 }, 1260 1261 // Bind all defined routes to `Backbone.history`. We have to reverse the 1262 // order of the routes here to support behavior where the most general 1263 // routes can be defined at the bottom of the route map. 1264 _bindRoutes: function() { 1265 if (!this.routes) return; 1266 this.routes = _.result(this, 'routes'); 1267 var route, routes = _.keys(this.routes); 1268 while ((route = routes.pop()) != null) { 1269 this.route(route, this.routes[route]); 1270 } 1271 }, 1272 1273 // Convert a route string into a regular expression, suitable for matching 1274 // against the current location hash. 1275 _routeToRegExp: function(route) { 1276 route = route.replace(escapeRegExp, '\\$&') 1277 .replace(optionalParam, '(?:$1)?') 1278 .replace(namedParam, function(match, optional){ 1279 return optional ? match : '([^\/]+)'; 1280 }) 1281 .replace(splatParam, '(.*?)'); 1282 return new RegExp('^' + route + '$'); 1283 }, 1284 1285 // Given a route, and a URL fragment that it matches, return the array of 1286 // extracted decoded parameters. Empty or unmatched parameters will be 1287 // treated as `null` to normalize cross-browser behavior. 1288 _extractParameters: function(route, fragment) { 1289 var params = route.exec(fragment).slice(1); 1290 return _.map(params, function(param) { 1291 return param ? decodeURIComponent(param) : null; 1292 }); 1293 } 1294 1295 }); 1296 1297 // Backbone.History 1298 // ---------------- 1299 1300 // Handles cross-browser history management, based on either 1301 // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1302 // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1303 // and URL fragments. If the browser supports neither (old IE, natch), 1304 // falls back to polling. 1305 var History = Backbone.History = function() { 1306 this.handlers = []; 1307 _.bindAll(this, 'checkUrl'); 1308 1309 // Ensure that `History` can be used outside of the browser. 1310 if (typeof window !== 'undefined') { 1311 this.location = window.location; 1312 this.history = window.history; 1313 } 1314 }; 1315 1316 // Cached regex for stripping a leading hash/slash and trailing space. 1317 var routeStripper = /^[#\/]|\s+$/g; 1318 1319 // Cached regex for stripping leading and trailing slashes. 1320 var rootStripper = /^\/+|\/+$/g; 1321 1322 // Cached regex for detecting MSIE. 1323 var isExplorer = /msie [\w.]+/; 1324 1325 // Cached regex for removing a trailing slash. 1326 var trailingSlash = /\/$/; 1327 1328 // Has the history handling already been started? 1329 History.started = false; 1330 1331 // Set up all inheritable **Backbone.History** properties and methods. 1332 _.extend(History.prototype, Events, { 1333 1334 // The default interval to poll for hash changes, if necessary, is 1335 // twenty times a second. 1336 interval: 50, 1337 1338 // Gets the true hash value. Cannot use location.hash directly due to bug 1339 // in Firefox where location.hash will always be decoded. 1340 getHash: function(window) { 1341 var match = (window || this).location.href.match(/#(.*)$/); 1342 return match ? match[1] : ''; 1343 }, 1344 1345 // Get the cross-browser normalized URL fragment, either from the URL, 1346 // the hash, or the override. 1347 getFragment: function(fragment, forcePushState) { 1348 if (fragment == null) { 1349 if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1350 fragment = this.location.pathname; 1351 var root = this.root.replace(trailingSlash, ''); 1352 if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); 1353 } else { 1354 fragment = this.getHash(); 1355 } 1356 } 1357 return fragment.replace(routeStripper, ''); 1358 }, 1359 1360 // Start the hash change handling, returning `true` if the current URL matches 1361 // an existing route, and `false` otherwise. 1362 start: function(options) { 1363 if (History.started) throw new Error("Backbone.history has already been started"); 1364 History.started = true; 1365 1366 // Figure out the initial configuration. Do we need an iframe? 1367 // Is pushState desired ... is it available? 1368 this.options = _.extend({}, {root: '/'}, this.options, options); 1369 this.root = this.options.root; 1370 this._wantsHashChange = this.options.hashChange !== false; 1371 this._wantsPushState = !!this.options.pushState; 1372 this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1373 var fragment = this.getFragment(); 1374 var docMode = document.documentMode; 1375 var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1376 1377 // Normalize root to always include a leading and trailing slash. 1378 this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1379 1380 if (oldIE && this._wantsHashChange) { 1381 this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; 1382 this.navigate(fragment); 1383 } 1384 1385 // Depending on whether we're using pushState or hashes, and whether 1386 // 'onhashchange' is supported, determine how we check the URL state. 1387 if (this._hasPushState) { 1388 Backbone.$(window).on('popstate', this.checkUrl); 1389 } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { 1390 Backbone.$(window).on('hashchange', this.checkUrl); 1391 } else if (this._wantsHashChange) { 1392 this._checkUrlInterval = setInterval(this.checkUrl, this.interval); 1393 } 1394 1395 // Determine if we need to change the base url, for a pushState link 1396 // opened by a non-pushState browser. 1397 this.fragment = fragment; 1398 var loc = this.location; 1399 var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; 1400 1401 // If we've started off with a route from a `pushState`-enabled browser, 1402 // but we're currently in a browser that doesn't support it... 1403 if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { 1404 this.fragment = this.getFragment(null, true); 1405 this.location.replace(this.root + this.location.search + '#' + this.fragment); 1406 // Return immediately as browser will do redirect to new url 1407 return true; 1408 1409 // Or if we've started out with a hash-based route, but we're currently 1410 // in a browser where it could be `pushState`-based instead... 1411 } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { 1412 this.fragment = this.getHash().replace(routeStripper, ''); 1413 this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); 1414 } 1415 1416 if (!this.options.silent) return this.loadUrl(); 1417 }, 1418 1419 // Disable Backbone.history, perhaps temporarily. Not useful in a real app, 1420 // but possibly useful for unit testing Routers. 1421 stop: function() { 1422 Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); 1423 clearInterval(this._checkUrlInterval); 1424 History.started = false; 1425 }, 1426 1427 // Add a route to be tested when the fragment changes. Routes added later 1428 // may override previous routes. 1429 route: function(route, callback) { 1430 this.handlers.unshift({route: route, callback: callback}); 1431 }, 1432 1433 // Checks the current URL to see if it has changed, and if it has, 1434 // calls `loadUrl`, normalizing across the hidden iframe. 1435 checkUrl: function(e) { 1436 var current = this.getFragment(); 1437 if (current === this.fragment && this.iframe) { 1438 current = this.getFragment(this.getHash(this.iframe)); 1439 } 1440 if (current === this.fragment) return false; 1441 if (this.iframe) this.navigate(current); 1442 this.loadUrl() || this.loadUrl(this.getHash()); 1443 }, 1444 1445 // Attempt to load the current URL fragment. If a route succeeds with a 1446 // match, returns `true`. If no defined routes matches the fragment, 1447 // returns `false`. 1448 loadUrl: function(fragmentOverride) { 1449 var fragment = this.fragment = this.getFragment(fragmentOverride); 1450 var matched = _.any(this.handlers, function(handler) { 1451 if (handler.route.test(fragment)) { 1452 handler.callback(fragment); 1453 return true; 1454 } 1455 }); 1456 return matched; 1457 }, 1458 1459 // Save a fragment into the hash history, or replace the URL state if the 1460 // 'replace' option is passed. You are responsible for properly URL-encoding 1461 // the fragment in advance. 1462 // 1463 // The options object can contain `trigger: true` if you wish to have the 1464 // route callback be fired (not usually desirable), or `replace: true`, if 1465 // you wish to modify the current URL without adding an entry to the history. 1466 navigate: function(fragment, options) { 1467 if (!History.started) return false; 1468 if (!options || options === true) options = {trigger: options}; 1469 fragment = this.getFragment(fragment || ''); 1470 if (this.fragment === fragment) return; 1471 this.fragment = fragment; 1472 var url = this.root + fragment; 1473 1474 // If pushState is available, we use it to set the fragment as a real URL. 1475 if (this._hasPushState) { 1476 this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 1477 1478 // If hash changes haven't been explicitly disabled, update the hash 1479 // fragment to store history. 1480 } else if (this._wantsHashChange) { 1481 this._updateHash(this.location, fragment, options.replace); 1482 if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { 1483 // Opening and closing the iframe tricks IE7 and earlier to push a 1484 // history entry on hash-tag change. When replace is true, we don't 1485 // want this. 1486 if(!options.replace) this.iframe.document.open().close(); 1487 this._updateHash(this.iframe.location, fragment, options.replace); 1488 } 1489 1490 // If you've told us that you explicitly don't want fallback hashchange- 1491 // based history, then `navigate` becomes a page refresh. 1492 } else { 1493 return this.location.assign(url); 1494 } 1495 if (options.trigger) this.loadUrl(fragment); 1496 }, 1497 1498 // Update the hash location, either replacing the current entry, or adding 1499 // a new one to the browser history. 1500 _updateHash: function(location, fragment, replace) { 1501 if (replace) { 1502 var href = location.href.replace(/(javascript:|#).*$/, ''); 1503 location.replace(href + '#' + fragment); 1504 } else { 1505 // Some browsers require that `hash` contains a leading #. 1506 location.hash = '#' + fragment; 1507 } 1508 } 1509 1510 }); 1511 1512 // Create the default Backbone.history. 1513 Backbone.history = new History; 1514 1515 // Helpers 1516 // ------- 1517 1518 // Helper function to correctly set up the prototype chain, for subclasses. 1519 // Similar to `goog.inherits`, but uses a hash of prototype properties and 1520 // class properties to be extended. 1521 var extend = function(protoProps, staticProps) { 1522 var parent = this; 1523 var child; 1524 1525 // The constructor function for the new subclass is either defined by you 1526 // (the "constructor" property in your `extend` definition), or defaulted 1527 // by us to simply call the parent's constructor. 1528 if (protoProps && _.has(protoProps, 'constructor')) { 1529 child = protoProps.constructor; 1530 } else { 1531 child = function(){ return parent.apply(this, arguments); }; 1532 } 1533 1534 // Add static properties to the constructor function, if supplied. 1535 _.extend(child, parent, staticProps); 1536 1537 // Set the prototype chain to inherit from `parent`, without calling 1538 // `parent`'s constructor function. 1539 var Surrogate = function(){ this.constructor = child; }; 1540 Surrogate.prototype = parent.prototype; 1541 child.prototype = new Surrogate; 1542 1543 // Add prototype properties (instance properties) to the subclass, 1544 // if supplied. 1545 if (protoProps) _.extend(child.prototype, protoProps); 1546 1547 // Set a convenience property in case the parent's prototype is needed 1548 // later. 1549 child.__super__ = parent.prototype; 1550 1551 return child; 1552 }; 1553 1554 // Set up inheritance for the model, collection, router, view and history. 1555 Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; 1556 1557 // Throw an error when a URL is needed, and none is supplied. 1558 var urlError = function() { 1559 throw new Error('A "url" property or function must be specified'); 1560 }; 1561 1562 // Wrap an optional error callback with a fallback error event. 1563 var wrapError = function (model, options) { 1564 var error = options.error; 1565 options.error = function(resp) { 1566 if (error) error(model, resp, options); 1567 model.trigger('error', model, resp, options); 1568 }; 1569 }; 1570 1571 }).call(this);
© Reprint statement
This article was written by Harry
Link to this article:https://www.361sale.com/en/11653
The article is copyrighted and must be reproduced with attribution.
THE END
No comments