// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://documentcloud.github.com/backbone (function(){ // Initial Setup // ------------- // The top-level namespace. var Backbone = {}; // Keep the version here in sync with `package.json`. Backbone.VERSION = '0.1.1'; // Export for both CommonJS and the browser. (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone; // Require Underscore, if we're on the server, and it's not already present. var _ = this._; if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; // For Backbone's purposes, jQuery owns the `$` variable. var $ = this.$; // Helper function to correctly set up the prototype chain, for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. var inherits = function(parent, protoProps, classProps) { var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor : function(){ return parent.apply(this, arguments); }; var ctor = function(){}; ctor.prototype = parent.prototype; child.prototype = new ctor(); _.extend(child.prototype, protoProps); if (classProps) _.extend(child, classProps); child.prototype.constructor = child; return child; }; // Helper function to get a URL from a Model or Collection as a property // or as a function. var getUrl = function(object) { return _.isFunction(object.url) ? object.url() : object.url; }; // Backbone.Events // ----------------- // A module that can be mixed in to *any object* in order to provide it with // custom events. You may `bind` or `unbind` a callback function to an event; // `trigger`-ing an event fires all callbacks in succession. // // var object = {}; // _.extend(object, Backbone.Events); // object.bind('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // Backbone.Events = { // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. bind : function(ev, callback) { var calls = this._callbacks || (this._callbacks = {}); var list = this._callbacks[ev] || (this._callbacks[ev] = []); list.push(callback); return this; }, // Remove one or many callbacks. If `callback` is null, removes all // callbacks for the event. If `ev` is null, removes all bound callbacks // for all events. unbind : function(ev, callback) { var calls; if (!ev) { this._callbacks = {}; } else if (calls = this._callbacks) { if (!callback) { calls[ev] = []; } else { var list = calls[ev]; if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { if (callback === list[i]) { list.splice(i, 1); break; } } } } return this; }, // Trigger an event, firing all bound callbacks. Callbacks are passed the // same arguments as `trigger` is, apart from the event name. // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { var list, calls, i, l; var calls = this._callbacks; if (!(calls = this._callbacks)) return this; if (list = calls[ev]) { for (i = 0, l = list.length; i < l; i++) { list[i].apply(this, _.rest(arguments)); } } if (list = calls['all']) { for (i = 0, l = list.length; i < l; i++) { list[i].apply(this, arguments); } } return this; } }; // Backbone.Model // -------------- // Create a new model, with defined attributes. A client id (`cid`) // is automatically generated and assigned for you. Backbone.Model = function(attributes) { this.attributes = {}; this.cid = _.uniqueId('c'); this.set(attributes || {}, {silent : true}); this._previousAttributes = _.clone(this.attributes); if (this.initialize) this.initialize(attributes); }; // Attach all inheritable methods to the Model prototype. _.extend(Backbone.Model.prototype, Backbone.Events, { // A snapshot of the model's previous attributes, taken immediately // after the last `changed` event was fired. _previousAttributes : null, // Has the item been changed since the last `changed` event? _changed : false, // Return a copy of the model's `attributes` object. toJSON : function() { return _.clone(this.attributes); }, // Get the value of an attribute. get : function(attr) { return this.attributes[attr]; }, // Set a hash of model attributes on the object, firing `changed` unless you // choose to silence it. set : function(attrs, options) { // Extract attributes and options. options || (options = {}); if (!attrs) return this; attrs = attrs.attributes || attrs; var now = this.attributes; // Run validation if `validate` is defined. if (this.validate) { var error = this.validate(attrs); if (error) { this.trigger('error', this, error); return false; } } // Check for changes of `id`. if ('id' in attrs) this.id = attrs.id; // Update attributes. for (var attr in attrs) { var val = attrs[attr]; if (val === '') val = null; if (!_.isEqual(now[attr], val)) { now[attr] = val; if (!options.silent) { this._changed = true; this.trigger('change:' + attr, this, val); } } } // Fire the `change` event, if the model has been changed. if (!options.silent && this._changed) this.change(); return this; }, // Remove an attribute from the model, firing `changed` unless you choose to // silence it. unset : function(attr, options) { options || (options = {}); var value = this.attributes[attr]; delete this.attributes[attr]; if (!options.silent) { this._changed = true; this.trigger('change:' + attr, this); this.change(); } return value; }, // Set a hash of model attributes, and sync the model to the server. // If the server returns an attributes hash that differs, the model's // state will be `set` again. save : function(attrs, options) { attrs || (attrs = {}); options || (options = {}); if (!this.set(attrs, options)) return false; var model = this; var success = function(resp) { if (!model.set(resp.model)) return false; if (options.success) options.success(model, resp); }; var method = this.isNew() ? 'create' : 'update'; Backbone.sync(method, this, success, options.error); return this; }, // Destroy this model on the server. Upon success, the model is removed // from its collection, if it has one. destroy : function(options) { options || (options = {}); var model = this; var success = function(resp) { if (model.collection) model.collection.remove(model); if (options.success) options.success(model, resp); }; Backbone.sync('delete', this, success, options.error); return this; }, // Default URL for the model's representation on the server -- if you're // using Backbone's restful methods, override this to change the endpoint // that will be called. url : function() { var base = getUrl(this.collection); if (this.isNew()) return base; return base + '/' + this.id; }, // Create a new model with identical attributes to this one. clone : function() { return new this.constructor(this); }, // A model is new if it has never been saved to the server, and has a negative // ID. isNew : function() { return !this.id; }, // Call this method to fire manually fire a `change` event for this model. // Calling this will cause all objects observing the model to update. change : function() { this.trigger('change', this); this._previousAttributes = _.clone(this.attributes); this._changed = false; }, // Determine if the model has changed since the last `changed` event. // If you specify an attribute name, determine if that attribute has changed. hasChanged : function(attr) { if (attr) return this._previousAttributes[attr] != this.attributes[attr]; return this._changed; }, // Return an object containing all the attributes that have changed, or false // if there are no changed attributes. Useful for determining what parts of a // view need to be updated and/or what attributes need to be persisted to // the server. changedAttributes : function(now) { var old = this._previousAttributes, now = now || this.attributes, changed = false; for (var attr in now) { if (!_.isEqual(old[attr], now[attr])) { changed = changed || {}; changed[attr] = now[attr]; } } return changed; }, // Get the previous value of an attribute, recorded at the time the last // `changed` event was fired. previous : function(attr) { if (!attr || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // Get all of the attributes of the model at the time of the previous // `changed` event. previousAttributes : function() { return _.clone(this._previousAttributes); } }); // Backbone.Collection // ------------------- // Provides a standard collection class for our sets of models, ordered // or unordered. If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. Backbone.Collection = function(models, options) { options || (options = {}); if (options.comparator) { this.comparator = options.comparator; delete options.comparator; } this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._reset(); if (models) this.refresh(models, {silent: true}); if (this.initialize) this.initialize(models, options); }; // Define the Collection's inheritable methods. _.extend(Backbone.Collection.prototype, Backbone.Events, { model : Backbone.Model, // Add a model, or list of models to the set. Pass **silent** to avoid // firing the `added` event for every new model. add : function(models, options) { if (!_.isArray(models)) return this._add(models, options); for (var i=0; i