(function (factory) { if (typeof define === 'function' && define.amd) { define(['jquery', 'sammy'], factory); } else { (window.Sammy = window.Sammy || {}).Storage = factory(window.jQuery, window.Sammy); } }(function ($, Sammy) { // Sammy.Store is an abstract adapter class that wraps the multitude of in // browser data storage into a single common set of methods for storing and // retreiving data. The JSON library is used (through the inclusion of the // Sammy.JSON) plugin, to automatically convert objects back and forth from // stored strings. // // Sammy.Store can be used directly, but within a Sammy.Application it is much // easier to use the `Sammy.Storage` plugin and its helper methods. // // Sammy.Store also supports the KVO pattern, by firing DOM/jQuery Events when // a key is set. // // ### Example // // // create a new store named 'mystore', tied to the #main element, using HTML5 localStorage // // Note: localStorage only works on browsers that support it // var store = new Sammy.Store({name: 'mystore', element: '#element', type: 'local'}); // store.set('foo', 'bar'); // store.get('foo'); //=> 'bar' // store.set('json', {obj: 'this is an obj'}); // store.get('json'); //=> {obj: 'this is an obj'} // store.keys(); //=> ['foo','json'] // store.clear('foo'); // store.keys(); //=> ['json'] // store.clearAll(); // store.keys(); //=> [] // // ### Arguments // // The constructor takes a single argument which is a Object containing these possible options. // // * `name` The name/namespace of this store. Stores are unique by name/type. (default 'store') // * `element` A selector for the element that the store is bound to. (default 'body') // * `type` The type of storage/proxy to use (default 'memory') // // Extra options are passed to the storage constructor. // Sammy.Store supports the following methods of storage: // // * `memory` Basic object storage // * `data` jQuery.data DOM Storage // * `cookie` Access to document.cookie. Limited to 2K // * `local` HTML5 DOM localStorage, browswer support is currently limited. // * `session` HTML5 DOM sessionStorage, browswer support is currently limited. // Sammy.Store = function(options) { var store = this; this.options = options || {}; this.name = this.options.name || 'store'; this.element = this.options.element || 'body'; this.$element = $(this.element); if ($.isArray(this.options.type)) { $.each(this.options.type, function(i, type) { if (Sammy.Store.isAvailable(type)) { store.type = type; return false; } }); } else { this.type = this.options.type || 'memory'; } this.meta_key = this.options.meta_key || '__keys__'; this.storage = new Sammy.Store[Sammy.Store.stores[this.type]](this.name, this.element, this.options); }; Sammy.Store.stores = { 'memory': 'Memory', 'data': 'Data', 'local': 'LocalStorage', 'session': 'SessionStorage', 'cookie': 'Cookie' }; $.extend(Sammy.Store.prototype, { // Checks for the availability of the current storage type in the current browser/config. isAvailable: function() { if ($.isFunction(this.storage.isAvailable)) { return this.storage.isAvailable(); } else { return true; } }, // Checks for the existance of `key` in the current store. Returns a boolean. exists: function(key) { return this.storage.exists(key); }, // Sets the value of `key` with `value`. If `value` is an // object, it is turned to and stored as a string with `JSON.stringify`. // It also tries to conform to the KVO pattern triggering jQuery events on the // element that the store is bound to. // // ### Example // // var store = new Sammy.Store({name: 'kvo'}); // $('body').bind('set-kvo-foo', function(e, data) { // Sammy.log(data.key + ' changed to ' + data.value); // }); // store.set('foo', 'bar'); // logged: foo changed to bar // set: function(key, value) { var string_value = (typeof value == 'string') ? value : JSON.stringify(value); key = key.toString(); this.storage.set(key, string_value); if (key != this.meta_key) { this._addKey(key); this.$element.trigger('set-' + this.name, {key: key, value: value}); this.$element.trigger('set-' + this.name + '-' + key, {key: key, value: value}); } // always return the original value return value; }, // Returns the set value at `key`, parsing with `JSON.parse` and // turning into an object if possible get: function(key) { var value = this.storage.get(key); if (typeof value == 'undefined' || value == null || value == '') { return value; } try { return JSON.parse(value); } catch(e) { return value; } }, // Removes the value at `key` from the current store clear: function(key) { this._removeKey(key); return this.storage.clear(key); }, // Clears all the values for the current store. clearAll: function() { var self = this; this.each(function(key, value) { self.clear(key); }); }, // Returns the all the keys set for the current store as an array. // Internally Sammy.Store keeps this array in a 'meta_key' for easy access. keys: function() { return this.get(this.meta_key) || []; }, // Iterates over each key value pair passing them to the `callback` function // // ### Example // // store.each(function(key, value) { // Sammy.log('key', key, 'value', value); // }); // each: function(callback) { var i = 0, keys = this.keys(), returned; for (i; i < keys.length; i++) { returned = callback(keys[i], this.get(keys[i])); if (returned === false) { return false; } } }, // Filters the store by a filter function that takes a key value. // Returns an array of arrays where the first element of each array // is the key and the second is the value of that key. // // ### Example // // var store = new Sammy.Store; // store.set('one', 'two'); // store.set('two', 'three'); // store.set('1', 'two'); // var returned = store.filter(function(key, value) { // // only return // return value === 'two'; // }); // // returned => [['one', 'two'], ['1', 'two']]; // filter: function(callback) { var found = []; this.each(function(key, value) { if (callback(key, value)) { found.push([key, value]); } return true; }); return found; }, // Works exactly like filter except only returns the first matching key // value pair instead of all of them first: function(callback) { var found = false; this.each(function(key, value) { if (callback(key, value)) { found = [key, value]; return false; } }); return found; }, // Returns the value at `key` if set, otherwise, runs the callback // and sets the value to the value returned in the callback. // // ### Example // // var store = new Sammy.Store; // store.exists('foo'); //=> false // store.fetch('foo', function() { // return 'bar!'; // }); //=> 'bar!' // store.get('foo') //=> 'bar!' // store.fetch('foo', function() { // return 'baz!'; // }); //=> 'bar! // fetch: function(key, callback) { if (!this.exists(key)) { return this.set(key, callback.apply(this)); } else { return this.get(key); } }, // loads the response of a request to `path` into `key`. // // ### Example // // In /mytemplate.tpl: // // My Template // // In app.js: // // var store = new Sammy.Store; // store.load('mytemplate', '/mytemplate.tpl', function() { // s.get('mytemplate') //=> My Template // }); // load: function(key, path, callback) { var s = this; $.get(path, function(response) { s.set(key, response); if (callback) { callback.apply(this, [response]); } }); }, _addKey: function(key) { var keys = this.keys(); if ($.inArray(key, keys) == -1) { keys.push(key); } this.set(this.meta_key, keys); }, _removeKey: function(key) { var keys = this.keys(); var index = $.inArray(key, keys); if (index != -1) { keys.splice(index, 1); } this.set(this.meta_key, keys); } }); // Tests if the type of storage is available/works in the current browser/config. // Especially useful for testing the availability of the awesome, but not widely // supported HTML5 DOM storage Sammy.Store.isAvailable = function(type) { try { return Sammy.Store[Sammy.Store.stores[type]].prototype.isAvailable(); } catch(e) { return false; } }; // Memory ('memory') is the basic/default store. It stores data in a global // JS object. Data is lost on refresh. Sammy.Store.Memory = function(name, element) { this.name = name; this.element = element; this.namespace = [this.element, this.name].join('.'); Sammy.Store.Memory.store = Sammy.Store.Memory.store || {}; Sammy.Store.Memory.store[this.namespace] = Sammy.Store.Memory.store[this.namespace] || {}; this.store = Sammy.Store.Memory.store[this.namespace]; }; $.extend(Sammy.Store.Memory.prototype, { isAvailable: function() { return true; }, exists: function(key) { return (typeof this.store[key] != "undefined"); }, set: function(key, value) { return this.store[key] = value; }, get: function(key) { return this.store[key]; }, clear: function(key) { delete this.store[key]; } }); // Data ('data') stores objects using the jQuery.data() methods. This has the advantadge // of scoping the data to the specific element. Like the 'memory' store its data // will only last for the length of the current request (data is lost on refresh/etc). Sammy.Store.Data = function(name, element) { this.name = name; this.element = element; this.$element = $(element); }; $.extend(Sammy.Store.Data.prototype, { isAvailable: function() { return true; }, exists: function(key) { return !!this.$element.data(this._key(key)); }, set: function(key, value) { return this.$element.data(this._key(key), value); }, get: function(key) { return this.$element.data(this._key(key)); }, clear: function(key) { this.$element.removeData(this._key(key)); }, _key: function(key) { return ['store', this.name, key].join('.'); } }); // LocalStorage ('local') makes use of HTML5 DOM Storage, and the window.localStorage // object. The great advantage of this method is that data will persist beyond // the current request. It can be considered a pretty awesome replacement for // cookies accessed via JS. The great disadvantage, though, is its only available // on the latest and greatest browsers. // // For more info on DOM Storage: // https://developer.mozilla.org/en/DOM/Storage // http://www.w3.org/TR/2009/WD-webstorage-20091222/ // Sammy.Store.LocalStorage = function(name, element) { this.name = name; this.element = element; }; $.extend(Sammy.Store.LocalStorage.prototype, { isAvailable: function() { return ('localStorage' in window) && (window.location.protocol != 'file:'); }, exists: function(key) { return (this.get(key) != null); }, set: function(key, value) { return window.localStorage.setItem(this._key(key), value); }, get: function(key) { return window.localStorage.getItem(this._key(key)); }, clear: function(key) { window.localStorage.removeItem(this._key(key));; }, _key: function(key) { return ['store', this.element, this.name, key].join('.'); } }); // .SessionStorage ('session') is similar to LocalStorage (part of the same API) // and shares similar browser support/availability. The difference is that // SessionStorage is only persistant through the current 'session' which is defined // as the length that the current window is open. This means that data will survive // refreshes but not close/open or multiple windows/tabs. For more info, check out // the `LocalStorage` documentation and links. Sammy.Store.SessionStorage = function(name, element) { this.name = name; this.element = element; }; $.extend(Sammy.Store.SessionStorage.prototype, { isAvailable: function() { return ('sessionStorage' in window) && (window.location.protocol != 'file:') && ($.isFunction(window.sessionStorage.setItem)); }, exists: function(key) { return (this.get(key) != null); }, set: function(key, value) { return window.sessionStorage.setItem(this._key(key), value); }, get: function(key) { var value = window.sessionStorage.getItem(this._key(key)); if (value && typeof value.value != "undefined") { value = value.value } return value; }, clear: function(key) { window.sessionStorage.removeItem(this._key(key));; }, _key: function(key) { return ['store', this.element, this.name, key].join('.'); } }); // .Cookie ('cookie') storage uses browser cookies to store data. JavaScript // has access to a single document.cookie variable, which is limited to 2Kb in // size. Cookies are also considered 'unsecure' as the data can be read easily // by other sites/JS. Cookies do have the advantage, though, of being widely // supported and persistent through refresh and close/open. Where available, // HTML5 DOM Storage like LocalStorage and SessionStorage should be used. // // .Cookie can also take additional options: // // * `expires_in` Number of seconds to keep the cookie alive (default 2 weeks). // * `path` The path to activate the current cookie for (default '/'). // // For more information about document.cookie, check out the pre-eminint article // by ppk: http://www.quirksmode.org/js/cookies.html // Sammy.Store.Cookie = function(name, element, options) { this.name = name; this.element = element; this.options = options || {}; this.path = this.options.path || '/'; // set the expires in seconds or default 14 days this.expires_in = this.options.expires_in || (14 * 24 * 60 * 60); }; $.extend(Sammy.Store.Cookie.prototype, { isAvailable: function() { return ('cookie' in document) && (window.location.protocol != 'file:'); }, exists: function(key) { return (this.get(key) != null); }, set: function(key, value) { return this._setCookie(key, value); }, get: function(key) { return this._getCookie(key); }, clear: function(key) { this._setCookie(key, "", -1); }, _key: function(key) { return ['store', this.element, this.name, key].join('.'); }, _getCookie: function(key) { var escaped = this._key(key).replace(/(\.|\*|\(|\)|\[|\])/g, '\\$1'); var match = document.cookie.match("(^|;\\s)" + escaped + "=([^;]*)(;|$)"); return (match ? match[2] : null); }, _setCookie: function(key, value, expires) { if (!expires) { expires = (this.expires_in * 1000) } var date = new Date(); date.setTime(date.getTime() + expires); var set_cookie = [ this._key(key), "=", value, "; expires=", date.toGMTString(), "; path=", this.path ].join(''); document.cookie = set_cookie; } }); // Sammy.Storage is a plugin that provides shortcuts for creating and using // Sammy.Store objects. Once included it provides the `store()` app level // and helper methods. Depends on Sammy.JSON (or json2.js). Sammy.Storage = function(app) { this.use(Sammy.JSON); this.stores = this.stores || {}; // `store()` creates and looks up existing `Sammy.Store` objects // for the current application. The first time used for a given `'name'` // initializes a `Sammy.Store` and also creates a helper under the store's // name. // // ### Example // // var app = $.sammy(function() { // this.use(Sammy.Storage); // // // initializes the store on app creation. // this.store('mystore', {type: 'cookie'}); // // this.get('#/', function() { // // returns the Sammy.Store object // this.store('mystore'); // // sets 'foo' to 'bar' using the shortcut/helper // // equivilent to this.store('mystore').set('foo', 'bar'); // this.mystore('foo', 'bar'); // // returns 'bar' // // equivilent to this.store('mystore').get('foo'); // this.mystore('foo'); // // returns 'baz!' // // equivilent to: // // this.store('mystore').fetch('foo!', function() { // // return 'baz!'; // // }) // this.mystore('foo!', function() { // return 'baz!'; // }); // // this.clearMystore(); // // equivilent to: // // this.store('mystore').clearAll() // }); // // }); // // ### Arguments // // * `name` The name of the store and helper. the name must be unique per application. // * `options` A JS object of options that can be passed to the Store constuctor on initialization. // this.store = function(name, options) { // if the store has not been initialized if (typeof this.stores[name] == 'undefined') { // create initialize the store var clear_method_name = "clear" + name.substr(0,1).toUpperCase() + name.substr(1); this.stores[name] = new Sammy.Store($.extend({ name: name, element: this.element_selector }, options || {})); // app.name() this[name] = function(key, value) { if (typeof value == 'undefined') { return this.stores[name].get(key); } else if ($.isFunction(value)) { return this.stores[name].fetch(key, value); } else { return this.stores[name].set(key, value) } }; // app.clearName(); this[clear_method_name] = function() { return this.stores[name].clearAll(); } // context.name() this.helper(name, function() { return this.app[name].apply(this.app, arguments); }); // context.clearName(); this.helper(clear_method_name, function() { return this.app[clear_method_name](); }); } return this.stores[name]; }; this.helpers({ store: function() { return this.app.store.apply(this.app, arguments); } }); }; // Sammy.Session is an additional plugin for creating a common 'session' store // for the given app. It is a very simple wrapper around `Sammy.Storage` // that provides a simple fallback mechanism for trying to provide the best // possible storage type for the session. This means, `LocalStorage` // if available, otherwise `Cookie`, otherwise `Memory`. // It provides the `session()` helper through `Sammy.Storage#store()`. // // See the `Sammy.Storage` plugin for full documentation. // Sammy.Session = function(app, options) { this.use(Sammy.Storage); // check for local storage, then cookie storage, then just use memory this.store('session', $.extend({type: ['local', 'cookie', 'memory']}, options)); }; // Sammy.Cache provides helpers for caching data within the lifecycle of a // Sammy app. The plugin provides two main methods on `Sammy.Application`, // `cache` and `clearCache`. Each app has its own cache store so that // you dont have to worry about collisions. As of 0.5 the original Sammy.Cache module // has been deprecated in favor of this one based on Sammy.Storage. The exposed // API is almost identical, but Sammy.Storage provides additional backends including // HTML5 Storage. `Sammy.Cache` will try to use these backends when available // (in this order) `LocalStorage`, `SessionStorage`, and `Memory` Sammy.Cache = function(app, options) { this.use(Sammy.Storage); // set cache_partials to true this.cache_partials = true; // check for local storage, then session storage, then just use memory this.store('cache', $.extend({type: ['local', 'session', 'memory']}, options)); }; return Sammy.Storage; }));