forked from sent/waves
1648 lines
53 KiB
JavaScript
1648 lines
53 KiB
JavaScript
/*
|
|
http://www.JSON.org/json2.js
|
|
2011-02-23
|
|
|
|
Public Domain.
|
|
|
|
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
|
|
|
See http://www.JSON.org/js.html
|
|
|
|
|
|
This code should be minified before deployment.
|
|
See http://javascript.crockford.com/jsmin.html
|
|
|
|
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
|
NOT CONTROL.
|
|
|
|
|
|
This file creates a global JSON object containing two methods: stringify
|
|
and parse.
|
|
|
|
JSON.stringify(value, replacer, space)
|
|
value any JavaScript value, usually an object or array.
|
|
|
|
replacer an optional parameter that determines how object
|
|
values are stringified for objects. It can be a
|
|
function or an array of strings.
|
|
|
|
space an optional parameter that specifies the indentation
|
|
of nested structures. If it is omitted, the text will
|
|
be packed without extra whitespace. If it is a number,
|
|
it will specify the number of spaces to indent at each
|
|
level. If it is a string (such as '\t' or ' '),
|
|
it contains the characters used to indent at each level.
|
|
|
|
This method produces a JSON text from a JavaScript value.
|
|
|
|
When an object value is found, if the object contains a toJSON
|
|
method, its toJSON method will be called and the result will be
|
|
stringified. A toJSON method does not serialize: it returns the
|
|
value represented by the name/value pair that should be serialized,
|
|
or undefined if nothing should be serialized. The toJSON method
|
|
will be passed the key associated with the value, and this will be
|
|
bound to the value
|
|
|
|
For example, this would serialize Dates as ISO strings.
|
|
|
|
Date.prototype.toJSON = function (key) {
|
|
function f(n) {
|
|
// Format integers to have at least two digits.
|
|
return n < 10 ? '0' + n : n;
|
|
}
|
|
|
|
return this.getUTCFullYear() + '-' +
|
|
f(this.getUTCMonth() + 1) + '-' +
|
|
f(this.getUTCDate()) + 'T' +
|
|
f(this.getUTCHours()) + ':' +
|
|
f(this.getUTCMinutes()) + ':' +
|
|
f(this.getUTCSeconds()) + 'Z';
|
|
};
|
|
|
|
You can provide an optional replacer method. It will be passed the
|
|
key and value of each member, with this bound to the containing
|
|
object. The value that is returned from your method will be
|
|
serialized. If your method returns undefined, then the member will
|
|
be excluded from the serialization.
|
|
|
|
If the replacer parameter is an array of strings, then it will be
|
|
used to select the members to be serialized. It filters the results
|
|
such that only members with keys listed in the replacer array are
|
|
stringified.
|
|
|
|
Values that do not have JSON representations, such as undefined or
|
|
functions, will not be serialized. Such values in objects will be
|
|
dropped; in arrays they will be replaced with null. You can use
|
|
a replacer function to replace those with JSON values.
|
|
JSON.stringify(undefined) returns undefined.
|
|
|
|
The optional space parameter produces a stringification of the
|
|
value that is filled with line breaks and indentation to make it
|
|
easier to read.
|
|
|
|
If the space parameter is a non-empty string, then that string will
|
|
be used for indentation. If the space parameter is a number, then
|
|
the indentation will be that many spaces.
|
|
|
|
Example:
|
|
|
|
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
|
// text is '["e",{"pluribus":"unum"}]'
|
|
|
|
|
|
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
|
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
|
|
|
text = JSON.stringify([new Date()], function (key, value) {
|
|
return this[key] instanceof Date ?
|
|
'Date(' + this[key] + ')' : value;
|
|
});
|
|
// text is '["Date(---current time---)"]'
|
|
|
|
|
|
JSON.parse(text, reviver)
|
|
This method parses a JSON text to produce an object or array.
|
|
It can throw a SyntaxError exception.
|
|
|
|
The optional reviver parameter is a function that can filter and
|
|
transform the results. It receives each of the keys and values,
|
|
and its return value is used instead of the original value.
|
|
If it returns what it received, then the structure is not modified.
|
|
If it returns undefined then the member is deleted.
|
|
|
|
Example:
|
|
|
|
// Parse the text. Values that look like ISO date strings will
|
|
// be converted to Date objects.
|
|
|
|
myData = JSON.parse(text, function (key, value) {
|
|
var a;
|
|
if (typeof value === 'string') {
|
|
a =
|
|
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
|
if (a) {
|
|
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
|
+a[5], +a[6]));
|
|
}
|
|
}
|
|
return value;
|
|
});
|
|
|
|
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
|
var d;
|
|
if (typeof value === 'string' &&
|
|
value.slice(0, 5) === 'Date(' &&
|
|
value.slice(-1) === ')') {
|
|
d = new Date(value.slice(5, -1));
|
|
if (d) {
|
|
return d;
|
|
}
|
|
}
|
|
return value;
|
|
});
|
|
|
|
|
|
This is a reference implementation. You are free to copy, modify, or
|
|
redistribute.
|
|
*/
|
|
|
|
/*jslint evil: true, strict: false, regexp: false */
|
|
|
|
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
|
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
|
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
|
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
|
test, toJSON, toString, valueOf
|
|
*/
|
|
|
|
|
|
// Create a JSON object only if one does not already exist. We create the
|
|
// methods in a closure to avoid creating global variables.
|
|
|
|
var JSON;
|
|
if (!JSON) {
|
|
JSON = {};
|
|
}
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
function f(n) {
|
|
// Format integers to have at least two digits.
|
|
return n < 10 ? '0' + n : n;
|
|
}
|
|
|
|
if (typeof Date.prototype.toJSON !== 'function') {
|
|
|
|
Date.prototype.toJSON = function (key) {
|
|
|
|
return isFinite(this.valueOf()) ?
|
|
this.getUTCFullYear() + '-' +
|
|
f(this.getUTCMonth() + 1) + '-' +
|
|
f(this.getUTCDate()) + 'T' +
|
|
f(this.getUTCHours()) + ':' +
|
|
f(this.getUTCMinutes()) + ':' +
|
|
f(this.getUTCSeconds()) + 'Z' : null;
|
|
};
|
|
|
|
String.prototype.toJSON =
|
|
Number.prototype.toJSON =
|
|
Boolean.prototype.toJSON = function (key) {
|
|
return this.valueOf();
|
|
};
|
|
}
|
|
|
|
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
|
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
|
gap,
|
|
indent,
|
|
meta = { // table of character substitutions
|
|
'\b': '\\b',
|
|
'\t': '\\t',
|
|
'\n': '\\n',
|
|
'\f': '\\f',
|
|
'\r': '\\r',
|
|
'"' : '\\"',
|
|
'\\': '\\\\'
|
|
},
|
|
rep;
|
|
|
|
|
|
function quote(string) {
|
|
|
|
// If the string contains no control characters, no quote characters, and no
|
|
// backslash characters, then we can safely slap some quotes around it.
|
|
// Otherwise we must also replace the offending characters with safe escape
|
|
// sequences.
|
|
|
|
escapable.lastIndex = 0;
|
|
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
|
|
var c = meta[a];
|
|
return typeof c === 'string' ? c :
|
|
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
|
}) + '"' : '"' + string + '"';
|
|
}
|
|
|
|
|
|
function str(key, holder) {
|
|
|
|
// Produce a string from holder[key].
|
|
|
|
var i, // The loop counter.
|
|
k, // The member key.
|
|
v, // The member value.
|
|
length,
|
|
mind = gap,
|
|
partial,
|
|
value = holder[key];
|
|
|
|
// If the value has a toJSON method, call it to obtain a replacement value.
|
|
|
|
if (value && typeof value === 'object' &&
|
|
typeof value.toJSON === 'function') {
|
|
value = value.toJSON(key);
|
|
}
|
|
|
|
// If we were called with a replacer function, then call the replacer to
|
|
// obtain a replacement value.
|
|
|
|
if (typeof rep === 'function') {
|
|
value = rep.call(holder, key, value);
|
|
}
|
|
|
|
// What happens next depends on the value's type.
|
|
|
|
switch (typeof value) {
|
|
case 'string':
|
|
return quote(value);
|
|
|
|
case 'number':
|
|
|
|
// JSON numbers must be finite. Encode non-finite numbers as null.
|
|
|
|
return isFinite(value) ? String(value) : 'null';
|
|
|
|
case 'boolean':
|
|
case 'null':
|
|
|
|
// If the value is a boolean or null, convert it to a string. Note:
|
|
// typeof null does not produce 'null'. The case is included here in
|
|
// the remote chance that this gets fixed someday.
|
|
|
|
return String(value);
|
|
|
|
// If the type is 'object', we might be dealing with an object or an array or
|
|
// null.
|
|
|
|
case 'object':
|
|
|
|
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
|
// so watch out for that case.
|
|
|
|
if (!value) {
|
|
return 'null';
|
|
}
|
|
|
|
// Make an array to hold the partial results of stringifying this object value.
|
|
|
|
gap += indent;
|
|
partial = [];
|
|
|
|
// Is the value an array?
|
|
|
|
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
|
|
|
// The value is an array. Stringify every element. Use null as a placeholder
|
|
// for non-JSON values.
|
|
|
|
length = value.length;
|
|
for (i = 0; i < length; i += 1) {
|
|
partial[i] = str(i, value) || 'null';
|
|
}
|
|
|
|
// Join all of the elements together, separated with commas, and wrap them in
|
|
// brackets.
|
|
|
|
v = partial.length === 0 ? '[]' : gap ?
|
|
'[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
|
|
'[' + partial.join(',') + ']';
|
|
gap = mind;
|
|
return v;
|
|
}
|
|
|
|
// If the replacer is an array, use it to select the members to be stringified.
|
|
|
|
if (rep && typeof rep === 'object') {
|
|
length = rep.length;
|
|
for (i = 0; i < length; i += 1) {
|
|
if (typeof rep[i] === 'string') {
|
|
k = rep[i];
|
|
v = str(k, value);
|
|
if (v) {
|
|
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
|
|
// Otherwise, iterate through all of the keys in the object.
|
|
|
|
for (k in value) {
|
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
v = str(k, value);
|
|
if (v) {
|
|
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Join all of the member texts together, separated with commas,
|
|
// and wrap them in braces.
|
|
|
|
v = partial.length === 0 ? '{}' : gap ?
|
|
'{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
|
|
'{' + partial.join(',') + '}';
|
|
gap = mind;
|
|
return v;
|
|
}
|
|
}
|
|
|
|
// If the JSON object does not yet have a stringify method, give it one.
|
|
|
|
if (typeof JSON.stringify !== 'function') {
|
|
JSON.stringify = function (value, replacer, space) {
|
|
|
|
// The stringify method takes a value and an optional replacer, and an optional
|
|
// space parameter, and returns a JSON text. The replacer can be a function
|
|
// that can replace values, or an array of strings that will select the keys.
|
|
// A default replacer method can be provided. Use of the space parameter can
|
|
// produce text that is more easily readable.
|
|
|
|
var i;
|
|
gap = '';
|
|
indent = '';
|
|
|
|
// If the space parameter is a number, make an indent string containing that
|
|
// many spaces.
|
|
|
|
if (typeof space === 'number') {
|
|
for (i = 0; i < space; i += 1) {
|
|
indent += ' ';
|
|
}
|
|
|
|
// If the space parameter is a string, it will be used as the indent string.
|
|
|
|
} else if (typeof space === 'string') {
|
|
indent = space;
|
|
}
|
|
|
|
// If there is a replacer, it must be a function or an array.
|
|
// Otherwise, throw an error.
|
|
|
|
rep = replacer;
|
|
if (replacer && typeof replacer !== 'function' &&
|
|
(typeof replacer !== 'object' ||
|
|
typeof replacer.length !== 'number')) {
|
|
throw new Error('JSON.stringify');
|
|
}
|
|
|
|
// Make a fake root object containing our value under the key of ''.
|
|
// Return the result of stringifying the value.
|
|
|
|
return str('', {'': value});
|
|
};
|
|
}
|
|
|
|
|
|
// If the JSON object does not yet have a parse method, give it one.
|
|
|
|
if (typeof JSON.parse !== 'function') {
|
|
JSON.parse = function (text, reviver) {
|
|
|
|
// The parse method takes a text and an optional reviver function, and returns
|
|
// a JavaScript value if the text is a valid JSON text.
|
|
|
|
var j;
|
|
|
|
function walk(holder, key) {
|
|
|
|
// The walk method is used to recursively walk the resulting structure so
|
|
// that modifications can be made.
|
|
|
|
var k, v, value = holder[key];
|
|
if (value && typeof value === 'object') {
|
|
for (k in value) {
|
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
v = walk(value, k);
|
|
if (v !== undefined) {
|
|
value[k] = v;
|
|
} else {
|
|
delete value[k];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return reviver.call(holder, key, value);
|
|
}
|
|
|
|
|
|
// Parsing happens in four stages. In the first stage, we replace certain
|
|
// Unicode characters with escape sequences. JavaScript handles many characters
|
|
// incorrectly, either silently deleting them, or treating them as line endings.
|
|
|
|
text = String(text);
|
|
cx.lastIndex = 0;
|
|
if (cx.test(text)) {
|
|
text = text.replace(cx, function (a) {
|
|
return '\\u' +
|
|
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
|
});
|
|
}
|
|
|
|
// In the second stage, we run the text against regular expressions that look
|
|
// for non-JSON patterns. We are especially concerned with '()' and 'new'
|
|
// because they can cause invocation, and '=' because it can cause mutation.
|
|
// But just to be safe, we want to reject all unexpected forms.
|
|
|
|
// We split the second stage into 4 regexp operations in order to work around
|
|
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
|
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
|
|
// replace all simple value tokens with ']' characters. Third, we delete all
|
|
// open brackets that follow a colon or comma or that begin the text. Finally,
|
|
// we look to see that the remaining characters are only whitespace or ']' or
|
|
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
|
|
|
|
if (/^[\],:{}\s]*$/
|
|
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
|
|
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
|
|
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
|
|
|
// In the third stage we use the eval function to compile the text into a
|
|
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
|
|
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
|
// in parens to eliminate the ambiguity.
|
|
|
|
j = eval('(' + text + ')');
|
|
|
|
// In the optional fourth stage, we recursively walk the new structure, passing
|
|
// each name/value pair to a reviver function for possible transformation.
|
|
|
|
return typeof reviver === 'function' ?
|
|
walk({'': j}, '') : j;
|
|
}
|
|
|
|
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
|
|
|
throw new SyntaxError('JSON.parse');
|
|
};
|
|
}
|
|
}());
|
|
/*
|
|
* ----------------------------- JSTORAGE -------------------------------------
|
|
* Simple local storage wrapper to save data on the browser side, supporting
|
|
* all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
|
|
*
|
|
* Author: Andris Reinman, andris.reinman@gmail.com
|
|
* Project homepage: www.jstorage.info
|
|
*
|
|
* Licensed under Unlicense:
|
|
*
|
|
* This is free and unencumbered software released into the public domain.
|
|
*
|
|
* Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
* distribute this software, either in source code form or as a compiled
|
|
* binary, for any purpose, commercial or non-commercial, and by any
|
|
* means.
|
|
*
|
|
* In jurisdictions that recognize copyright laws, the author or authors
|
|
* of this software dedicate any and all copyright interest in the
|
|
* software to the public domain. We make this dedication for the benefit
|
|
* of the public at large and to the detriment of our heirs and
|
|
* successors. We intend this dedication to be an overt act of
|
|
* relinquishment in perpetuity of all present and future rights to this
|
|
* software under copyright law.
|
|
*
|
|
* 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 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.
|
|
*
|
|
* For more information, please refer to <http://unlicense.org/>
|
|
*/
|
|
|
|
/* global ActiveXObject: false */
|
|
/* jshint browser: true */
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
var
|
|
/* jStorage version */
|
|
JSTORAGE_VERSION = '0.4.12',
|
|
|
|
/* 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 (typeof JSON.parse !== 'function' || typeof JSON.stringify !== 'function') {
|
|
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 = {
|
|
__jstorage_meta: {
|
|
CRC32: {}
|
|
}
|
|
},
|
|
|
|
/* 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,
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
};
|
|
|
|
|
|
////////////////////////// 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) {
|
|
if (window.location.hostname == 'localhost') {
|
|
_storage_service = window.globalStorage['localhost.localdomain'];
|
|
} else {
|
|
_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();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
|
|
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 || []);
|
|
|
|
var i, j, len, jlen;
|
|
|
|
if (action == 'flushed') {
|
|
keys = [];
|
|
for (var key in _observers) {
|
|
if (_observers.hasOwnProperty(key)) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
action = 'deleted';
|
|
}
|
|
for (i = 0, len = keys.length; i < len; i++) {
|
|
if (_observers[keys[i]]) {
|
|
for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
|
|
_observers[keys[i]][j](keys[i], action);
|
|
}
|
|
}
|
|
if (_observers['*']) {
|
|
for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
|
|
_observers['*'][j](keys[i], action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Publishes key change to listeners
|
|
*/
|
|
function _publishChange() {
|
|
var updateTime = (+new Date()).toString();
|
|
|
|
if (_backend == 'localStorage' || _backend == 'globalStorage') {
|
|
try {
|
|
_storage_service.jStorage_update = updateTime;
|
|
} catch (E8) {
|
|
// safari private mode has been enabled after the jStorage initialization
|
|
_backend = false;
|
|
}
|
|
} 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 (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, Math.min(nextExpire - curtime, 0x7FFFFFFF));
|
|
}
|
|
|
|
// save changes
|
|
if (changed) {
|
|
_save();
|
|
_publishChange();
|
|
_fireObservers(deleted, 'deleted');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if there's any events on hold to be fired to listeners
|
|
*/
|
|
function _handlePubSub() {
|
|
var i, len;
|
|
if (!_storage.__jstorage_meta.PubSub) {
|
|
return;
|
|
}
|
|
var pubelm,
|
|
_pubsubCurrent = _pubsub_last,
|
|
needFired = [];
|
|
|
|
for (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];
|
|
needFired.unshift(pubelm);
|
|
}
|
|
}
|
|
|
|
for (i = needFired.length - 1; i >= 0; i--) {
|
|
_fireSubscribers(needFired[i][1], needFired[i][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
|
|
try {
|
|
_pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
|
|
} catch (E) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
|
|
/**
|
|
* JS Implementation of MurmurHash2
|
|
*
|
|
* SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
|
|
*
|
|
* @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
|
|
* @see http://github.com/garycourt/murmurhash-js
|
|
* @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
|
|
* @see http://sites.google.com/site/murmurhash/
|
|
*
|
|
* @param {string} str ASCII only
|
|
* @param {number} seed Positive integer only
|
|
* @return {number} 32-bit positive integer hash
|
|
*/
|
|
|
|
function murmurhash2_32_gc(str, seed) {
|
|
var
|
|
l = str.length,
|
|
h = seed ^ l,
|
|
i = 0,
|
|
k;
|
|
|
|
while (l >= 4) {
|
|
k =
|
|
((str.charCodeAt(i) & 0xff)) |
|
|
((str.charCodeAt(++i) & 0xff) << 8) |
|
|
((str.charCodeAt(++i) & 0xff) << 16) |
|
|
((str.charCodeAt(++i) & 0xff) << 24);
|
|
|
|
k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
|
|
k ^= k >>> 24;
|
|
k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
|
|
|
|
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
|
|
|
|
l -= 4;
|
|
++i;
|
|
}
|
|
|
|
switch (l) {
|
|
case 3:
|
|
h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
|
|
/* falls through */
|
|
case 2:
|
|
h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
|
|
/* falls through */
|
|
case 1:
|
|
h ^= (str.charCodeAt(i) & 0xff);
|
|
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
|
|
}
|
|
|
|
h ^= h >>> 13;
|
|
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
|
|
h ^= h >>> 15;
|
|
|
|
return h >>> 0;
|
|
}
|
|
|
|
////////////////////////// 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, in milliseconds
|
|
* @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] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
|
|
|
|
this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
|
|
|
|
_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) {
|
|
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];
|
|
|
|
_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: {}
|
|
}
|
|
};
|
|
_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();
|
|
},
|
|
|
|
/**
|
|
* Removes reference from global objects and saves it as jStorage
|
|
*
|
|
* @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
|
|
*/
|
|
noConflict: function(saveInGlobal) {
|
|
delete window.$.jStorage;
|
|
|
|
if (saveInGlobal) {
|
|
window.jStorage = this;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// Initialize jStorage
|
|
_init();
|
|
|
|
})();
|
|
"use strict";
|
|
|
|
/*\
|
|
|*|
|
|
|*| Base64 / binary data / UTF-8 strings utilities
|
|
|*|
|
|
|*| https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
|
|
|*|
|
|
\*/
|
|
|
|
/* Array of bytes to base64 string decoding */
|
|
|
|
function b64ToUint6 (nChr) {
|
|
|
|
return nChr > 64 && nChr < 91 ?
|
|
nChr - 65
|
|
: nChr > 96 && nChr < 123 ?
|
|
nChr - 71
|
|
: nChr > 47 && nChr < 58 ?
|
|
nChr + 4
|
|
: nChr === 43 ?
|
|
62
|
|
: nChr === 47 ?
|
|
63
|
|
:
|
|
0;
|
|
|
|
}
|
|
|
|
function base64DecToArr (sBase64, nBlocksSize) {
|
|
|
|
var
|
|
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
|
|
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);
|
|
|
|
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
|
|
nMod4 = nInIdx & 3;
|
|
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
|
|
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
|
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
|
|
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
|
|
}
|
|
nUint24 = 0;
|
|
|
|
}
|
|
}
|
|
|
|
return taBytes;
|
|
}
|
|
|
|
/* Base64 string to array encoding */
|
|
|
|
function uint6ToB64 (nUint6) {
|
|
|
|
return nUint6 < 26 ?
|
|
nUint6 + 65
|
|
: nUint6 < 52 ?
|
|
nUint6 + 71
|
|
: nUint6 < 62 ?
|
|
nUint6 - 4
|
|
: nUint6 === 62 ?
|
|
43
|
|
: nUint6 === 63 ?
|
|
47
|
|
:
|
|
65;
|
|
|
|
}
|
|
|
|
function base64EncArr (aBytes) {
|
|
|
|
var nMod3 = 2, sB64Enc = "";
|
|
|
|
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
|
|
nMod3 = nIdx % 3;
|
|
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
|
|
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
|
|
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
|
|
sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
|
|
nUint24 = 0;
|
|
}
|
|
}
|
|
|
|
return sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) + (nMod3 === 2 ? '' : nMod3 === 1 ? '=' : '==');
|
|
|
|
}
|
|
|
|
/* UTF-8 array to DOMString and vice versa */
|
|
|
|
function UTF8ArrToStr (aBytes) {
|
|
|
|
var sView = "";
|
|
|
|
for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) {
|
|
nPart = aBytes[nIdx];
|
|
sView += String.fromCharCode(
|
|
nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */
|
|
/* (nPart - 252 << 32) is not possible in ECMAScript! So...: */
|
|
(nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
|
|
: nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */
|
|
(nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
|
|
: nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */
|
|
(nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
|
|
: nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */
|
|
(nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
|
|
: nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */
|
|
(nPart - 192 << 6) + aBytes[++nIdx] - 128
|
|
: /* nPart < 127 ? */ /* one byte */
|
|
nPart
|
|
);
|
|
}
|
|
|
|
return sView;
|
|
|
|
}
|
|
|
|
function strToUTF8Arr (sDOMStr) {
|
|
|
|
var aBytes, nChr, nStrLen = sDOMStr.length, nArrLen = 0;
|
|
|
|
/* mapping... */
|
|
|
|
for (var nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
|
|
nChr = sDOMStr.charCodeAt(nMapIdx);
|
|
nArrLen += nChr < 0x80 ? 1 : nChr < 0x800 ? 2 : nChr < 0x10000 ? 3 : nChr < 0x200000 ? 4 : nChr < 0x4000000 ? 5 : 6;
|
|
}
|
|
|
|
aBytes = new Uint8Array(nArrLen);
|
|
|
|
/* transcription... */
|
|
|
|
for (var nIdx = 0, nChrIdx = 0; nIdx < nArrLen; nChrIdx++) {
|
|
nChr = sDOMStr.charCodeAt(nChrIdx);
|
|
if (nChr < 128) {
|
|
/* one byte */
|
|
aBytes[nIdx++] = nChr;
|
|
} else if (nChr < 0x800) {
|
|
/* two bytes */
|
|
aBytes[nIdx++] = 192 + (nChr >>> 6);
|
|
aBytes[nIdx++] = 128 + (nChr & 63);
|
|
} else if (nChr < 0x10000) {
|
|
/* three bytes */
|
|
aBytes[nIdx++] = 224 + (nChr >>> 12);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr & 63);
|
|
} else if (nChr < 0x200000) {
|
|
/* four bytes */
|
|
aBytes[nIdx++] = 240 + (nChr >>> 18);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr & 63);
|
|
} else if (nChr < 0x4000000) {
|
|
/* five bytes */
|
|
aBytes[nIdx++] = 248 + (nChr >>> 24);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 18 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr & 63);
|
|
} else /* if (nChr <= 0x7fffffff) */ {
|
|
/* six bytes */
|
|
aBytes[nIdx++] = 252 + /* (nChr >>> 32) is not possible in ECMAScript! So...: */ (nChr / 1073741824);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 24 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 18 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
|
|
aBytes[nIdx++] = 128 + (nChr & 63);
|
|
}
|
|
}
|
|
|
|
return aBytes;
|
|
|
|
} |