initialized kettu on gitorious
[kettu:kettu.git] / vendor / sammy / sammy.storage.js
1 (function($) {
2
3   Sammy = Sammy || {};
4
5   // Sammy.Store is an abstract adapter class that wraps the multitude of in
6   // browser data storage into a single common set of methods for storing and
7   // retreiving data. The JSON library is used (through the inclusion of the 
8   // Sammy.JSON) plugin, to automatically convert objects back and forth from 
9   // stored strings.
10   // 
11   // Sammy.Store can be used directly, but within a Sammy.Application it is much
12   // easier to use the <tt>Sammy.Storage</tt> plugin and its helper methods. 
13   //
14   // Sammy.Store also supports the KVO pattern, by firing DOM/jQuery Events when
15   // a key is set.
16   //
17   // === Example
18   //
19   //      // create a new store named 'mystore', tied to the #main element, using HTML5 localStorage
20   //      // Note: localStorage only works on browsers that support it
21   //      var store = new Sammy.Store({name: 'mystore', element: '#element', type: 'local'});
22   //      store.set('foo', 'bar');
23   //      store.get('foo'); //=> 'bar'
24   //      store.set('json', {obj: 'this is an obj'});
25   //      store.get('json'); //=> {obj: 'this is an obj'}
26   //      store.keys(); //=> ['foo','json']
27   //      store.clear('foo');
28   //      store.keys(); //=> ['json']
29   //      store.clearAll();
30   //      store.keys(); //=> []
31   // 
32   // === Arguments
33   //
34   // The constructor takes a single argument which is a Object containing these possible options.
35   // 
36   // +name+::     The name/namespace of this store. Stores are unique by name/type. (default 'store')
37   // +element+::  A selector for the element that the store is bound to. (default 'body')
38   // +type+::     The type of storage/proxy to use (default 'memory')
39   //  
40   // Extra options are passed to the storage constructor.
41   // Sammy.Store supports the following methods of storage:
42   // 
43   // +memory+::   Basic object storage
44   // +data+::     jQuery.data DOM Storage
45   // +cookie+::   Access to document.cookie. Limited to 2K
46   // +local+::    HTML5 DOM localStorage, browswer support is currently limited.
47   // +session+::  HTML5 DOM sessionStorage, browswer support is currently limited.
48   //
49   Sammy.Store = function(options) {
50     this.options  = options || {};
51     this.name     = this.options.name || 'store';
52     this.element  = this.options.element || 'body';
53     this.$element = $(this.element);
54     this.type     = this.options.type || 'memory';
55     this.meta_key = this.options.meta_key || '__keys__';
56     this.storage  = new Sammy.Store[Sammy.Store.stores[this.type]](this.name, this.element, this.options);
57   };
58
59   Sammy.Store.stores = {
60     'memory': 'Memory',
61     'data': 'Data',
62     'local': 'LocalStorage',
63     'session': 'SessionStorage',
64     'cookie': 'Cookie'
65   };
66
67   $.extend(Sammy.Store.prototype, {
68     // Checks for the availability of the current storage type in the current browser/config.
69     isAvailable: function() {
70       if ($.isFunction(this.storage.isAvailable)) {
71         return this.storage.isAvailable();
72       } else {
73         true;
74       }
75     },
76     // Checks for the existance of <tt>key</tt> in the current store. Returns a boolean.
77     exists: function(key) {
78       return this.storage.exists(key);
79     },
80     // Sets the value of <tt>key<tt> with <tt>value</tt>. If <tt>value<tt> is an
81     // object, it is turned to and stored as a string with <tt>JSON.stringify</tt>.
82     // It also tries to conform to the KVO pattern triggering jQuery events on the 
83     // element that the store is bound to.
84     // 
85     // === Example
86     //
87     //      var store = new Sammy.Store({name: 'kvo'});
88     //      $('body').bind('set-kvo.foo', function(e, data) { 
89     //        Sammy.log(data.key + ' changed to ' + data.value);
90     //      });
91     //      store.set('foo', 'bar'); // logged: foo changed to bar
92     //
93     set: function(key, value) {
94       var string_value = (typeof value == 'string') ? value : JSON.stringify(value);
95       key = key.toString();
96       this.storage.set(key, string_value);
97       if (key != this.meta_key) { 
98         this._addKey(key); 
99         this.$element.trigger('set-' + this.name + '.' + key, {key: key, value: value});
100       };
101       // always return the original value
102       return value;
103     },
104     // Returns the set value at <tt>key</tt>, parsing with <tt>JSON.parse</tt> and 
105     // turning into an object if possible
106     get: function(key) {
107       var value = this.storage.get(key);
108       if (typeof value == 'undefined' || value == null || value == '') {
109         return value;
110       }
111       try {
112         return JSON.parse(value);
113       } catch(e) {
114         return value;
115       }
116     },
117     // Removes the value at <tt>key</tt> from the current store
118     clear: function(key) {
119       this._removeKey(key);
120       return this.storage.clear(key);
121     },
122     // Clears all the values for the current store.
123     clearAll: function() {
124       var self = this;
125       $.each(this.keys(), function(i, key) {
126         self.clear(key);
127       });
128     },
129     // Returns the all the keys set for the current store as an array.
130     // Internally Sammy.Store keeps this array in a 'meta_key' for easy access.
131     keys: function() {
132       return this.get(this.meta_key) || [];
133     },
134     // Returns the value at <tt>key</tt> if set, otherwise, runs the callback
135     // and sets the value to the value returned in the callback.
136     // 
137     // === Example
138     // 
139     //    var store = new Sammy.Store;
140     //    store.exists('foo'); //=> false
141     //    store.fetch('foo', function() {
142     //      return 'bar!';
143     //    }); //=> 'bar!'
144     //    store.get('foo') //=> 'bar!'
145     //    store.fetch('foo', function() {
146     //      return 'baz!';
147     //    }); //=> 'bar!
148     // 
149     fetch: function(key, callback) {
150       if (!this.exists(key)) {
151         return this.set(key, callback.apply(this));
152       } else {
153         return this.get(key);
154       }
155     },
156     // loads the response of a request to <tt>path</tt> into <tt>key</tt>.
157     // 
158     // === Example
159     // 
160     // In /mytemplate.tpl:
161     // 
162     //    My Template
163     //     
164     // In app.js:
165     // 
166     //    var store = new Sammy.Store;
167     //    store.load('mytemplate', '/mytemplate.tpl', function() {
168     //      s.get('mytemplate') //=> My Template
169     //    });
170     // 
171     load: function(key, path, callback) {
172       var s = this;
173       $.get(path, function(response) {
174         s.set(key, response);
175         if (callback) { callback.apply(this, [response]); }
176       });
177     },
178     _addKey: function(key) {
179       var keys = this.keys();
180       if ($.inArray(key, keys) == -1) { keys.push(key); }
181       this.set(this.meta_key, keys);
182     },
183     _removeKey: function(key) {
184       var keys = this.keys();
185       var index = $.inArray(key, keys);
186       if (index != -1) { keys.splice(index, 1); }
187       this.set(this.meta_key, keys);
188     }
189   });
190   
191   // Tests if the type of storage is available/works in the current browser/config.
192   // Especially useful for testing the availability of the awesome, but not widely
193   // supported HTML5 DOM storage
194   Sammy.Store.isAvailable = function(type) {
195     try {
196       return Sammy.Store[Sammy.Store.stores[type]].prototype.isAvailable();
197     } catch(e) {
198       return false;
199     }
200   };
201
202   // Memory ('memory') is the basic/default store. It stores data in a global 
203   // JS object. Data is lost on refresh.
204   Sammy.Store.Memory = function(name, element) {
205     this.name  = name;
206     this.element = element;
207     this.namespace = [this.element, this.name].join('.');
208     Sammy.Store.Memory.store = Sammy.Store.Memory.store || {};
209     Sammy.Store.Memory.store[this.namespace] = Sammy.Store.Memory.store[this.namespace] || {};
210     this.store = Sammy.Store.Memory.store[this.namespace];
211   };
212   $.extend(Sammy.Store.Memory.prototype, {
213     isAvailable: function() { return true; },
214     exists: function(key) {
215       return (typeof this.store[key] != "undefined");
216     },
217     set: function(key, value) {
218       return this.store[key] = value;
219     },
220     get: function(key) {
221       return this.store[key];
222     },
223     clear: function(key) {
224       delete this.store[key];
225     }
226   });
227
228   // Data ('data') stores objects using the jQuery.data() methods. This has the advantadge
229   // of scoping the data to the specific element. Like the 'memory' store its data
230   // will only last for the length of the current request (data is lost on refresh/etc).
231   Sammy.Store.Data = function(name, element) {
232     this.name = name;
233     this.element = element;
234     this.$element = $(element);
235   };
236   $.extend(Sammy.Store.Data.prototype, {
237     isAvailable: function() { return true; },
238     exists: function(key) {
239       return (typeof this.$element.data(this._key(key)) != "undefined");
240     },
241     set: function(key, value) {
242       return this.$element.data(this._key(key), value);
243     },
244     get: function(key) {
245       return this.$element.data(this._key(key));
246     },
247     clear: function(key) {
248       this.$element.removeData(this._key(key));
249     },
250     _key: function(key) {
251       return ['store', this.name, key].join('.');
252     }
253   });
254
255   // LocalStorage ('local') makes use of HTML5 DOM Storage, and the window.localStorage
256   // object. The great advantage of this method is that data will persist beyond 
257   // the current request. It can be considered a pretty awesome replacement for 
258   // cookies accessed via JS. The great disadvantage, though, is its only available
259   // on the latest and greatest browsers. 
260   //
261   // For more info on DOM Storage: 
262   // [https://developer.mozilla.org/en/DOM/Storage]
263   // [http://www.w3.org/TR/2009/WD-webstorage-20091222/]
264   //
265   Sammy.Store.LocalStorage = function(name, element) {
266     this.name = name;
267     this.element = element;
268   };
269   $.extend(Sammy.Store.LocalStorage.prototype, {
270     isAvailable: function() {
271       return ('localStorage' in window) && (window.location.protocol != 'file:');
272     },
273     exists: function(key) {
274       return (this.get(key) != null);
275     },
276     set: function(key, value) {
277       return window.localStorage.setItem(this._key(key), value);
278     },
279     get: function(key) {
280       return window.localStorage.getItem(this._key(key));
281     },
282     clear: function(key) {
283       window.localStorage.removeItem(this._key(key));;
284     },
285     _key: function(key) {
286       return ['store', this.element, this.name, key].join('.');
287     }
288   }); 
289
290   // .SessionStorage ('session') is similar to LocalStorage (part of the same API)
291   // and shares similar browser support/availability. The difference is that 
292   // SessionStorage is only persistant through the current 'session' which is defined
293   // as the length that the current window is open. This means that data will survive
294   // refreshes but not close/open or multiple windows/tabs. For more info, check out
295   // the <tt>LocalStorage</tt> documentation and links.
296   Sammy.Store.SessionStorage = function(name, element) {
297     this.name = name;
298     this.element = element;
299   };
300   $.extend(Sammy.Store.SessionStorage.prototype, {
301     isAvailable: function() {
302       return ('sessionStorage' in window) && 
303       (window.location.protocol != 'file:') && 
304       ($.isFunction(window.sessionStorage.setItem));
305     },
306     exists: function(key) {
307       return (this.get(key) != null);
308     },
309     set: function(key, value) {
310       return window.sessionStorage.setItem(this._key(key), value);
311     },
312     get: function(key) {
313       var value = window.sessionStorage.getItem(this._key(key));
314       if (value && typeof value.value != "undefined") { value = value.value }
315       return value;
316     },
317     clear: function(key) {
318       window.sessionStorage.removeItem(this._key(key));;
319     },
320     _key: function(key) {
321       return ['store', this.element, this.name, key].join('.');
322     }
323   });
324
325   // .Cookie ('cookie') storage uses browser cookies to store data. JavaScript
326   // has access to a single document.cookie variable, which is limited to 2Kb in
327   // size. Cookies are also considered 'unsecure' as the data can be read easily
328   // by other sites/JS. Cookies do have the advantage, though, of being widely
329   // supported and persistent through refresh and close/open. Where available,
330   // HTML5 DOM Storage like LocalStorage and SessionStorage should be used.
331   //
332   // .Cookie can also take additional options:
333   // +expires_in+:: Number of seconds to keep the cookie alive (default 2 weeks).
334   // +path+::       The path to activate the current cookie for (default '/').
335   //
336   // For more information about document.cookie, check out the pre-eminint article 
337   // by ppk: [http://www.quirksmode.org/js/cookies.html]
338   //
339   Sammy.Store.Cookie = function(name, element, options) {
340     this.name = name;
341     this.element = element;
342     this.options = options || {};
343     this.path = this.options.path || '/';
344     // set the expires in seconds or default 14 days
345     this.expires_in = this.options.expires_in || (14 * 24 * 60 * 60); 
346   };
347   $.extend(Sammy.Store.Cookie.prototype, {
348     isAvailable: function() {
349       return ('cookie' in document) && (window.location.protocol != 'file:');
350     },
351     exists: function(key) {
352       return (this.get(key) != null);
353     },
354     set: function(key, value) {
355       return this._setCookie(key, value);
356     },
357     get: function(key) {
358       return this._getCookie(key);
359     },
360     clear: function(key) {
361       this._setCookie(key, "", -1);
362     },
363     _key: function(key) {
364       return ['store', this.element, this.name, key].join('.');
365     },
366     _getCookie: function(key) {
367       var escaped = this._key(key).replace(/(\.|\*|\(|\)|\[|\])/g, '\\$1');
368       var match = document.cookie.match("(^|;\\s)" + escaped + "=([^;]*)(;|$)")
369       return (match ? match[2] : null);
370     },
371     _setCookie: function(key, value, expires) {
372       if (!expires) { expires = (this.expires_in * 1000) }
373       var date = new Date();
374       date.setTime(date.getTime() + expires);
375       var set_cookie = [
376         this._key(key), "=", value, 
377         "; expires=", date.toGMTString(), 
378         "; path=", this.path
379       ].join('');
380       document.cookie = set_cookie;
381     }
382   });  
383
384   // Sammy.Storage is a plugin that provides shortcuts for creating and using
385   // Sammy.Store objects. Once included it provides the <tt>store()</tt> app level
386   // and helper methods. Depends on Sammy.JSON (or json2.js).
387   Sammy.Storage = function(app) {
388     this.use(Sammy.JSON);
389
390     this.stores = this.stores || {};
391
392     // <tt>store()</tt> creates and looks up existing <tt>Sammy.Store</tt> objects
393     // for the current application. The first time used for a given <tt>'name'</tt>
394     // initializes a <tt>Sammy.Store</tt> and also creates a helper under the store's
395     // name. 
396     //
397     // === Example
398     //
399     //      var app = $.sammy(function() {
400     //        this.use(Sammy.Storage);
401     //        
402     //        // initializes the store on app creation.
403     //        this.store('mystore', {type: 'cookie'});
404     //        
405     //        this.get('#/', function() {
406     //          // returns the Sammy.Store object
407     //          this.store('mystore'); 
408     //          // sets 'foo' to 'bar' using the shortcut/helper
409     //          // equivilent to this.store('mystore').set('foo', 'bar');
410     //          this.mystore('foo', 'bar'); 
411     //          // returns 'bar'
412     //          // equivilent to this.store('mystore').get('foo');
413     //          this.mystore('foo');
414     //          // returns 'baz!' 
415     //          // equivilent to:
416     //          // this.store('mystore').fetch('foo!', function() {
417     //          //   return 'baz!';
418     //          // })
419     //          this.mystore('foo!', function() {
420     //            return 'baz!';
421     //          });
422     //
423     //          this.clearMystore(); 
424     //          // equivilent to:
425     //          // this.store('mystore').clearAll()
426     //        });
427     //        
428     //      });
429     //
430     // === Arguments
431     //
432     // +name+::     The name of the store and helper. the name must be unique per application.
433     // +options+::  A JS object of options that can be passed to the Store constuctor on initialization.
434     //
435     this.store = function(name, options) {
436       // if the store has not been initialized
437       if (typeof this.stores[name] == 'undefined') { 
438         // create initialize the store
439         var clear_method_name = "clear" + name.substr(0,1).toUpperCase() + name.substr(1);
440         this.stores[name] = new Sammy.Store($.extend({
441           name: name, 
442           element: this.element_selector
443         }, options || {}));
444         // app.name()
445         this[name] = function(key, value) {
446           if (typeof value == 'undefined') {
447             return this.stores[name].get(key);
448           } else if ($.isFunction(value)) {
449             return this.stores[name].fetch(key, value);
450           } else {
451             return this.stores[name].set(key, value)
452           }
453         };
454         // app.clearName();
455         this[clear_method_name] = function() {
456           return this.stores[name].clearAll();
457         }
458         // context.name()
459         this.helper(name, function() {
460           return this.app[name].apply(this.app, arguments);
461         });
462         // context.clearName();
463         this.helper(clear_method_name, function() {
464           return this.app[clear_method_name]();
465         });
466       }
467       return this.stores[name];
468     };
469     
470     this.helpers({
471       store: function() {
472         return this.app.store.apply(this.app, arguments);
473       }
474     });
475   };
476   
477   // Sammy.Session is an additional plugin for creating a common 'session' store
478   // for the given app. It is a very simple wrapper around <tt>Sammy.Storage</tt>
479   // that provides a simple fallback mechanism for trying to provide the best
480   // possible storage type for the session. This means, <tt>LocalStorage</tt> 
481   // if available, otherwise <tt>Cookie</tt>, otherwise <tt>Memory</tt>.
482   // It provides the <tt>session()</tt> helper through <tt>Sammy.Storage#store()</tt>.
483   //
484   // See the <tt>Sammy.Storage</tt> plugin for full documentation.
485   //
486   Sammy.Session = function(app, options) {
487     this.use(Sammy.Storage);
488     // check for local storage, then cookie storage, then just use memory
489     var type = 'memory';
490     if (Sammy.Store.isAvailable('local')) {
491       type = 'local';
492     } else if (Sammy.Store.isAvailable('cookie')) {
493       type = 'cookie';
494     }
495     this.store('session', $.extend({type: type}, options));
496   };
497   
498   // Sammy.Cache provides helpers for caching data within the lifecycle of a 
499   // Sammy app. The plugin provides two main methods on <tt>Sammy.Application<tt>,
500   // <tt>cache</tt> and <tt>clearCache</tt>. Each app has its own cache store so that
501   // you dont have to worry about collisions. As of 0.5 the original Sammy.Cache module
502   // has been deprecated in favor of this one based on Sammy.Storage. The exposed
503   // API is almost identical, but Sammy.Storage provides additional backends including
504   // HTML5 Storage. <tt>Sammy.Cache</tt> will try to use these backends when available
505   // (in this order) <tt>LocalStorage</tt>, <tt>SessionStorage</tt>, and <tt>Memory</tt>
506   Sammy.Cache = function(app, options) {
507     this.use(Sammy.Storage);
508     // set cache_partials to true
509     this.cache_partials = true;
510     // check for local storage, then session storage, then just use memory
511     var type = 'memory';
512     if (Sammy.Store.isAvailable('local')) {
513       type = 'local';
514     } else if (Sammy.Store.isAvailable('session')) {
515       type = 'session';
516     }
517     this.store('cache', $.extend({type: type}, options));
518   };
519   
520 })(jQuery);