// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the terms of 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 in sync with `package.json`. Backbone.VERSION = '0.1.0'; // Export for both CommonJS and the Browser. (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone; // Helper function to correctly set up the prototype chain, for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // static 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; }; // Backbone.Bindable // ----------------- // 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. // // _.extend(object, Backbone.Bindable); // object.bind('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // Backbone.Bindable = { // 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]; 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. trigger : function(ev) { var calls = this._callbacks; for (var i = 0; i < 2; i++) { var list = calls && calls[i ? 'all' : ev]; if (!list) continue; for (var j = 0, l = list.length; j < l; j++) { list[j].apply(this, arguments); } } return this; } }; // Backbone.Model // -------------- // Create a new model, with defined attributes. // If you do not specify the id, a negative id will be assigned for you. Backbone.Model = function(attributes) { this._attributes = {}; attributes = attributes || {}; this.set(attributes, true); this.cid = _.uniqueId('c'); this._formerAttributes = this.attributes(); }; // Create a model on the server and add it to the set. // When the server returns a JSON representation of the model, we update it // on the client. Backbone.Model.create = function(attributes, options) { options || (options = {}); var model = new this(attributes); $.ajax({ url : model.set.resource, type : 'POST', data : {model : JSON.stringify(model.attributes())}, dataType : 'json', success : function(resp) { model.set(resp.model); if (options.success) return options.success(model, resp); }, error : function(resp) { if (options.error) options.error(model, resp); } }); return model; }; // Attach all inheritable methods to the Model prototype. _.extend(Backbone.Model.prototype, Backbone.Bindable, { // A snapshot of the model's previous attributes, taken immediately // after the last `changed` event was fired. _formerAttributes : null, // Has the item been changed since the last `changed` event? _changed : false, // Create a new model with identical attributes to this one. clone : function() { return new (this.constructor)(this.attributes()); }, // Are this model's attributes identical to another model? isEqual : function(other) { return other && _.isEqual(this._attributes, other._attributes); }, // 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 `changed` event for this model. // Calling this will cause all objects observing the model to update. changed : function() { this.trigger('change', this); this._formerAttributes = 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._formerAttributes[attr] != this._attributes[attr]; return this._changed; }, // Get the previous value of an attribute, recorded at the time the last // `changed` event was fired. formerValue : function(attr) { if (!attr || !this._formerAttributes) return null; return this._formerAttributes[attr]; }, // Get all of the attributes of the model at the time of the previous // `changed` event. formerAttributes : function() { return this._formerAttributes; }, // 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.formerAttributes(), 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; }, // Set a hash of model attributes on the object, firing `changed` unless you // choose to silence it. set : function(attrs, options) { options || (options = {}); if (!attrs) return this; attrs = attrs._attributes || attrs; var now = this._attributes; if (attrs.id) { this.id = attrs.id; if (this.collection) this.resource = this.collection.resource + '/' + this.id; } for (var attr in attrs) { var val = attrs[attr]; if (val === '') val = null; if (!_.isEqual(now[attr], val)) { if (!options.silent) this._changed = true; now[attr] = val; } } if (!options.silent && this._changed) this.changed(); return this; }, // Get the value of an attribute. get : function(attr) { return this._attributes[attr]; }, // 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(); return value; }, // Return a copy of the model's attributes. attributes : function() { return _.clone(this._attributes); }, // Bind all methods in the list to the model. bindAll : function() { _.bindAll.apply(_, [this].concat(arguments)); }, toString : function() { return 'Model ' + this.id; }, // Return the URL used to {save,delete} url : function() { if (!this.id) throw new Error(this.toString() + " has no id."); return this.collection.url() + '/' + this.id; }, // Set a hash of model attributes, and sync the model to the server. save : function(attrs, options) { options || (options = {}); this.set(attrs, options); var model = this; var success = function(resp) { model.set(resp.model); if (options.success) options.success(model, resp); }; Backbone.request('PUT', this, success, options.error); return this; }, // Destroy this model on the server. destroy : function(options) { Backbone.request('DELETE', this, options.success, options.error); return this; } }); // 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) { if (options && options.comparator) { this.comparator = options.comparator; delete options.comparator; } this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._initialize(); if (models) { this.refresh(models,true); } }; // Define the Collection's inheritable methods. _.extend(Backbone.Collection.prototype, Backbone.Bindable, { // Initialize or re-initialize all internal state. Called when the // collection is refreshed. _initialize : function() { this.length = 0; this.models = []; this._byId = {}; this._byCid = {}; }, // Get a model from the set by id. get : function(id) { return id && this._byId[id.id || id]; }, // Get a model from the set by client id. getByCid : function(cid) { return cid && this._byCid[cid.cid || cid]; }, // What are the ids for every model in the set? getIds : function() { return _.keys(this._byId); }, // What are the client ids for every model in the set? getCids : function() { return _.keys(this._byCid); }, // Get the model at the given index. at: function(index) { return this.models[index]; }, // 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, silent) { if (!_.isArray(models)) return this._add(models, silent); for (var i=0; i