mirror of
https://github.com/YunoHost-Apps/mediawiki_ynh.git
synced 2024-09-03 19:46:05 +02:00
1143 lines
37 KiB
JavaScript
1143 lines
37 KiB
JavaScript
/*
|
|
* ----------------------------- 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<len; i++){
|
|
if(_observers[keys[i]]){
|
|
for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
|
|
_observers[keys[i]][j](keys[i], action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Publishes key change to listeners
|
|
*/
|
|
function _publishChange(){
|
|
var updateTime = (+new Date()).toString();
|
|
|
|
if(_backend == "localStorage" || _backend == "globalStorage"){
|
|
_storage_service.jStorage_update = updateTime;
|
|
}else if(_backend == "userDataBehavior"){
|
|
_storage_elm.setAttribute("jStorage_update", updateTime);
|
|
_storage_elm.save("jStorage");
|
|
}
|
|
|
|
_storageObserver();
|
|
}
|
|
|
|
/**
|
|
* Loads the data from the storage based on the supported mechanism
|
|
*/
|
|
function _load_storage(){
|
|
/* if jStorage string is retrieved, then decode it */
|
|
if(_storage_service.jStorage){
|
|
try{
|
|
_storage = JSON.parse(String(_storage_service.jStorage));
|
|
}catch(E6){_storage_service.jStorage = "{}";}
|
|
}else{
|
|
_storage_service.jStorage = "{}";
|
|
}
|
|
_storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
|
|
|
|
if(!_storage.__jstorage_meta){
|
|
_storage.__jstorage_meta = {};
|
|
}
|
|
if(!_storage.__jstorage_meta.CRC32){
|
|
_storage.__jstorage_meta.CRC32 = {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This functions provides the "save" mechanism to store the jStorage object
|
|
*/
|
|
function _save(){
|
|
_dropOldEvents(); // remove expired events
|
|
try{
|
|
_storage_service.jStorage = JSON.stringify(_storage);
|
|
// If userData is used as the storage engine, additional
|
|
if(_storage_elm) {
|
|
_storage_elm.setAttribute("jStorage",_storage_service.jStorage);
|
|
_storage_elm.save("jStorage");
|
|
}
|
|
_storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
|
|
}catch(E7){/* probably cache is full, nothing is saved this way*/}
|
|
}
|
|
|
|
/**
|
|
* Function checks if a key is set and is string or numberic
|
|
*
|
|
* @param {String} key Key name
|
|
*/
|
|
function _checkKey(key){
|
|
if(!key || (typeof key != "string" && typeof key != "number")){
|
|
throw new TypeError('Key name must be string or numeric');
|
|
}
|
|
if(key == "__jstorage_meta"){
|
|
throw new TypeError('Reserved key name');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Removes expired keys
|
|
*/
|
|
function _handleTTL(){
|
|
var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
|
|
|
|
clearTimeout(_ttl_timeout);
|
|
|
|
if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
|
|
// nothing to do here
|
|
return;
|
|
}
|
|
|
|
curtime = +new Date();
|
|
TTL = _storage.__jstorage_meta.TTL;
|
|
|
|
CRC32 = _storage.__jstorage_meta.CRC32;
|
|
for(i in TTL){
|
|
if(TTL.hasOwnProperty(i)){
|
|
if(TTL[i] <= curtime){
|
|
delete TTL[i];
|
|
delete CRC32[i];
|
|
delete _storage[i];
|
|
changed = true;
|
|
deleted.push(i);
|
|
}else if(TTL[i] < nextExpire){
|
|
nextExpire = TTL[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// set next check
|
|
if(nextExpire != Infinity){
|
|
_ttl_timeout = setTimeout(_handleTTL, nextExpire - curtime);
|
|
}
|
|
|
|
// save changes
|
|
if(changed){
|
|
_save();
|
|
_publishChange();
|
|
_fireObservers(deleted, "deleted");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if there's any events on hold to be fired to listeners
|
|
*/
|
|
function _handlePubSub(){
|
|
if(!_storage.__jstorage_meta.PubSub){
|
|
return;
|
|
}
|
|
var pubelm,
|
|
_pubsubCurrent = _pubsub_last;
|
|
|
|
for(var i=len=_storage.__jstorage_meta.PubSub.length-1; 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<len; i++){
|
|
// send immutable data that can't be modified by listeners
|
|
_pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove old events from the publish stream (at least 2sec old)
|
|
*/
|
|
function _dropOldEvents(){
|
|
if(!_storage.__jstorage_meta.PubSub){
|
|
return;
|
|
}
|
|
|
|
var retire = +new Date() - 2000;
|
|
|
|
for(var i=0, len = _storage.__jstorage_meta.PubSub.length; i<len; i++){
|
|
if(_storage.__jstorage_meta.PubSub[i][0] <= retire){
|
|
// deleteCount is needed for IE6
|
|
_storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!_storage.__jstorage_meta.PubSub.length){
|
|
delete _storage.__jstorage_meta.PubSub;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Publish payload to a channel
|
|
*
|
|
* @param {String} channel Channel name
|
|
* @param {Mixed} payload Payload to send to the subscribers
|
|
*/
|
|
function _publish(channel, payload){
|
|
if(!_storage.__jstorage_meta){
|
|
_storage.__jstorage_meta = {};
|
|
}
|
|
if(!_storage.__jstorage_meta.PubSub){
|
|
_storage.__jstorage_meta.PubSub = [];
|
|
}
|
|
|
|
_storage.__jstorage_meta.PubSub.unshift([+new Date, channel, payload]);
|
|
|
|
_save();
|
|
_publishChange();
|
|
}
|
|
|
|
/**
|
|
* CRC32 calculation based on http://noteslog.com/post/crc32-for-javascript/
|
|
*
|
|
* @param {String} str String to be hashed
|
|
* @param {Number} [crc] Last crc value in case of streams
|
|
*/
|
|
function _crc32(str, crc){
|
|
crc = crc || 0;
|
|
|
|
var n = 0, //a number between 0 and 255
|
|
x = 0; //an hex number
|
|
|
|
crc = crc ^ (-1);
|
|
for(var i = 0, len = str.length; i < len; i++){
|
|
n = (crc ^ str.charCodeAt(i)) & 0xFF;
|
|
x = "0x" + _crc32Table.substr(n * 9, 8);
|
|
crc = (crc >>> 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();
|
|
|
|
})();
|