//i18n // function _t(s) { return s; } var i18n = { // format('hello, {0}', 'rambo') -> "hello, rambo" format: function (str) { for(var i = 1; i < arguments.length; ++i) { var attrs = arguments[i]; for(var attr in attrs) { str = str.replace(RegExp('\\{' + attr + '\\}', 'g'), attrs[attr]); } } return str; } }; (function() { // helper functions needed from backbone (they are not exported) var getValue = function(object, prop, method) { if (!(object && object[prop])) return null; return _.isFunction(object[prop]) ? object[prop](method) : object[prop]; }; // Throw an error when a URL is needed, and none is supplied. var urlError = function() { throw new Error('A "url" property or function must be specified'); }; // backbone.sync replacement to control url prefix Backbone.originalSync = Backbone.sync; Backbone.sync = function(method, model, options) { var url = options.url || getValue(model, 'url', method) || urlError(); // prefix if http is not present var absoluteUrl = url.indexOf('http') === 0 || url.indexOf("//") === 0; if (!absoluteUrl) { options.url = cdb.config.prefixUrl() + url; } else { options.url = url; } if (method !== 'read') { // remove everything related if (model.surrogateKeys) { Backbone.cachedSync.invalidateSurrogateKeys(getValue(model, 'surrogateKeys')); } } return Backbone.originalSync(method, model, options); }; Backbone.currentSync = Backbone.sync; Backbone.withCORS = function(method, model, options) { if (!options) { options = {}; } if (!options.crossDomain) { options.crossDomain = true; } if (!options.xhrFields) { options.xhrFields = { withCredentials: true }; } return Backbone.currentSync(method, model, options); }; // this method returns a cached version of backbone sync // take a look at https://github.com/teambox/backbone.memoized_sync/blob/master/backbone.memoized_sync.js // this is the same concept but implemented as a wrapper for ``Backbone.sync`` // usage: // initialize: function() { // this.sync = Backbone.cachedSync(this.user_name); // } Backbone.cachedSync = function(namespace, sync) { if (!namespace) { throw new Error("cachedSync needs a namespace as argument"); } var surrogateKey = namespace; var session = window.user_data && window.user_data.username; // no user session, no cache // there should be a session to have cache so we avoid // cache collision for someone with more than one account if (session) { namespace += "-" + session; } else { return Backbone.sync; } var namespaceKey = "cdb-cache/" + namespace; // saves all the localstore references to the namespace // inside localstore. It allows to remove all the references // at a time var index = { // return a list of references for the namespace _keys: function() { return JSON.parse(localStorage.getItem(namespaceKey) || '{}'); }, // add a new reference for the namespace add: function(key) { var keys = this._keys(); keys[key] = +new Date(); localStorage.setItem(namespaceKey, JSON.stringify(keys)); }, // remove all the references for the namespace invalidate: function() { var keys = this._keys(); _.each(keys, function(v, k) { localStorage.removeItem(k); }); localStorage.removeItem(namespaceKey); } } // localstore-like cache wrapper var cache = { setItem: function(key, value) { localStorage.setItem(key, value); index.add(key); return this; }, // this is async in case the data needs to be compressed getItem: function(key, callback) { var val = localStorage.getItem(key); _.defer(function() { callback(val); }); }, removeItem: function(key) { localStorage.removeItem(key); index.invalidate(); } } var cached = function(method, model, options) { var url = options.url || getValue(model, 'url') || urlError(); var key = namespaceKey + "/" + url; if (method === 'read') { var success = options.success; var cachedValue = null; options.success = function(resp, status, xhr) { // if cached value is ok if (cachedValue && xhr.responseText === cachedValue) { return; } cache.setItem(key, xhr.responseText); success(resp, status, xhr); } cache.getItem(key, function(val) { cachedValue = val; if (val) { success(JSON.parse(val), "success"); } }); } else { cache.removeItem(key); } return (sync || Backbone.sync)(method, model, options); } // create a public function to invalidate all the namespace // items cached.invalidate = function() { index.invalidate(); } // for testing and debugging porpuposes cached.cache = cache; // have a global namespace -> sync function in order to avoid invalidation Backbone.cachedSync.surrogateKeys[surrogateKey] = cached; return cached; } Backbone.cachedSync.surrogateKeys = {}; Backbone.cachedSync.invalidateSurrogateKeys = function(keys) { _.each(keys, function(k) { var s = Backbone.cachedSync.surrogateKeys[k]; if (s) { s.invalidate(); } else { cdb.log.debug("surrogate key not found: " + k); } }); } })(); Backbone.syncAbort = function() { var self = arguments[1]; if (self._xhr) { self._xhr.abort(); } self._xhr = Backbone.sync.apply(this, arguments); self._xhr.always(function() { self._xhr = null; }); return self._xhr; }; Backbone.delayedSaveSync = function(sync, delay) { var dsync = _.debounce(sync, delay); return function(method, model, options) { if (method === 'create' || method === 'update') { return dsync(method, model, options); } else { return sync(method, model, options); } } } Backbone.saveAbort = function() { var self = this; if (this._saving && this._xhr) { this._xhr.abort(); } this._saving = true; var xhr = Backbone.Model.prototype.save.apply(this, arguments); this._xhr = xhr; xhr.always(function() { self._saving = false; }); return xhr; };