UNPKG

lettuce

Version:

Lettuce JS, Mini Mobile Framework for Romantic with DSL.

532 lines (464 loc) 17.6 kB
// Backbone.js // (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. var inherits = function(parent, protoProps, classProps) { if (protoProps.hasOwnProperty('constructor')) { child = protoProps.constructor; } else { child = 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. 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 || {}; attributes.id = attributes.id || -_.uniqueId(); 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 < 0; }, // 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.collection) { this.collection = attrs.collection; delete attrs.collection; this.resource = this.collection.resource + '/' + this.id; } 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; }, // Set a hash of model attributes, and sync the model to the server. save : function(attrs, options) { if (!this.resource) throw new Error(this.toString() + " cannot be saved without a resource."); options || (options = {}); this.set(attrs, options); var model = this; $.ajax({ url : this.resource, type : 'PUT', data : {model : JSON.stringify(this.attributes())}, dataType : 'json', success : function(resp) { model.set(resp.model); if (options.success) options.success(model, resp); }, error : function(resp) { if (options.error) options.error(model, resp); } }); }, // 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; }, // Destroy this model on the server. destroy : function(options) { if (this.collection) this.collection.remove(this); $.ajax({ url : this.resource, type : 'DELETE', data : {}, dataType : 'json', success : function(resp) { if (options.success) options.success(model, resp); }, error : function(resp) { if (options.error) options.error(model, resp); } }); } }); // 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(options) { this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._initialize(); }; // 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<models.length; i++) this._add(models[i], silent); return models; }, // Internal implementation of adding a single model to the set. _add : function(model, silent) { var already = this.get(model); if (already) throw new Error(["Can't add the same model to a set twice", already.id]); this._byId[model.id] = model; this._byCid[model.cid] = model; var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length - 1; this.models.splice(index, 0, model); model.bind('all', this._boundOnModelEvent); this.length++; if (!silent) this.trigger('add', model); return model; }, // Remove a model, or a list of models from the set. Pass silent to avoid // firing the `removed` event for every model removed. remove : function(models, silent) { if (!_.isArray(models)) return this._remove(models, silent); for (var i=0; i<models.length; i++) this._remove(models[i], silent); return models; }, // Internal implementation of removing a single model from the set. _remove : function(model, silent) { model = this.get(model); if (!model) return null; delete this._byId[model.id]; delete this._byCid[model.cid]; this.models.splice(this.indexOf(model), 1); model.unbind('all', this._boundOnModelEvent); this.length--; if (!silent) this.trigger('remove', model); return model; }, // When you have more items than you want to add or remove individually, // you can refresh the entire set with a new list of models, without firing // any `added` or `removed` events. Fires `refreshed` when finished. refresh : function(models, silent) { models = models || []; if (models[0] && !(models[0] instanceof Backbone.Model)) { for (var i = 0, l = models.length; i < l; i++) { models[i].collection = this; models[i] = new this.model(models[i]); } } this._initialize(); this.add(models, true); if (!silent) this.trigger('refresh'); }, // Force the set to re-sort itself. You don't need to call this under normal // circumstances, as the set will maintain sort order as each item is added. sort : function(silent) { if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); if (!silent) this.trigger('refresh'); }, // Internal method called every time a model in the set fires an event. // Sets need to update their indexes when models change ids. _onModelEvent : function(ev, model) { if (ev == 'change') { if (model.hasChanged('id')) { delete this._byId[model.formerValue('id')]; this._byId[model.id] = model; } this.trigger('change', model); } }, // Inspect. toString : function() { return 'Set (' + this.length + " models)"; } }); // Underscore methods that we want to implement on the Collection. var methods = ['each', 'map', 'reduce', 'reduceRight', 'detect', 'select', 'reject', 'all', 'any', 'include', 'invoke', 'pluck', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { Backbone.Collection.prototype[method] = function() { return _[method].apply(_, [this.models].concat(_.toArray(arguments))); }; }); // Backbone View // ------------- Backbone.View = function(options) { this.modes = {}; this.configure(options || {}); if (this.options.el) { this.el = this.options.el; } else { var attrs = {}; if (this.id) attrs.id = this.id; if (this.className) attrs['class'] = this.className; this.el = this.make(this.tagName, attrs); } return this; }; // Set up all interitable view properties and methods. _.extend(Backbone.View.prototype, { el : null, model : null, modes : null, id : null, className : null, callbacks : null, options : null, tagName : 'div', configure : function(options) { if (this.options) options = _.extend({}, this.options, options); if (options.model) this.model = options.model; if (options.collection) this.collection = options.collection; if (options.id) this.id = options.id; if (options.className) this.className = options.className; this.options = options; }, render : function() { return this; }, // jQuery lookup, scoped to the current view. $ : function(selector) { return $(selector, this.el); }, // Quick-create a dom element with attributes. make : function(tagName, attributes, content) { var el = document.createElement(tagName); if (attributes) $(el).attr(attributes); if (content) $(el).html(content); return el; }, // Makes the view enter a mode. Modes have both a 'mode' and a 'group', // and are mutually exclusive with any other modes in the same group. // Setting will update the view's modes hash, as well as set an HTML className // of [mode]_[group] on the view's element. Convenient way to swap styles // and behavior. setMode : function(mode, group) { if (this.modes[group] == mode) return; $(this.el).setMode(mode, group); this.modes[group] = mode; }, // Set callbacks, where this.callbacks is a hash of // {selector.event_name, callback_name} // pairs. Callbacks will be bound to the view, with 'this' set properly. // Passing a selector of 'el' binds to the view's root element. // Change events are not delegated through the view because IE does not bubble // change events at all. setCallbacks : function(callbacks) { $(this.el).unbind(); if (!(callbacks || (callbacks = this.callbacks))) return this; for (key in callbacks) { var methodName = callbacks[key]; key = key.split(/\.(?!.*\.)/); var selector = key[0], eventName = key[1]; var method = _.bind(this[methodName], this); if (selector === '' || eventName == 'change') { $(this.el).bind(eventName, method); } else { $(this.el).delegate(selector, eventName, method); } } return this; } }); // Set up inheritance for the model, collection, and view. var extend = Backbone.Model.extend = Backbone.Collection.extend = Backbone.View.extend = function (protoProps, classProps) { var child = inherits(this, protoProps, classProps); child.extend = extend; return child; }; })();