From 99500504517a8763ffc80dee5dca9bda26a4a57b Mon Sep 17 00:00:00 2001 From: Marius Hoch Date: Sun, 23 Sep 2012 19:58:36 +0200 Subject: [PATCH] Update jStorage from upstream v0.1.7 to v0.3.0 Upstream: * src: https://raw.github.com/andris9/jStorage/v0.3.0/jstorage.js * log: https://github.com/andris9/jStorage/compare/v0.1.6.1...v0.3.0 Did not maintain the modifications made as they were not necessary (only stylistic), and jStorage is already listed in the .jshintignore file. Change-Id: I9c92c47ef165157a40412100e9e532bce36fc638 --- RELEASE-NOTES-1.20 | 6 +- resources/jquery/jquery.jStorage.js | 853 ++++++++++++++++++++++++---- 2 files changed, 735 insertions(+), 124 deletions(-) diff --git a/RELEASE-NOTES-1.20 b/RELEASE-NOTES-1.20 index b28cc70c10..979cfafd3f 100644 --- a/RELEASE-NOTES-1.20 +++ b/RELEASE-NOTES-1.20 @@ -132,11 +132,11 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. * (bug 22749) Create Special:MostInterwikis. * Show change tags when transclude Special:Recentchanges(linked) or Special:Newpages. * (bug 23226) Add |class= parameter to image links in order to add class(es) to HTML img tag. -* (bug 39431) SVG animated status is now shown in long description +* (bug 39431) SVG animated status is now shown in long description. * (bug 39376) jquery.form upgraded to 3.14. * SVG files will now show the actual width in the SVG's specified units in the metadata box. -* Added ResourceLoader module "jquery.jStorage". +* Added ResourceLoader module "jquery.jStorage" (v0.3.0, http://jStorage.info/). * (bug 39273) Added AJAX support for "Show changes" (diff) in LivePreview. * Added ResourceLoader module "jquery.badge". * mw.util.$content now points to the overall content area in the skin rather than just @@ -145,7 +145,7 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. with auto-hide, multi-message support, and message replacement tags. * jquery.messageBox which appears to be unused by both core and extensions has been removed. -* (bug 34939) Made link parsing insensitive ([HttP://]) +* (bug 34939) Made link parsing insensitive ([HttP://]). * (bug 40072) Add CSS classes to items in output of ChangesList pages. === Bug fixes in 1.20 === diff --git a/resources/jquery/jquery.jStorage.js b/resources/jquery/jquery.jStorage.js index 95959cf7a4..6ca21b5c53 100644 --- a/resources/jquery/jquery.jStorage.js +++ b/resources/jquery/jquery.jStorage.js @@ -3,12 +3,9 @@ * 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 Andris Reinman, andris.reinman@gmail.com + * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com * Project homepage: www.jstorage.info * - * Taken from Github with slight modifications by Hoo man - * https://raw.github.com/andris9/jStorage/master/jstorage.js - * * Licensed under MIT-style license: * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -27,52 +24,30 @@ * SOFTWARE. */ -/** - * $.jStorage - * - * USAGE: - * - * jStorage requires Prototype, MooTools or jQuery! If jQuery is used, then - * jQuery-JSON (http://code.google.com/p/jquery-json/) is also needed. - * (jQuery-JSON needs to be loaded BEFORE jStorage!) - * - * Methods: - * - * -set(key, value[, options]) - * $.jStorage.set(key, value) -> saves a value - * - * -get(key[, default]) - * value = $.jStorage.get(key [, default]) -> - * retrieves value if key exists, or default if it doesn't - * - * -deleteKey(key) - * $.jStorage.deleteKey(key) -> removes a key from the storage - * - * -flush() - * $.jStorage.flush() -> clears the cache - * - * -storageObj() - * $.jStorage.storageObj() -> returns a read-ony copy of the actual storage - * - * -storageSize() - * $.jStorage.storageSize() -> returns the size of the storage in bytes - * - * -index() - * $.jStorage.index() -> returns the used keys as an array - * - * -storageAvailable() - * $.jStorage.storageAvailable() -> returns true if storage is available - * - * -reInit() - * $.jStorage.reInit() -> reloads the data from browser storage - * - * can be any JSON-able value, including objects and arrays. - * - **/ + (function(){ + var + /* jStorage version */ + JSTORAGE_VERSION = "0.3.0", + + /* detect a dollar object or create one if not found */ + $ = window.jQuery || window.$ || (window.$ = {}), -(function($){ - if(!$ || !($.toJSON || Object.toJSON || window.JSON)){ - throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!"); + /* 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 @@ -88,20 +63,58 @@ /* How much space does the storage take */ _storage_size = 0, - /* function to encode objects to JSON strings */ - json_encode = $.toJSON || Object.toJSON || (window.JSON && (JSON.encode || JSON.stringify)), - - /* function to decode objects from JSON strings */ - json_decode = $.evalJSON || (window.JSON && (JSON.decode || JSON.parse)) || function(str){ - return String(str).evalJSON(); - }, - /* 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 @@ -158,14 +171,16 @@ 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. - * @returns undefined */ function _init(){ /* Check if browser supports localStorage */ @@ -180,11 +195,13 @@ // 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 */} } @@ -194,6 +211,7 @@ 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 */} } @@ -208,11 +226,24 @@ /* userData element needs to be inserted into the DOM! */ document.getElementsByTagName('head')[0].appendChild(_storage_elm); - _storage_elm.load("jStorage"); + 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{ @@ -221,35 +252,427 @@ } } + // 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: "0.1.7.0", + version: JSTORAGE_VERSION, /** * Sets a key's value. * - * @param {String} key - Key to set. If this value is not set or not + * @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 + * @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 - * @returns the used 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"){ - value = null; // functions can't be saved! - }else if(value && typeof value === "object"){ + }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_decode(json_encode(value)); + value = JSON.parse(JSON.stringify(value)); } + _storage[key] = value; - if(!isNaN(options.TTL)){ - this.setTTL(key, options.TTL); - // also handles saving - }else{ - _save(); - } + _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; }, @@ -356,12 +896,12 @@ * * @param {String} key - Key to look up. * @param {mixed} def - Default value to return, if key didn't exist. - * @returns the key value, default value or + * @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" && + if(_storage[key] && typeof _storage[key] == "object" && _storage[key]._is_xml && _storage[key]._is_xml){ return _XMLService.decode(_storage[key].xml); @@ -369,26 +909,31 @@ return _storage[key]; } } - return typeof(def) === 'undefined' ? null : def; + return typeof(def) == 'undefined' ? null : def; }, /** * Deletes a key from cache. * * @param {String} key - Key to delete. - * @returns true if key existed or false if it didn't + * @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(_storage.__jstorage_meta && - typeof _storage.__jstorage_meta.TTL === "object" && + 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; @@ -399,7 +944,7 @@ * * @param {String} key - key to set the TTL for * @param {Number} ttl - TTL timeout in milliseconds - * @returns true if key existed or false if it didn't + * @return {Boolean} true if key existed or false if it didn't */ setTTL: function(key, ttl){ var curtime = +new Date(); @@ -407,9 +952,6 @@ ttl = Number(ttl) || 0; if(key in _storage){ - if(!_storage.__jstorage_meta){ - _storage.__jstorage_meta = {}; - } if(!_storage.__jstorage_meta.TTL){ _storage.__jstorage_meta.TTL = {}; } @@ -424,26 +966,47 @@ _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 true + * @return {Boolean} Always true */ flush: function(){ - _storage = {}; + _storage = {__jstorage_meta:{CRC32:{}}}; + _createPolyfillStorage("local", true); _save(); + _publishChange(); + _fireObservers(null, "flushed"); return true; }, /** * Returns a read-only copy of _storage * - * @returns Object + * @return {Object} Read-only copy of _storage */ storageObj: function(){ function F() {} @@ -455,12 +1018,12 @@ * Returns an index of all used keys as an array * ['key1', 'key2',..'keyN'] * - * @returns Array + * @return {Array} Used keys */ index: function(){ var index = [], i; for(i in _storage){ - if(_storage.hasOwnProperty(i) && i !== "__jstorage_meta"){ + if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){ index.push(i); } } @@ -470,7 +1033,8 @@ /** * How much space in bytes does the storage take? * - * @returns Number + * @return {Number} Storage size in chars (not the same as in bytes, + * since some chars may take several bytes) */ storageSize: function(){ return _storage_size; @@ -479,7 +1043,7 @@ /** * Which backend is currently in use? * - * @returns String + * @return {String} Backend name */ currentBackend: function(){ return _backend; @@ -488,45 +1052,92 @@ /** * Test if storage is available * - * @returns Boolean + * @return {Boolean} True if storage can be used */ storageAvailable: function(){ return !!_backend; }, /** - * Reloads the data from browser storage + * Register change listeners * - * @returns undefined + * @param {String} key Key name + * @param {Function} callback Function to run when the key changes */ - reInit: function(){ - var new_storage_elm, data; - if(_storage_elm && _storage_elm.addBehavior){ - new_storage_elm = document.createElement('link'); + listenKeyChange: function(key, callback){ + _checkKey(key); + if(!_observers[key]){ + _observers[key] = []; + } + _observers[key].push(callback); + }, - _storage_elm.parentNode.replaceChild(new_storage_elm, _storage_elm); - _storage_elm = new_storage_elm; + /** + * 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); - /* Use a DOM element to act as userData storage */ - _storage_elm.style.behavior = 'url(#default#userData)'; + if(!_observers[key]){ + return; + } - /* userData element needs to be inserted into the DOM! */ - document.getElementsByTagName('head')[0].appendChild(_storage_elm); + if(!callback){ + delete _observers[key]; + return; + } - _storage_elm.load("jStorage"); - data = "{}"; - try{ - data = _storage_elm.getAttribute("jStorage"); - }catch(E5){} - _storage_service.jStorage = data; - _backend = "userDataBehavior"; + 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); + }, - _load_storage(); + /** + * 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(); -})(window.$ || window.jQuery); +})(); -- 2.20.1