mirror of
https://github.com/jashkenas/backbone.git
synced 2026-01-23 13:58:06 -05:00
527 lines
17 KiB
JavaScript
527 lines
17 KiB
JavaScript
// 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;
|
|
};
|
|
|
|
})(); |