Merge "Add one more missing directory to findHooks.php"
[lhc/web/wiklou.git] / resources / jquery / jquery.jStorage.js
1 /*
2 * ----------------------------- JSTORAGE -------------------------------------
3 * Simple local storage wrapper to save data on the browser side, supporting
4 * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
5 *
6 * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com
7 * Project homepage: www.jstorage.info
8 *
9 * Licensed under MIT-style license:
10 *
11 * Permission is hereby granted, free of charge, to any person obtaining a copy
12 * of this software and associated documentation files (the "Software"), to deal
13 * in the Software without restriction, including without limitation the rights
14 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 * copies of the Software, and to permit persons to whom the Software is
16 * furnished to do so, subject to the following conditions:
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 * SOFTWARE.
25 */
26
27 (function(){
28 var
29 /* jStorage version */
30 JSTORAGE_VERSION = "0.3.0",
31
32 /* detect a dollar object or create one if not found */
33 $ = window.jQuery || window.$ || (window.$ = {}),
34
35 /* check for a JSON handling support */
36 JSON = {
37 parse:
38 window.JSON && (window.JSON.parse || window.JSON.decode) ||
39 String.prototype.evalJSON && function(str){return String(str).evalJSON();} ||
40 $.parseJSON ||
41 $.evalJSON,
42 stringify:
43 Object.toJSON ||
44 window.JSON && (window.JSON.stringify || window.JSON.encode) ||
45 $.toJSON
46 };
47
48 // Break if no JSON support was found
49 if(!JSON.parse || !JSON.stringify){
50 throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
51 }
52
53 var
54 /* This is the object, that holds the cached values */
55 _storage = {},
56
57 /* Actual browser storage (localStorage or globalStorage['domain']) */
58 _storage_service = {jStorage:"{}"},
59
60 /* DOM element for older IE versions, holds userData behavior */
61 _storage_elm = null,
62
63 /* How much space does the storage take */
64 _storage_size = 0,
65
66 /* which backend is currently used */
67 _backend = false,
68
69 /* onchange observers */
70 _observers = {},
71
72 /* timeout to wait after onchange event */
73 _observer_timeout = false,
74
75 /* last update time */
76 _observer_update = 0,
77
78 /* pubsub observers */
79 _pubsub_observers = {},
80
81 /* skip published items older than current timestamp */
82 _pubsub_last = +new Date(),
83
84 /* Next check for TTL */
85 _ttl_timeout,
86
87 /* crc32 table */
88 _crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+
89 "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+
90 "6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+
91 "FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+
92 "A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+
93 "32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+
94 "56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+
95 "C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+
96 "E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+
97 "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+
98 "12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+
99 "A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+
100 "DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+
101 "5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+
102 "2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+
103 "04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+
104 "7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+
105 "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+
106 "A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+
107 "36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+
108 "5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+
109 "C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+
110 "EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+
111 "7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+
112 "18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+
113 "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+
114 "D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+
115 "53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+
116 "2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D",
117
118 /**
119 * XML encoding and decoding as XML nodes can't be JSON'ized
120 * XML nodes are encoded and decoded if the node is the value to be saved
121 * but not if it's as a property of another object
122 * Eg. -
123 * $.jStorage.set("key", xmlNode); // IS OK
124 * $.jStorage.set("key", {xml: xmlNode}); // NOT OK
125 */
126 _XMLService = {
127
128 /**
129 * Validates a XML node to be XML
130 * based on jQuery.isXML function
131 */
132 isXML: function(elm){
133 var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
134 return documentElement ? documentElement.nodeName !== "HTML" : false;
135 },
136
137 /**
138 * Encodes a XML node to string
139 * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
140 */
141 encode: function(xmlNode) {
142 if(!this.isXML(xmlNode)){
143 return false;
144 }
145 try{ // Mozilla, Webkit, Opera
146 return new XMLSerializer().serializeToString(xmlNode);
147 }catch(E1) {
148 try { // IE
149 return xmlNode.xml;
150 }catch(E2){}
151 }
152 return false;
153 },
154
155 /**
156 * Decodes a XML node from string
157 * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
158 */
159 decode: function(xmlString){
160 var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) ||
161 (window.ActiveXObject && function(_xmlString) {
162 var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
163 xml_doc.async = 'false';
164 xml_doc.loadXML(_xmlString);
165 return xml_doc;
166 }),
167 resultXML;
168 if(!dom_parser){
169 return false;
170 }
171 resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
172 return this.isXML(resultXML)?resultXML:false;
173 }
174 },
175
176 _localStoragePolyfillSetKey = function(){};
177
178
179 ////////////////////////// PRIVATE METHODS ////////////////////////
180
181 /**
182 * Initialization function. Detects if the browser supports DOM Storage
183 * or userData behavior and behaves accordingly.
184 */
185 function _init(){
186 /* Check if browser supports localStorage */
187 var localStorageReallyWorks = false;
188 if("localStorage" in window){
189 try {
190 window.localStorage.setItem('_tmptest', 'tmpval');
191 localStorageReallyWorks = true;
192 window.localStorage.removeItem('_tmptest');
193 } catch(BogusQuotaExceededErrorOnIos5) {
194 // Thanks be to iOS5 Private Browsing mode which throws
195 // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
196 }
197 }
198
199 if(localStorageReallyWorks){
200 try {
201 if(window.localStorage) {
202 _storage_service = window.localStorage;
203 _backend = "localStorage";
204 _observer_update = _storage_service.jStorage_update;
205 }
206 } catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */}
207 }
208 /* Check if browser supports globalStorage */
209 else if("globalStorage" in window){
210 try {
211 if(window.globalStorage) {
212 _storage_service = window.globalStorage[window.location.hostname];
213 _backend = "globalStorage";
214 _observer_update = _storage_service.jStorage_update;
215 }
216 } catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */}
217 }
218 /* Check if browser supports userData behavior */
219 else {
220 _storage_elm = document.createElement('link');
221 if(_storage_elm.addBehavior){
222
223 /* Use a DOM element to act as userData storage */
224 _storage_elm.style.behavior = 'url(#default#userData)';
225
226 /* userData element needs to be inserted into the DOM! */
227 document.getElementsByTagName('head')[0].appendChild(_storage_elm);
228
229 try{
230 _storage_elm.load("jStorage");
231 }catch(E){
232 // try to reset cache
233 _storage_elm.setAttribute("jStorage", "{}");
234 _storage_elm.save("jStorage");
235 _storage_elm.load("jStorage");
236 }
237
238 var data = "{}";
239 try{
240 data = _storage_elm.getAttribute("jStorage");
241 }catch(E5){}
242
243 try{
244 _observer_update = _storage_elm.getAttribute("jStorage_update");
245 }catch(E6){}
246
247 _storage_service.jStorage = data;
248 _backend = "userDataBehavior";
249 }else{
250 _storage_elm = null;
251 return;
252 }
253 }
254
255 // Load data from storage
256 _load_storage();
257
258 // remove dead keys
259 _handleTTL();
260
261 // create localStorage and sessionStorage polyfills if needed
262 _createPolyfillStorage("local");
263 _createPolyfillStorage("session");
264
265 // start listening for changes
266 _setupObserver();
267
268 // initialize publish-subscribe service
269 _handlePubSub();
270
271 // handle cached navigation
272 if("addEventListener" in window){
273 window.addEventListener("pageshow", function(event){
274 if(event.persisted){
275 _storageObserver();
276 }
277 }, false);
278 }
279 }
280
281 /**
282 * Create a polyfill for localStorage (type="local") or sessionStorage (type="session")
283 *
284 * @param {String} type Either "local" or "session"
285 * @param {Boolean} forceCreate If set to true, recreate the polyfill (needed with flush)
286 */
287 function _createPolyfillStorage(type, forceCreate){
288 var _skipSave = false,
289 _length = 0,
290 i,
291 storage,
292 storage_source = {};
293
294 var rand = Math.random();
295
296 if(!forceCreate && typeof window[type+"Storage"] != "undefined"){
297 return;
298 }
299
300 // Use globalStorage for localStorage if available
301 if(type == "local" && window.globalStorage){
302 localStorage = window.globalStorage[window.location.hostname];
303 return;
304 }
305
306 // only IE6/7 from this point on
307 if(_backend != "userDataBehavior"){
308 return;
309 }
310
311 // Remove existing storage element if available
312 if(forceCreate && window[type+"Storage"] && window[type+"Storage"].parentNode){
313 window[type+"Storage"].parentNode.removeChild(window[type+"Storage"]);
314 }
315
316 storage = document.createElement("button");
317 document.getElementsByTagName('head')[0].appendChild(storage);
318
319 if(type == "local"){
320 storage_source = _storage;
321 }else if(type == "session"){
322 _sessionStoragePolyfillUpdate();
323 }
324
325 for(i in storage_source){
326
327 if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i != "length" && typeof storage_source[i] != "undefined"){
328 if(!(i in storage)){
329 _length++;
330 }
331 storage[i] = storage_source[i];
332 }
333 }
334
335 // Polyfill API
336
337 /**
338 * Indicates how many keys are stored in the storage
339 */
340 storage.length = _length;
341
342 /**
343 * Returns the key of the nth stored value
344 *
345 * @param {Number} n Index position
346 * @return {String} Key name of the nth stored value
347 */
348 storage.key = function(n){
349 var count = 0, i;
350 _sessionStoragePolyfillUpdate();
351 for(i in storage_source){
352 if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i!="length" && typeof storage_source[i] != "undefined"){
353 if(count == n){
354 return i;
355 }
356 count++;
357 }
358 }
359 }
360
361 /**
362 * Returns the current value associated with the given key
363 *
364 * @param {String} key key name
365 * @return {Mixed} Stored value
366 */
367 storage.getItem = function(key){
368 _sessionStoragePolyfillUpdate();
369 if(type == "session"){
370 return storage_source[key];
371 }
372 return $.jStorage.get(key);
373 }
374
375 /**
376 * Sets or updates value for a give key
377 *
378 * @param {String} key Key name to be updated
379 * @param {String} value String value to be stored
380 */
381 storage.setItem = function(key, value){
382 if(typeof value == "undefined"){
383 return;
384 }
385 storage[key] = (value || "").toString();
386 }
387
388 /**
389 * Removes key from the storage
390 *
391 * @param {String} key Key name to be removed
392 */
393 storage.removeItem = function(key){
394 if(type == "local"){
395 return $.jStorage.deleteKey(key);
396 }
397
398 storage[key] = undefined;
399
400 _skipSave = true;
401 if(key in storage){
402 storage.removeAttribute(key);
403 }
404 _skipSave = false;
405 }
406
407 /**
408 * Clear storage
409 */
410 storage.clear = function(){
411 if(type == "session"){
412 window.name = "";
413 _createPolyfillStorage("session", true);
414 return;
415 }
416 $.jStorage.flush();
417 }
418
419 if(type == "local"){
420
421 _localStoragePolyfillSetKey = function(key, value){
422 if(key == "length"){
423 return;
424 }
425 _skipSave = true;
426 if(typeof value == "undefined"){
427 if(key in storage){
428 _length--;
429 storage.removeAttribute(key);
430 }
431 }else{
432 if(!(key in storage)){
433 _length++;
434 }
435 storage[key] = (value || "").toString();
436 }
437 storage.length = _length;
438 _skipSave = false;
439 }
440 }
441
442 function _sessionStoragePolyfillUpdate(){
443 if(type != "session"){
444 return;
445 }
446 try{
447 storage_source = JSON.parse(window.name || "{}");
448 }catch(E){
449 storage_source = {};
450 }
451 }
452
453 function _sessionStoragePolyfillSave(){
454 if(type != "session"){
455 return;
456 }
457 window.name = JSON.stringify(storage_source);
458 };
459
460 storage.attachEvent("onpropertychange", function(e){
461 if(e.propertyName == "length"){
462 return;
463 }
464
465 if(_skipSave || e.propertyName == "length"){
466 return;
467 }
468
469 if(type == "local"){
470 if(!(e.propertyName in storage_source) && typeof storage[e.propertyName] != "undefined"){
471 _length ++;
472 }
473 }else if(type == "session"){
474 _sessionStoragePolyfillUpdate();
475 if(typeof storage[e.propertyName] != "undefined" && !(e.propertyName in storage_source)){
476 storage_source[e.propertyName] = storage[e.propertyName];
477 _length++;
478 }else if(typeof storage[e.propertyName] == "undefined" && e.propertyName in storage_source){
479 delete storage_source[e.propertyName];
480 _length--;
481 }else{
482 storage_source[e.propertyName] = storage[e.propertyName];
483 }
484
485 _sessionStoragePolyfillSave();
486 storage.length = _length;
487 return;
488 }
489
490 $.jStorage.set(e.propertyName, storage[e.propertyName]);
491 storage.length = _length;
492 });
493
494 window[type+"Storage"] = storage;
495 }
496
497 /**
498 * Reload data from storage when needed
499 */
500 function _reloadData(){
501 var data = "{}";
502
503 if(_backend == "userDataBehavior"){
504 _storage_elm.load("jStorage");
505
506 try{
507 data = _storage_elm.getAttribute("jStorage");
508 }catch(E5){}
509
510 try{
511 _observer_update = _storage_elm.getAttribute("jStorage_update");
512 }catch(E6){}
513
514 _storage_service.jStorage = data;
515 }
516
517 _load_storage();
518
519 // remove dead keys
520 _handleTTL();
521
522 _handlePubSub();
523 }
524
525 /**
526 * Sets up a storage change observer
527 */
528 function _setupObserver(){
529 if(_backend == "localStorage" || _backend == "globalStorage"){
530 if("addEventListener" in window){
531 window.addEventListener("storage", _storageObserver, false);
532 }else{
533 document.attachEvent("onstorage", _storageObserver);
534 }
535 }else if(_backend == "userDataBehavior"){
536 setInterval(_storageObserver, 1000);
537 }
538 }
539
540 /**
541 * Fired on any kind of data change, needs to check if anything has
542 * really been changed
543 */
544 function _storageObserver(){
545 var updateTime;
546 // cumulate change notifications with timeout
547 clearTimeout(_observer_timeout);
548 _observer_timeout = setTimeout(function(){
549
550 if(_backend == "localStorage" || _backend == "globalStorage"){
551 updateTime = _storage_service.jStorage_update;
552 }else if(_backend == "userDataBehavior"){
553 _storage_elm.load("jStorage");
554 try{
555 updateTime = _storage_elm.getAttribute("jStorage_update");
556 }catch(E5){}
557 }
558
559 if(updateTime && updateTime != _observer_update){
560 _observer_update = updateTime;
561 _checkUpdatedKeys();
562 }
563
564 }, 25);
565 }
566
567 /**
568 * Reloads the data and checks if any keys are changed
569 */
570 function _checkUpdatedKeys(){
571 var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
572 newCrc32List;
573
574 _reloadData();
575 newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
576
577 var key,
578 updated = [],
579 removed = [];
580
581 for(key in oldCrc32List){
582 if(oldCrc32List.hasOwnProperty(key)){
583 if(!newCrc32List[key]){
584 removed.push(key);
585 continue;
586 }
587 if(oldCrc32List[key] != newCrc32List[key]){
588 updated.push(key);
589 }
590 }
591 }
592
593 for(key in newCrc32List){
594 if(newCrc32List.hasOwnProperty(key)){
595 if(!oldCrc32List[key]){
596 updated.push(key);
597 }
598 }
599 }
600
601 _fireObservers(updated, "updated");
602 _fireObservers(removed, "deleted");
603 }
604
605 /**
606 * Fires observers for updated keys
607 *
608 * @param {Array|String} keys Array of key names or a key
609 * @param {String} action What happened with the value (updated, deleted, flushed)
610 */
611 function _fireObservers(keys, action){
612 keys = [].concat(keys || []);
613 if(action == "flushed"){
614 keys = [];
615 for(var key in _observers){
616 if(_observers.hasOwnProperty(key)){
617 keys.push(key);
618 }
619 }
620 action = "deleted";
621 }
622 for(var i=0, len = keys.length; i<len; i++){
623 if(_observers[keys[i]]){
624 for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
625 _observers[keys[i]][j](keys[i], action);
626 }
627 }
628 }
629 }
630
631 /**
632 * Publishes key change to listeners
633 */
634 function _publishChange(){
635 var updateTime = (+new Date()).toString();
636
637 if(_backend == "localStorage" || _backend == "globalStorage"){
638 _storage_service.jStorage_update = updateTime;
639 }else if(_backend == "userDataBehavior"){
640 _storage_elm.setAttribute("jStorage_update", updateTime);
641 _storage_elm.save("jStorage");
642 }
643
644 _storageObserver();
645 }
646
647 /**
648 * Loads the data from the storage based on the supported mechanism
649 */
650 function _load_storage(){
651 /* if jStorage string is retrieved, then decode it */
652 if(_storage_service.jStorage){
653 try{
654 _storage = JSON.parse(String(_storage_service.jStorage));
655 }catch(E6){_storage_service.jStorage = "{}";}
656 }else{
657 _storage_service.jStorage = "{}";
658 }
659 _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
660
661 if(!_storage.__jstorage_meta){
662 _storage.__jstorage_meta = {};
663 }
664 if(!_storage.__jstorage_meta.CRC32){
665 _storage.__jstorage_meta.CRC32 = {};
666 }
667 }
668
669 /**
670 * This functions provides the "save" mechanism to store the jStorage object
671 */
672 function _save(){
673 _dropOldEvents(); // remove expired events
674 try{
675 _storage_service.jStorage = JSON.stringify(_storage);
676 // If userData is used as the storage engine, additional
677 if(_storage_elm) {
678 _storage_elm.setAttribute("jStorage",_storage_service.jStorage);
679 _storage_elm.save("jStorage");
680 }
681 _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
682 }catch(E7){/* probably cache is full, nothing is saved this way*/}
683 }
684
685 /**
686 * Function checks if a key is set and is string or numberic
687 *
688 * @param {String} key Key name
689 */
690 function _checkKey(key){
691 if(!key || (typeof key != "string" && typeof key != "number")){
692 throw new TypeError('Key name must be string or numeric');
693 }
694 if(key == "__jstorage_meta"){
695 throw new TypeError('Reserved key name');
696 }
697 return true;
698 }
699
700 /**
701 * Removes expired keys
702 */
703 function _handleTTL(){
704 var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
705
706 clearTimeout(_ttl_timeout);
707
708 if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
709 // nothing to do here
710 return;
711 }
712
713 curtime = +new Date();
714 TTL = _storage.__jstorage_meta.TTL;
715
716 CRC32 = _storage.__jstorage_meta.CRC32;
717 for(i in TTL){
718 if(TTL.hasOwnProperty(i)){
719 if(TTL[i] <= curtime){
720 delete TTL[i];
721 delete CRC32[i];
722 delete _storage[i];
723 changed = true;
724 deleted.push(i);
725 }else if(TTL[i] < nextExpire){
726 nextExpire = TTL[i];
727 }
728 }
729 }
730
731 // set next check
732 if(nextExpire != Infinity){
733 _ttl_timeout = setTimeout(_handleTTL, nextExpire - curtime);
734 }
735
736 // save changes
737 if(changed){
738 _save();
739 _publishChange();
740 _fireObservers(deleted, "deleted");
741 }
742 }
743
744 /**
745 * Checks if there's any events on hold to be fired to listeners
746 */
747 function _handlePubSub(){
748 if(!_storage.__jstorage_meta.PubSub){
749 return;
750 }
751 var pubelm,
752 _pubsubCurrent = _pubsub_last;
753
754 for(var i=len=_storage.__jstorage_meta.PubSub.length-1; i>=0; i--){
755 pubelm = _storage.__jstorage_meta.PubSub[i];
756 if(pubelm[0] > _pubsub_last){
757 _pubsubCurrent = pubelm[0];
758 _fireSubscribers(pubelm[1], pubelm[2]);
759 }
760 }
761
762 _pubsub_last = _pubsubCurrent;
763 }
764
765 /**
766 * Fires all subscriber listeners for a pubsub channel
767 *
768 * @param {String} channel Channel name
769 * @param {Mixed} payload Payload data to deliver
770 */
771 function _fireSubscribers(channel, payload){
772 if(_pubsub_observers[channel]){
773 for(var i=0, len = _pubsub_observers[channel].length; i<len; i++){
774 // send immutable data that can't be modified by listeners
775 _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
776 }
777 }
778 }
779
780 /**
781 * Remove old events from the publish stream (at least 2sec old)
782 */
783 function _dropOldEvents(){
784 if(!_storage.__jstorage_meta.PubSub){
785 return;
786 }
787
788 var retire = +new Date() - 2000;
789
790 for(var i=0, len = _storage.__jstorage_meta.PubSub.length; i<len; i++){
791 if(_storage.__jstorage_meta.PubSub[i][0] <= retire){
792 // deleteCount is needed for IE6
793 _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
794 break;
795 }
796 }
797
798 if(!_storage.__jstorage_meta.PubSub.length){
799 delete _storage.__jstorage_meta.PubSub;
800 }
801
802 }
803
804 /**
805 * Publish payload to a channel
806 *
807 * @param {String} channel Channel name
808 * @param {Mixed} payload Payload to send to the subscribers
809 */
810 function _publish(channel, payload){
811 if(!_storage.__jstorage_meta){
812 _storage.__jstorage_meta = {};
813 }
814 if(!_storage.__jstorage_meta.PubSub){
815 _storage.__jstorage_meta.PubSub = [];
816 }
817
818 _storage.__jstorage_meta.PubSub.unshift([+new Date, channel, payload]);
819
820 _save();
821 _publishChange();
822 }
823
824 /**
825 * CRC32 calculation based on http://noteslog.com/post/crc32-for-javascript/
826 *
827 * @param {String} str String to be hashed
828 * @param {Number} [crc] Last crc value in case of streams
829 */
830 function _crc32(str, crc){
831 crc = crc || 0;
832
833 var n = 0, //a number between 0 and 255
834 x = 0; //an hex number
835
836 crc = crc ^ (-1);
837 for(var i = 0, len = str.length; i < len; i++){
838 n = (crc ^ str.charCodeAt(i)) & 0xFF;
839 x = "0x" + _crc32Table.substr(n * 9, 8);
840 crc = (crc >>> 8)^x;
841 }
842 return crc^(-1);
843 }
844
845 ////////////////////////// PUBLIC INTERFACE /////////////////////////
846
847 $.jStorage = {
848 /* Version number */
849 version: JSTORAGE_VERSION,
850
851 /**
852 * Sets a key's value.
853 *
854 * @param {String} key Key to set. If this value is not set or not
855 * a string an exception is raised.
856 * @param {Mixed} value Value to set. This can be any value that is JSON
857 * compatible (Numbers, Strings, Objects etc.).
858 * @param {Object} [options] - possible options to use
859 * @param {Number} [options.TTL] - optional TTL value
860 * @return {Mixed} the used value
861 */
862 set: function(key, value, options){
863 _checkKey(key);
864
865 options = options || {};
866
867 // undefined values are deleted automatically
868 if(typeof value == "undefined"){
869 this.deleteKey(key);
870 return value;
871 }
872
873 if(_XMLService.isXML(value)){
874 value = {_is_xml:true,xml:_XMLService.encode(value)};
875 }else if(typeof value == "function"){
876 return undefined; // functions can't be saved!
877 }else if(value && typeof value == "object"){
878 // clone the object before saving to _storage tree
879 value = JSON.parse(JSON.stringify(value));
880 }
881
882 _storage[key] = value;
883
884 _storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value));
885
886 this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
887
888 _localStoragePolyfillSetKey(key, value);
889
890 _fireObservers(key, "updated");
891 return value;
892 },
893
894 /**
895 * Looks up a key in cache
896 *
897 * @param {String} key - Key to look up.
898 * @param {mixed} def - Default value to return, if key didn't exist.
899 * @return {Mixed} the key value, default value or null
900 */
901 get: function(key, def){
902 _checkKey(key);
903 if(key in _storage){
904 if(_storage[key] && typeof _storage[key] == "object" &&
905 _storage[key]._is_xml &&
906 _storage[key]._is_xml){
907 return _XMLService.decode(_storage[key].xml);
908 }else{
909 return _storage[key];
910 }
911 }
912 return typeof(def) == 'undefined' ? null : def;
913 },
914
915 /**
916 * Deletes a key from cache.
917 *
918 * @param {String} key - Key to delete.
919 * @return {Boolean} true if key existed or false if it didn't
920 */
921 deleteKey: function(key){
922 _checkKey(key);
923 if(key in _storage){
924 delete _storage[key];
925 // remove from TTL list
926 if(typeof _storage.__jstorage_meta.TTL == "object" &&
927 key in _storage.__jstorage_meta.TTL){
928 delete _storage.__jstorage_meta.TTL[key];
929 }
930
931 delete _storage.__jstorage_meta.CRC32[key];
932 _localStoragePolyfillSetKey(key, undefined);
933
934 _save();
935 _publishChange();
936 _fireObservers(key, "deleted");
937 return true;
938 }
939 return false;
940 },
941
942 /**
943 * Sets a TTL for a key, or remove it if ttl value is 0 or below
944 *
945 * @param {String} key - key to set the TTL for
946 * @param {Number} ttl - TTL timeout in milliseconds
947 * @return {Boolean} true if key existed or false if it didn't
948 */
949 setTTL: function(key, ttl){
950 var curtime = +new Date();
951 _checkKey(key);
952 ttl = Number(ttl) || 0;
953 if(key in _storage){
954
955 if(!_storage.__jstorage_meta.TTL){
956 _storage.__jstorage_meta.TTL = {};
957 }
958
959 // Set TTL value for the key
960 if(ttl>0){
961 _storage.__jstorage_meta.TTL[key] = curtime + ttl;
962 }else{
963 delete _storage.__jstorage_meta.TTL[key];
964 }
965
966 _save();
967
968 _handleTTL();
969
970 _publishChange();
971 return true;
972 }
973 return false;
974 },
975
976 /**
977 * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
978 *
979 * @param {String} key Key to check
980 * @return {Number} Remaining TTL in milliseconds
981 */
982 getTTL: function(key){
983 var curtime = +new Date(), ttl;
984 _checkKey(key);
985 if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){
986 ttl = _storage.__jstorage_meta.TTL[key] - curtime;
987 return ttl || 0;
988 }
989 return 0;
990 },
991
992 /**
993 * Deletes everything in cache.
994 *
995 * @return {Boolean} Always true
996 */
997 flush: function(){
998 _storage = {__jstorage_meta:{CRC32:{}}};
999 _createPolyfillStorage("local", true);
1000 _save();
1001 _publishChange();
1002 _fireObservers(null, "flushed");
1003 return true;
1004 },
1005
1006 /**
1007 * Returns a read-only copy of _storage
1008 *
1009 * @return {Object} Read-only copy of _storage
1010 */
1011 storageObj: function(){
1012 function F() {}
1013 F.prototype = _storage;
1014 return new F();
1015 },
1016
1017 /**
1018 * Returns an index of all used keys as an array
1019 * ['key1', 'key2',..'keyN']
1020 *
1021 * @return {Array} Used keys
1022 */
1023 index: function(){
1024 var index = [], i;
1025 for(i in _storage){
1026 if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){
1027 index.push(i);
1028 }
1029 }
1030 return index;
1031 },
1032
1033 /**
1034 * How much space in bytes does the storage take?
1035 *
1036 * @return {Number} Storage size in chars (not the same as in bytes,
1037 * since some chars may take several bytes)
1038 */
1039 storageSize: function(){
1040 return _storage_size;
1041 },
1042
1043 /**
1044 * Which backend is currently in use?
1045 *
1046 * @return {String} Backend name
1047 */
1048 currentBackend: function(){
1049 return _backend;
1050 },
1051
1052 /**
1053 * Test if storage is available
1054 *
1055 * @return {Boolean} True if storage can be used
1056 */
1057 storageAvailable: function(){
1058 return !!_backend;
1059 },
1060
1061 /**
1062 * Register change listeners
1063 *
1064 * @param {String} key Key name
1065 * @param {Function} callback Function to run when the key changes
1066 */
1067 listenKeyChange: function(key, callback){
1068 _checkKey(key);
1069 if(!_observers[key]){
1070 _observers[key] = [];
1071 }
1072 _observers[key].push(callback);
1073 },
1074
1075 /**
1076 * Remove change listeners
1077 *
1078 * @param {String} key Key name to unregister listeners against
1079 * @param {Function} [callback] If set, unregister the callback, if not - unregister all
1080 */
1081 stopListening: function(key, callback){
1082 _checkKey(key);
1083
1084 if(!_observers[key]){
1085 return;
1086 }
1087
1088 if(!callback){
1089 delete _observers[key];
1090 return;
1091 }
1092
1093 for(var i = _observers[key].length - 1; i>=0; i--){
1094 if(_observers[key][i] == callback){
1095 _observers[key].splice(i,1);
1096 }
1097 }
1098 },
1099
1100 /**
1101 * Subscribe to a Publish/Subscribe event stream
1102 *
1103 * @param {String} channel Channel name
1104 * @param {Function} callback Function to run when the something is published to the channel
1105 */
1106 subscribe: function(channel, callback){
1107 channel = (channel || "").toString();
1108 if(!channel){
1109 throw new TypeError('Channel not defined');
1110 }
1111 if(!_pubsub_observers[channel]){
1112 _pubsub_observers[channel] = [];
1113 }
1114 _pubsub_observers[channel].push(callback);
1115 },
1116
1117 /**
1118 * Publish data to an event stream
1119 *
1120 * @param {String} channel Channel name
1121 * @param {Mixed} payload Payload to deliver
1122 */
1123 publish: function(channel, payload){
1124 channel = (channel || "").toString();
1125 if(!channel){
1126 throw new TypeError('Channel not defined');
1127 }
1128
1129 _publish(channel, payload);
1130 },
1131
1132 /**
1133 * Reloads the data from browser storage
1134 */
1135 reInit: function(){
1136 _reloadData();
1137 }
1138 };
1139
1140 // Initialize jStorage
1141 _init();
1142
1143 })();