/zan/vendors/js/mvc/backbone/backbone.js
JavaScript | 692 lines | 686 code | 6 blank | 0 comment | 248 complexity | 7f8d042b5163ea2cf25f9f557eb6274a MD5 | raw file
- (function(){
- var root = this;
- var previousBackbone = root.Backbone;
- var slice = Array.prototype.slice;
- var Backbone;
- if (typeof exports !== 'undefined') {
- Backbone = exports;
- } else {
- Backbone = root.Backbone = {};
- }
- Backbone.VERSION = '0.5.3';
- var _ = root._;
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
- var $ = root.jQuery || root.Zepto || root.ender;
- Backbone.noConflict = function() {
- root.Backbone = previousBackbone;
- return this;
- };
- Backbone.emulateHTTP = false;
- Backbone.emulateJSON = false;
- Backbone.Events = {
- bind : function(ev, callback, context) {
- var calls = this._callbacks || (this._callbacks = {});
- var list = calls[ev] || (calls[ev] = {});
- var tail = list.tail || (list.tail = list.next = {});
- tail.callback = callback;
- tail.context = context;
- list.tail = tail.next = {};
- return this;
- },
- unbind : function(ev, callback) {
- var calls, node, prev;
- if (!ev) {
- this._callbacks = null;
- } else if (calls = this._callbacks) {
- if (!callback) {
- calls[ev] = {};
- } else if (node = calls[ev]) {
- while ((prev = node) && (node = node.next)) {
- if (node.callback !== callback) continue;
- prev.next = node.next;
- node.context = node.callback = null;
- break;
- }
- }
- }
- return this;
- },
- trigger : function(eventName) {
- var node, calls, callback, args, ev, events = ['all', eventName];
- if (!(calls = this._callbacks)) return this;
- while (ev = events.pop()) {
- if (!(node = calls[ev])) continue;
- args = ev == 'all' ? arguments : slice.call(arguments, 1);
- while (node = node.next) if (callback = node.callback) callback.apply(node.context || this, args);
- }
- return this;
- }
- };
- Backbone.Model = function(attributes, options) {
- var defaults;
- attributes || (attributes = {});
- if (defaults = this.defaults) {
- if (_.isFunction(defaults)) defaults = defaults.call(this);
- attributes = _.extend({}, defaults, attributes);
- }
- this.attributes = {};
- this._escapedAttributes = {};
- this.cid = _.uniqueId('c');
- this.set(attributes, {silent : true});
- this._changed = false;
- this._previousAttributes = _.clone(this.attributes);
- if (options && options.collection) this.collection = options.collection;
- this.initialize(attributes, options);
- };
- _.extend(Backbone.Model.prototype, Backbone.Events, {
- _changed : false,
- idAttribute : 'id',
- initialize : function(){},
- toJSON : function() {
- return _.clone(this.attributes);
- },
- get : function(attr) {
- return this.attributes[attr];
- },
- escape : function(attr) {
- var html;
- if (html = this._escapedAttributes[attr]) return html;
- var val = this.attributes[attr];
- return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
- },
- has : function(attr) {
- return this.attributes[attr] != null;
- },
- set : function(key, value, options) {
- var attrs;
- if (_.isObject(key)) {
- attrs = key;
- options = value;
- } else {
- attrs = {};
- attrs[key] = value;
- }
- options || (options = {});
- if (!attrs) return this;
- if (attrs.attributes) attrs = attrs.attributes;
- if (options.unset) for (var attr in attrs) attrs[attr] = void 0;
- var now = this.attributes, escaped = this._escapedAttributes;
- if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
- if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
- var alreadyChanging = this._changing;
- this._changing = true;
- for (var attr in attrs) {
- var val = attrs[attr];
- if (!_.isEqual(now[attr], val) || (options.unset && (attr in now))) {
- options.unset ? delete now[attr] : now[attr] = val;
- delete escaped[attr];
- this._changed = true;
- if (!options.silent) this.trigger('change:' + attr, this, val, options);
- }
- }
- if (!alreadyChanging) {
- if (!options.silent && this._changed) this.change(options);
- this._changing = false;
- }
- return this;
- },
- unset : function(attr, options) {
- (options || (options = {})).unset = true;
- return this.set(attr, null, options);
- },
- clear : function(options) {
- (options || (options = {})).unset = true;
- return this.set(_.clone(this.attributes), options);
- },
- fetch : function(options) {
- options || (options = {});
- var model = this;
- var success = options.success;
- options.success = function(resp, status, xhr) {
- if (!model.set(model.parse(resp, xhr), options)) return false;
- if (success) success(model, resp);
- };
- options.error = wrapError(options.error, model, options);
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
- },
- save : function(attrs, options) {
- options || (options = {});
- if (attrs && !this.set(attrs, options)) return false;
- var model = this;
- var success = options.success;
- options.success = function(resp, status, xhr) {
- if (!model.set(model.parse(resp, xhr), options)) return false;
- if (success) success(model, resp, xhr);
- };
- options.error = wrapError(options.error, model, options);
- var method = this.isNew() ? 'create' : 'update';
- return (this.sync || Backbone.sync).call(this, method, this, options);
- },
- destroy : function(options) {
- options || (options = {});
- if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
- var model = this;
- var success = options.success;
- options.success = function(resp) {
- model.trigger('destroy', model, model.collection, options);
- if (success) success(model, resp);
- };
- options.error = wrapError(options.error, model, options);
- return (this.sync || Backbone.sync).call(this, 'delete', this, options);
- },
- url : function() {
- var base = getUrl(this.collection) || this.urlRoot || urlError();
- if (this.isNew()) return base;
- return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
- },
- parse : function(resp, xhr) {
- return resp;
- },
- clone : function() {
- return new this.constructor(this);
- },
- isNew : function() {
- return this.id == null;
- },
- change : function(options) {
- this.trigger('change', this, options);
- this._previousAttributes = _.clone(this.attributes);
- this._changed = false;
- },
- hasChanged : function(attr) {
- if (attr) return this._previousAttributes[attr] != this.attributes[attr];
- return this._changed;
- },
- changedAttributes : function(now) {
- if (!this._changed) return false;
- now || (now = this.attributes);
- var changed = false, old = this._previousAttributes;
- for (var attr in now) {
- if (_.isEqual(old[attr], now[attr])) continue;
- (changed || (changed = {}))[attr] = now[attr];
- }
- for (var attr in old) {
- if (!(attr in now)) (changed || (changed = {}))[attr] = void 0;
- }
- return changed;
- },
- previous : function(attr) {
- if (!attr || !this._previousAttributes) return null;
- return this._previousAttributes[attr];
- },
- previousAttributes : function() {
- return _.clone(this._previousAttributes);
- },
- _performValidation : function(attrs, options) {
- var error = this.validate(attrs, options);
- if (error) {
- if (options.error) {
- options.error(this, error, options);
- } else {
- this.trigger('error', this, error, options);
- }
- return false;
- }
- return true;
- }
- });
- Backbone.Collection = function(models, options) {
- options || (options = {});
- if (options.comparator) this.comparator = options.comparator;
- _.bindAll(this, '_onModelEvent', '_removeReference');
- this._reset();
- if (models) this.reset(models, {silent: true});
- this.initialize.apply(this, arguments);
- };
- _.extend(Backbone.Collection.prototype, Backbone.Events, {
- model : Backbone.Model,
- initialize : function(){},
- toJSON : function() {
- return this.map(function(model){ return model.toJSON(); });
- },
- add : function(models, options) {
- if (_.isArray(models)) {
- for (var i = 0, l = models.length; i < l; i++) {
- this._add(models[i], options);
- }
- } else {
- this._add(models, options);
- }
- return this;
- },
- remove : function(models, options) {
- if (_.isArray(models)) {
- for (var i = 0, l = models.length; i < l; i++) {
- this._remove(models[i], options);
- }
- } else {
- this._remove(models, options);
- }
- return this;
- },
- get : function(id) {
- if (id == null) return null;
- return this._byId[id.id != null ? id.id : id];
- },
- getByCid : function(cid) {
- return cid && this._byCid[cid.cid || cid];
- },
- at : function(index) {
- return this.models[index];
- },
- sort : function(options) {
- options || (options = {});
- if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
- this.models = this.sortBy(this.comparator);
- if (!options.silent) this.trigger('reset', this, options);
- return this;
- },
- pluck : function(attr) {
- return _.map(this.models, function(model){ return model.get(attr); });
- },
- reset : function(models, options) {
- models || (models = []);
- options || (options = {});
- this.each(this._removeReference);
- this._reset();
- this.add(models, {silent: true});
- if (!options.silent) this.trigger('reset', this, options);
- return this;
- },
- fetch : function(options) {
- options || (options = {});
- var collection = this;
- var success = options.success;
- options.success = function(resp, status, xhr) {
- collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
- if (success) success(collection, resp);
- };
- options.error = wrapError(options.error, collection, options);
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
- },
- create : function(model, options) {
- var coll = this;
- options || (options = {});
- model = this._prepareModel(model, options);
- if (!model) return false;
- var success = options.success;
- options.success = function(nextModel, resp, xhr) {
- coll.add(nextModel, options);
- if (success) success(nextModel, resp, xhr);
- };
- model.save(null, options);
- return model;
- },
- parse : function(resp, xhr) {
- return resp;
- },
- chain : function () {
- return _(this.models).chain();
- },
- _reset : function(options) {
- this.length = 0;
- this.models = [];
- this._byId = {};
- this._byCid = {};
- },
- _prepareModel : function(model, options) {
- if (!(model instanceof Backbone.Model)) {
- var attrs = model;
- model = new this.model(attrs, {collection: this});
- if (model.validate && !model._performValidation(model.attributes, options)) model = false;
- } else if (!model.collection) {
- model.collection = this;
- }
- return model;
- },
- _add : function(model, options) {
- options || (options = {});
- model = this._prepareModel(model, options);
- if (!model) return false;
- var already = this.getByCid(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 = options.at != null ? options.at :
- this.comparator ? this.sortedIndex(model, this.comparator) :
- this.length;
- this.models.splice(index, 0, model);
- model.bind('all', this._onModelEvent);
- this.length++;
- options.index = index;
- if (!options.silent) model.trigger('add', model, this, options);
- return model;
- },
- _remove : function(model, options) {
- options || (options = {});
- model = this.getByCid(model) || this.get(model);
- if (!model) return null;
- delete this._byId[model.id];
- delete this._byCid[model.cid];
- var index = this.indexOf(model);
- this.models.splice(index, 1);
- this.length--;
- options.index = index;
- if (!options.silent) model.trigger('remove', model, this, options);
- this._removeReference(model);
- return model;
- },
- _removeReference : function(model) {
- if (this == model.collection) {
- delete model.collection;
- }
- model.unbind('all', this._onModelEvent);
- },
- _onModelEvent : function(ev, model, collection, options) {
- if ((ev == 'add' || ev == 'remove') && collection != this) return;
- if (ev == 'destroy') {
- this._remove(model, options);
- }
- if (model && ev === 'change:' + model.idAttribute) {
- delete this._byId[model.previous(model.idAttribute)];
- this._byId[model.id] = model;
- }
- this.trigger.apply(this, arguments);
- }
- });
- var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
- 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
- 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
- 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
- _.each(methods, function(method) {
- Backbone.Collection.prototype[method] = function() {
- return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
- };
- });
- Backbone.Router = function(options) {
- options || (options = {});
- if (options.routes) this.routes = options.routes;
- this._bindRoutes();
- this.initialize.apply(this, arguments);
- };
- var namedParam = /:([\w\d]+)/g;
- var splatParam = /\*([\w\d]+)/g;
- var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
- _.extend(Backbone.Router.prototype, Backbone.Events, {
- initialize : function(){},
- route : function(route, name, callback) {
- Backbone.history || (Backbone.history = new Backbone.History);
- if (!_.isRegExp(route)) route = this._routeToRegExp(route);
- Backbone.history.route(route, _.bind(function(fragment) {
- var args = this._extractParameters(route, fragment);
- callback && callback.apply(this, args);
- this.trigger.apply(this, ['route:' + name].concat(args));
- }, this));
- },
- navigate : function(fragment, options) {
- Backbone.history.navigate(fragment, options);
- },
- _bindRoutes : function() {
- if (!this.routes) return;
- var routes = [];
- for (var route in this.routes) {
- routes.unshift([route, this.routes[route]]);
- }
- for (var i = 0, l = routes.length; i < l; i++) {
- this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
- }
- },
- _routeToRegExp : function(route) {
- route = route.replace(escapeRegExp, "\\$&")
- .replace(namedParam, "([^\/]*)")
- .replace(splatParam, "(.*?)");
- return new RegExp('^' + route + '$');
- },
- _extractParameters : function(route, fragment) {
- return route.exec(fragment).slice(1);
- }
- });
- Backbone.History = function() {
- this.handlers = [];
- _.bindAll(this, 'checkUrl');
- };
- var hashStrip = /^#*/;
- var isExplorer = /msie [\w.]+/;
- var historyStarted = false;
- _.extend(Backbone.History.prototype, {
- interval: 50,
- getFragment : function(fragment, forcePushState) {
- if (fragment == null) {
- if (this._hasPushState || forcePushState) {
- fragment = window.location.pathname;
- var search = window.location.search;
- if (search) fragment += search;
- } else {
- fragment = window.location.hash;
- }
- }
- fragment = decodeURIComponent(fragment.replace(hashStrip, ''));
- if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
- return fragment;
- },
- start : function(options) {
- if (historyStarted) throw new Error("Backbone.history has already been started");
- this.options = _.extend({}, {root: '/'}, this.options, options);
- this._wantsPushState = !!this.options.pushState;
- this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
- var fragment = this.getFragment();
- var docMode = document.documentMode;
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
- if (oldIE) {
- this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
- this.navigate(fragment);
- }
- if (this._hasPushState) {
- $(window).bind('popstate', this.checkUrl);
- } else if ('onhashchange' in window && !oldIE) {
- $(window).bind('hashchange', this.checkUrl);
- } else {
- setInterval(this.checkUrl, this.interval);
- }
- this.fragment = fragment;
- historyStarted = true;
- var loc = window.location;
- var atRoot = loc.pathname == this.options.root;
- if (this._wantsPushState && !this._hasPushState && !atRoot) {
- this.fragment = this.getFragment(null, true);
- window.location.replace(this.options.root + '#' + this.fragment);
- return true;
- } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
- this.fragment = loc.hash.replace(hashStrip, '');
- window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
- }
- if (!this.options.silent) {
- return this.loadUrl();
- }
- },
- route : function(route, callback) {
- this.handlers.unshift({route : route, callback : callback});
- },
- checkUrl : function(e) {
- var current = this.getFragment();
- if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
- if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
- if (this.iframe) this.navigate(current);
- this.loadUrl() || this.loadUrl(window.location.hash);
- },
- loadUrl : function(fragmentOverride) {
- var fragment = this.fragment = this.getFragment(fragmentOverride);
- var matched = _.any(this.handlers, function(handler) {
- if (handler.route.test(fragment)) {
- handler.callback(fragment);
- return true;
- }
- });
- return matched;
- },
- navigate : function(fragment, options) {
- if (!options || options === true) options = {trigger: options};
- var frag = (fragment || '').replace(hashStrip, '');
- if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
- if (this._hasPushState) {
- if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
- this.fragment = frag;
- window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
- } else {
- this.fragment = frag;
- this._updateHash(window.location, frag, options.replace);
- if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
- if(!options.replace) this.iframe.document.open().close();
- this._updateHash(this.iframe.location, frag, options.replace);
- }
- }
- if (options.trigger) this.loadUrl(fragment);
- },
- _updateHash: function(location, fragment, replace) {
- if (replace) {
- location.replace(location.toString().replace(/(javascript:|#).*$/, "") + "#" + fragment);
- } else {
- location.hash = fragment;
- }
- }
- });
- Backbone.View = function(options) {
- this.cid = _.uniqueId('view');
- this._configure(options || {});
- this._ensureElement();
- this.delegateEvents();
- this.initialize.apply(this, arguments);
- };
- var eventSplitter = /^(\S+)\s*(.*)$/;
- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
- _.extend(Backbone.View.prototype, Backbone.Events, {
- tagName : 'div',
- $ : function(selector) {
- return (selector == null) ? $(this.el) : $(selector, this.el);
- },
- initialize : function(){},
- render : function() {
- return this;
- },
- remove : function() {
- $(this.el).remove();
- return this;
- },
- make : function(tagName, attributes, content) {
- var el = document.createElement(tagName);
- if (attributes) $(el).attr(attributes);
- if (content) $(el).html(content);
- return el;
- },
- delegateEvents : function(events) {
- if (!(events || (events = this.events))) return;
- if (_.isFunction(events)) events = events.call(this);
- this.undelegateEvents();
- for (var key in events) {
- var method = this[events[key]];
- if (!method) throw new Error('Event "' + events[key] + '" does not exist');
- var match = key.match(eventSplitter);
- var eventName = match[1], selector = match[2];
- method = _.bind(method, this);
- eventName += '.delegateEvents' + this.cid;
- if (selector === '') {
- $(this.el).bind(eventName, method);
- } else {
- $(this.el).delegate(selector, eventName, method);
- }
- }
- },
- undelegateEvents : function() {
- $(this.el).unbind('.delegateEvents' + this.cid);
- },
- _configure : function(options) {
- if (this.options) options = _.extend({}, this.options, options);
- for (var i = 0, l = viewOptions.length; i < l; i++) {
- var attr = viewOptions[i];
- if (options[attr]) this[attr] = options[attr];
- }
- this.options = options;
- },
- _ensureElement : function() {
- if (!this.el) {
- var attrs = this.attributes || {};
- if (this.id) attrs.id = this.id;
- if (this.className) attrs['class'] = this.className;
- this.el = this.make(this.tagName, attrs);
- } else if (_.isString(this.el)) {
- this.el = $(this.el).get(0);
- }
- }
- });
- var extend = function (protoProps, classProps) {
- var child = inherits(this, protoProps, classProps);
- child.extend = this.extend;
- return child;
- };
- Backbone.Model.extend = Backbone.Collection.extend =
- Backbone.Router.extend = Backbone.View.extend = extend;
- var methodMap = {
- 'create': 'POST',
- 'update': 'PUT',
- 'delete': 'DELETE',
- 'read' : 'GET'
- };
- Backbone.sync = function(method, model, options) {
- var type = methodMap[method];
- var params = {type : type, dataType : 'json'};
- if (!options.url) {
- params.url = getUrl(model) || urlError();
- }
- if (!options.data && model && (method == 'create' || method == 'update')) {
- params.contentType = 'application/json';
- params.data = JSON.stringify(model.toJSON());
- }
- if (Backbone.emulateJSON) {
- params.contentType = 'application/x-www-form-urlencoded';
- params.data = params.data ? {model : params.data} : {};
- }
- if (Backbone.emulateHTTP) {
- if (type === 'PUT' || type === 'DELETE') {
- if (Backbone.emulateJSON) params.data._method = type;
- params.type = 'POST';
- params.beforeSend = function(xhr) {
- xhr.setRequestHeader('X-HTTP-Method-Override', type);
- };
- }
- }
- if (params.type !== 'GET' && !Backbone.emulateJSON) {
- params.processData = false;
- }
- return $.ajax(_.extend(params, options));
- };
- var ctor = function(){};
- var inherits = function(parent, protoProps, staticProps) {
- var child;
- if (protoProps && protoProps.hasOwnProperty('constructor')) {
- child = protoProps.constructor;
- } else {
- child = function(){ return parent.apply(this, arguments); };
- }
- _.extend(child, parent);
- ctor.prototype = parent.prototype;
- child.prototype = new ctor();
- if (protoProps) _.extend(child.prototype, protoProps);
- if (staticProps) _.extend(child, staticProps);
- child.prototype.constructor = child;
- child.__super__ = parent.prototype;
- return child;
- };
- var getUrl = function(object) {
- if (!(object && object.url)) return null;
- return _.isFunction(object.url) ? object.url() : object.url;
- };
- var urlError = function() {
- throw new Error('A "url" property or function must be specified');
- };
- var wrapError = function(onError, originalModel, options) {
- return function(model, resp) {
- var resp = model === originalModel ? resp : model;
- if (onError) {
- onError(model, resp, options);
- } else {
- originalModel.trigger('error', model, resp, options);
- }
- };
- };
- }).call(this);