/* * ----------------------------- JSTORAGE ------------------------------------- * Simple local storage wrapper to save data on the browser side, supporting * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+ * * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com * Project homepage: www.jstorage.info * * Licensed under MIT-style license: * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function(){ var /* jStorage version */ JSTORAGE_VERSION = "0.3.0", /* detect a dollar object or create one if not found */ $ = window.jQuery || window.$ || (window.$ = {}), /* check for a JSON handling support */ JSON = { parse: window.JSON && (window.JSON.parse || window.JSON.decode) || String.prototype.evalJSON && function(str){return String(str).evalJSON();} || $.parseJSON || $.evalJSON, stringify: Object.toJSON || window.JSON && (window.JSON.stringify || window.JSON.encode) || $.toJSON }; // Break if no JSON support was found if(!JSON.parse || !JSON.stringify){ throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page"); } var /* This is the object, that holds the cached values */ _storage = {}, /* Actual browser storage (localStorage or globalStorage['domain']) */ _storage_service = {jStorage:"{}"}, /* DOM element for older IE versions, holds userData behavior */ _storage_elm = null, /* How much space does the storage take */ _storage_size = 0, /* which backend is currently used */ _backend = false, /* onchange observers */ _observers = {}, /* timeout to wait after onchange event */ _observer_timeout = false, /* last update time */ _observer_update = 0, /* pubsub observers */ _pubsub_observers = {}, /* skip published items older than current timestamp */ _pubsub_last = +new Date(), /* Next check for TTL */ _ttl_timeout, /* crc32 table */ _crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+ "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+ "6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+ "FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+ "A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+ "32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+ "56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+ "C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+ "E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+ "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+ "12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+ "A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+ "DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+ "5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+ "2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+ "04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+ "7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+ "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+ "A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+ "36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+ "5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+ "C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+ "EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+ "7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+ "18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+ "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+ "D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+ "53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+ "2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D", /** * XML encoding and decoding as XML nodes can't be JSON'ized * XML nodes are encoded and decoded if the node is the value to be saved * but not if it's as a property of another object * Eg. - * $.jStorage.set("key", xmlNode); // IS OK * $.jStorage.set("key", {xml: xmlNode}); // NOT OK */ _XMLService = { /** * Validates a XML node to be XML * based on jQuery.isXML function */ isXML: function(elm){ var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }, /** * Encodes a XML node to string * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/ */ encode: function(xmlNode) { if(!this.isXML(xmlNode)){ return false; } try{ // Mozilla, Webkit, Opera return new XMLSerializer().serializeToString(xmlNode); }catch(E1) { try { // IE return xmlNode.xml; }catch(E2){} } return false; }, /** * Decodes a XML node from string * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/ */ decode: function(xmlString){ var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) || (window.ActiveXObject && function(_xmlString) { var xml_doc = new ActiveXObject('Microsoft.XMLDOM'); xml_doc.async = 'false'; xml_doc.loadXML(_xmlString); return xml_doc; }), resultXML; if(!dom_parser){ return false; } resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml'); return this.isXML(resultXML)?resultXML:false; } }, _localStoragePolyfillSetKey = function(){}; ////////////////////////// PRIVATE METHODS //////////////////////// /** * Initialization function. Detects if the browser supports DOM Storage * or userData behavior and behaves accordingly. */ function _init(){ /* Check if browser supports localStorage */ var localStorageReallyWorks = false; if("localStorage" in window){ try { window.localStorage.setItem('_tmptest', 'tmpval'); localStorageReallyWorks = true; window.localStorage.removeItem('_tmptest'); } catch(BogusQuotaExceededErrorOnIos5) { // Thanks be to iOS5 Private Browsing mode which throws // QUOTA_EXCEEDED_ERRROR DOM Exception 22. } } if(localStorageReallyWorks){ try { if(window.localStorage) { _storage_service = window.localStorage; _backend = "localStorage"; _observer_update = _storage_service.jStorage_update; } } catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */} } /* Check if browser supports globalStorage */ else if("globalStorage" in window){ try { if(window.globalStorage) { _storage_service = window.globalStorage[window.location.hostname]; _backend = "globalStorage"; _observer_update = _storage_service.jStorage_update; } } catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */} } /* Check if browser supports userData behavior */ else { _storage_elm = document.createElement('link'); if(_storage_elm.addBehavior){ /* Use a DOM element to act as userData storage */ _storage_elm.style.behavior = 'url(#default#userData)'; /* userData element needs to be inserted into the DOM! */ document.getElementsByTagName('head')[0].appendChild(_storage_elm); try{ _storage_elm.load("jStorage"); }catch(E){ // try to reset cache _storage_elm.setAttribute("jStorage", "{}"); _storage_elm.save("jStorage"); _storage_elm.load("jStorage"); } var data = "{}"; try{ data = _storage_elm.getAttribute("jStorage"); }catch(E5){} try{ _observer_update = _storage_elm.getAttribute("jStorage_update"); }catch(E6){} _storage_service.jStorage = data; _backend = "userDataBehavior"; }else{ _storage_elm = null; return; } } // Load data from storage _load_storage(); // remove dead keys _handleTTL(); // create localStorage and sessionStorage polyfills if needed _createPolyfillStorage("local"); _createPolyfillStorage("session"); // start listening for changes _setupObserver(); // initialize publish-subscribe service _handlePubSub(); // handle cached navigation if("addEventListener" in window){ window.addEventListener("pageshow", function(event){ if(event.persisted){ _storageObserver(); } }, false); } } /** * Create a polyfill for localStorage (type="local") or sessionStorage (type="session") * * @param {String} type Either "local" or "session" * @param {Boolean} forceCreate If set to true, recreate the polyfill (needed with flush) */ function _createPolyfillStorage(type, forceCreate){ var _skipSave = false, _length = 0, i, storage, storage_source = {}; var rand = Math.random(); if(!forceCreate && typeof window[type+"Storage"] != "undefined"){ return; } // Use globalStorage for localStorage if available if(type == "local" && window.globalStorage){ localStorage = window.globalStorage[window.location.hostname]; return; } // only IE6/7 from this point on if(_backend != "userDataBehavior"){ return; } // Remove existing storage element if available if(forceCreate && window[type+"Storage"] && window[type+"Storage"].parentNode){ window[type+"Storage"].parentNode.removeChild(window[type+"Storage"]); } storage = document.createElement("button"); document.getElementsByTagName('head')[0].appendChild(storage); if(type == "local"){ storage_source = _storage; }else if(type == "session"){ _sessionStoragePolyfillUpdate(); } for(i in storage_source){ if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i != "length" && typeof storage_source[i] != "undefined"){ if(!(i in storage)){ _length++; } storage[i] = storage_source[i]; } } // Polyfill API /** * Indicates how many keys are stored in the storage */ storage.length = _length; /** * Returns the key of the nth stored value * * @param {Number} n Index position * @return {String} Key name of the nth stored value */ storage.key = function(n){ var count = 0, i; _sessionStoragePolyfillUpdate(); for(i in storage_source){ if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i!="length" && typeof storage_source[i] != "undefined"){ if(count == n){ return i; } count++; } } } /** * Returns the current value associated with the given key * * @param {String} key key name * @return {Mixed} Stored value */ storage.getItem = function(key){ _sessionStoragePolyfillUpdate(); if(type == "session"){ return storage_source[key]; } return $.jStorage.get(key); } /** * Sets or updates value for a give key * * @param {String} key Key name to be updated * @param {String} value String value to be stored */ storage.setItem = function(key, value){ if(typeof value == "undefined"){ return; } storage[key] = (value || "").toString(); } /** * Removes key from the storage * * @param {String} key Key name to be removed */ storage.removeItem = function(key){ if(type == "local"){ return $.jStorage.deleteKey(key); } storage[key] = undefined; _skipSave = true; if(key in storage){ storage.removeAttribute(key); } _skipSave = false; } /** * Clear storage */ storage.clear = function(){ if(type == "session"){ window.name = ""; _createPolyfillStorage("session", true); return; } $.jStorage.flush(); } if(type == "local"){ _localStoragePolyfillSetKey = function(key, value){ if(key == "length"){ return; } _skipSave = true; if(typeof value == "undefined"){ if(key in storage){ _length--; storage.removeAttribute(key); } }else{ if(!(key in storage)){ _length++; } storage[key] = (value || "").toString(); } storage.length = _length; _skipSave = false; } } function _sessionStoragePolyfillUpdate(){ if(type != "session"){ return; } try{ storage_source = JSON.parse(window.name || "{}"); }catch(E){ storage_source = {}; } } function _sessionStoragePolyfillSave(){ if(type != "session"){ return; } window.name = JSON.stringify(storage_source); }; storage.attachEvent("onpropertychange", function(e){ if(e.propertyName == "length"){ return; } if(_skipSave || e.propertyName == "length"){ return; } if(type == "local"){ if(!(e.propertyName in storage_source) && typeof storage[e.propertyName] != "undefined"){ _length ++; } }else if(type == "session"){ _sessionStoragePolyfillUpdate(); if(typeof storage[e.propertyName] != "undefined" && !(e.propertyName in storage_source)){ storage_source[e.propertyName] = storage[e.propertyName]; _length++; }else if(typeof storage[e.propertyName] == "undefined" && e.propertyName in storage_source){ delete storage_source[e.propertyName]; _length--; }else{ storage_source[e.propertyName] = storage[e.propertyName]; } _sessionStoragePolyfillSave(); storage.length = _length; return; } $.jStorage.set(e.propertyName, storage[e.propertyName]); storage.length = _length; }); window[type+"Storage"] = storage; } /** * Reload data from storage when needed */ function _reloadData(){ var data = "{}"; if(_backend == "userDataBehavior"){ _storage_elm.load("jStorage"); try{ data = _storage_elm.getAttribute("jStorage"); }catch(E5){} try{ _observer_update = _storage_elm.getAttribute("jStorage_update"); }catch(E6){} _storage_service.jStorage = data; } _load_storage(); // remove dead keys _handleTTL(); _handlePubSub(); } /** * Sets up a storage change observer */ function _setupObserver(){ if(_backend == "localStorage" || _backend == "globalStorage"){ if("addEventListener" in window){ window.addEventListener("storage", _storageObserver, false); }else{ document.attachEvent("onstorage", _storageObserver); } }else if(_backend == "userDataBehavior"){ setInterval(_storageObserver, 1000); } } /** * Fired on any kind of data change, needs to check if anything has * really been changed */ function _storageObserver(){ var updateTime; // cumulate change notifications with timeout clearTimeout(_observer_timeout); _observer_timeout = setTimeout(function(){ if(_backend == "localStorage" || _backend == "globalStorage"){ updateTime = _storage_service.jStorage_update; }else if(_backend == "userDataBehavior"){ _storage_elm.load("jStorage"); try{ updateTime = _storage_elm.getAttribute("jStorage_update"); }catch(E5){} } if(updateTime && updateTime != _observer_update){ _observer_update = updateTime; _checkUpdatedKeys(); } }, 25); } /** * Reloads the data and checks if any keys are changed */ function _checkUpdatedKeys(){ var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)), newCrc32List; _reloadData(); newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)); var key, updated = [], removed = []; for(key in oldCrc32List){ if(oldCrc32List.hasOwnProperty(key)){ if(!newCrc32List[key]){ removed.push(key); continue; } if(oldCrc32List[key] != newCrc32List[key]){ updated.push(key); } } } for(key in newCrc32List){ if(newCrc32List.hasOwnProperty(key)){ if(!oldCrc32List[key]){ updated.push(key); } } } _fireObservers(updated, "updated"); _fireObservers(removed, "deleted"); } /** * Fires observers for updated keys * * @param {Array|String} keys Array of key names or a key * @param {String} action What happened with the value (updated, deleted, flushed) */ function _fireObservers(keys, action){ keys = [].concat(keys || []); if(action == "flushed"){ keys = []; for(var key in _observers){ if(_observers.hasOwnProperty(key)){ keys.push(key); } } action = "deleted"; } for(var i=0, len = keys.length; i=0; i--){ pubelm = _storage.__jstorage_meta.PubSub[i]; if(pubelm[0] > _pubsub_last){ _pubsubCurrent = pubelm[0]; _fireSubscribers(pubelm[1], pubelm[2]); } } _pubsub_last = _pubsubCurrent; } /** * Fires all subscriber listeners for a pubsub channel * * @param {String} channel Channel name * @param {Mixed} payload Payload data to deliver */ function _fireSubscribers(channel, payload){ if(_pubsub_observers[channel]){ for(var i=0, len = _pubsub_observers[channel].length; i>> 8)^x; } return crc^(-1); } ////////////////////////// PUBLIC INTERFACE ///////////////////////// $.jStorage = { /* Version number */ version: JSTORAGE_VERSION, /** * Sets a key's value. * * @param {String} key Key to set. If this value is not set or not * a string an exception is raised. * @param {Mixed} value Value to set. This can be any value that is JSON * compatible (Numbers, Strings, Objects etc.). * @param {Object} [options] - possible options to use * @param {Number} [options.TTL] - optional TTL value * @return {Mixed} the used value */ set: function(key, value, options){ _checkKey(key); options = options || {}; // undefined values are deleted automatically if(typeof value == "undefined"){ this.deleteKey(key); return value; } if(_XMLService.isXML(value)){ value = {_is_xml:true,xml:_XMLService.encode(value)}; }else if(typeof value == "function"){ return undefined; // functions can't be saved! }else if(value && typeof value == "object"){ // clone the object before saving to _storage tree value = JSON.parse(JSON.stringify(value)); } _storage[key] = value; _storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value)); this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange _localStoragePolyfillSetKey(key, value); _fireObservers(key, "updated"); return value; }, /** * Looks up a key in cache * * @param {String} key - Key to look up. * @param {mixed} def - Default value to return, if key didn't exist. * @return {Mixed} the key value, default value or null */ get: function(key, def){ _checkKey(key); if(key in _storage){ if(_storage[key] && typeof _storage[key] == "object" && _storage[key]._is_xml && _storage[key]._is_xml){ return _XMLService.decode(_storage[key].xml); }else{ return _storage[key]; } } return typeof(def) == 'undefined' ? null : def; }, /** * Deletes a key from cache. * * @param {String} key - Key to delete. * @return {Boolean} true if key existed or false if it didn't */ deleteKey: function(key){ _checkKey(key); if(key in _storage){ delete _storage[key]; // remove from TTL list if(typeof _storage.__jstorage_meta.TTL == "object" && key in _storage.__jstorage_meta.TTL){ delete _storage.__jstorage_meta.TTL[key]; } delete _storage.__jstorage_meta.CRC32[key]; _localStoragePolyfillSetKey(key, undefined); _save(); _publishChange(); _fireObservers(key, "deleted"); return true; } return false; }, /** * Sets a TTL for a key, or remove it if ttl value is 0 or below * * @param {String} key - key to set the TTL for * @param {Number} ttl - TTL timeout in milliseconds * @return {Boolean} true if key existed or false if it didn't */ setTTL: function(key, ttl){ var curtime = +new Date(); _checkKey(key); ttl = Number(ttl) || 0; if(key in _storage){ if(!_storage.__jstorage_meta.TTL){ _storage.__jstorage_meta.TTL = {}; } // Set TTL value for the key if(ttl>0){ _storage.__jstorage_meta.TTL[key] = curtime + ttl; }else{ delete _storage.__jstorage_meta.TTL[key]; } _save(); _handleTTL(); _publishChange(); return true; } return false; }, /** * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set * * @param {String} key Key to check * @return {Number} Remaining TTL in milliseconds */ getTTL: function(key){ var curtime = +new Date(), ttl; _checkKey(key); if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){ ttl = _storage.__jstorage_meta.TTL[key] - curtime; return ttl || 0; } return 0; }, /** * Deletes everything in cache. * * @return {Boolean} Always true */ flush: function(){ _storage = {__jstorage_meta:{CRC32:{}}}; _createPolyfillStorage("local", true); _save(); _publishChange(); _fireObservers(null, "flushed"); return true; }, /** * Returns a read-only copy of _storage * * @return {Object} Read-only copy of _storage */ storageObj: function(){ function F() {} F.prototype = _storage; return new F(); }, /** * Returns an index of all used keys as an array * ['key1', 'key2',..'keyN'] * * @return {Array} Used keys */ index: function(){ var index = [], i; for(i in _storage){ if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){ index.push(i); } } return index; }, /** * How much space in bytes does the storage take? * * @return {Number} Storage size in chars (not the same as in bytes, * since some chars may take several bytes) */ storageSize: function(){ return _storage_size; }, /** * Which backend is currently in use? * * @return {String} Backend name */ currentBackend: function(){ return _backend; }, /** * Test if storage is available * * @return {Boolean} True if storage can be used */ storageAvailable: function(){ return !!_backend; }, /** * Register change listeners * * @param {String} key Key name * @param {Function} callback Function to run when the key changes */ listenKeyChange: function(key, callback){ _checkKey(key); if(!_observers[key]){ _observers[key] = []; } _observers[key].push(callback); }, /** * Remove change listeners * * @param {String} key Key name to unregister listeners against * @param {Function} [callback] If set, unregister the callback, if not - unregister all */ stopListening: function(key, callback){ _checkKey(key); if(!_observers[key]){ return; } if(!callback){ delete _observers[key]; return; } for(var i = _observers[key].length - 1; i>=0; i--){ if(_observers[key][i] == callback){ _observers[key].splice(i,1); } } }, /** * Subscribe to a Publish/Subscribe event stream * * @param {String} channel Channel name * @param {Function} callback Function to run when the something is published to the channel */ subscribe: function(channel, callback){ channel = (channel || "").toString(); if(!channel){ throw new TypeError('Channel not defined'); } if(!_pubsub_observers[channel]){ _pubsub_observers[channel] = []; } _pubsub_observers[channel].push(callback); }, /** * Publish data to an event stream * * @param {String} channel Channel name * @param {Mixed} payload Payload to deliver */ publish: function(channel, payload){ channel = (channel || "").toString(); if(!channel){ throw new TypeError('Channel not defined'); } _publish(channel, payload); }, /** * Reloads the data from browser storage */ reInit: function(){ _reloadData(); } }; // Initialize jStorage _init(); })();