Adding ResourceLoader module "jquery.jStorage"
authorHoo man <hoo@online.de>
Fri, 17 Aug 2012 18:36:27 +0000 (20:36 +0200)
committerTimo Tijhof <ttijhof@wikimedia.org>
Mon, 27 Aug 2012 17:48:31 +0000 (19:48 +0200)
Adding jStorage from http://www.jstorage.info/ to MediaWiki
It's MIT-style licensed and useful for local caching of data.

Adding to .jshintignore since it is a third party library.

Change-Id: I2343744304191d5846cf346e4ac6ca083a6414b3

.jshintignore
RELEASE-NOTES-1.20
resources/Resources.php
resources/jquery/jquery.jStorage.js [new file with mode: 0644]

index 9534f97..45f2da7 100644 (file)
@@ -8,6 +8,7 @@ resources/jquery/jquery.form.js
 resources/jquery/jquery.hoverIntent.js
 resources/jquery/jquery.js
 resources/jquery/jquery.json.js
+resources/jquery/jquery.jStorage.js
 resources/jquery/jquery.mockjax.js
 resources/jquery/jquery.qunit.js
 resources/jquery/jquery.validate.js
index 92f050e..2ab67ed 100644 (file)
@@ -133,6 +133,7 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * (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".
 
 === Bug fixes in 1.20 ===
 * (bug 30245) Use the correct way to construct a log page title.
index 5a24355..82ab718 100644 (file)
@@ -204,6 +204,10 @@ return array(
                'scripts' => 'resources/jquery/jquery.spinner.js',
                'styles' => 'resources/jquery/jquery.spinner.css',
        ),
+       'jquery.jStorage' => array(
+               'scripts' => 'resources/jquery/jquery.jStorage.js',
+               'dependencies' => 'jquery.json',
+       ),
        'jquery.suggestions' => array(
                'scripts' => 'resources/jquery/jquery.suggestions.js',
                'styles' => 'resources/jquery/jquery.suggestions.css',
diff --git a/resources/jquery/jquery.jStorage.js b/resources/jquery/jquery.jStorage.js
new file mode 100644 (file)
index 0000000..95959cf
--- /dev/null
@@ -0,0 +1,532 @@
+/*
+ * ----------------------------- 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 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
+ * 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.
+ */
+
+/**
+ * $.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
+ *
+ * <value> can be any JSON-able value, including objects and arrays.
+ *
+ **/
+
+(function($){
+    if(!$ || !($.toJSON || Object.toJSON || window.JSON)){
+        throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!");
+    }
+
+    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,
+
+        /* 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,
+
+        /* 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.
+     * @returns undefined
+     */
+    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";
+                }
+            } 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";
+                }
+            } 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);
+
+                _storage_elm.load("jStorage");
+                var data = "{}";
+                try{
+                    data = _storage_elm.getAttribute("jStorage");
+                }catch(E5){}
+                _storage_service.jStorage = data;
+                _backend = "userDataBehavior";
+            }else{
+                _storage_elm = null;
+                return;
+            }
+        }
+
+        _load_storage();
+
+        // remove dead keys
+        _handleTTL();
+    }
+
+    /**
+     * Loads the data from the storage based on the supported mechanism
+     * @returns undefined
+     */
+    function _load_storage(){
+        /* if jStorage string is retrieved, then decode it */
+        if(_storage_service.jStorage){
+            try{
+                _storage = json_decode(String(_storage_service.jStorage));
+            }catch(E6){_storage_service.jStorage = "{}";}
+        }else{
+            _storage_service.jStorage = "{}";
+        }
+        _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
+    }
+
+    /**
+     * This functions provides the "save" mechanism to store the jStorage object
+     * @returns undefined
+     */
+    function _save(){
+        try{
+            _storage_service.jStorage = json_encode(_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
+     */
+    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, nextExpire = Infinity, changed = false;
+
+        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;
+        for(i in TTL){
+            if(TTL.hasOwnProperty(i)){
+                if(TTL[i] <= curtime){
+                    delete TTL[i];
+                    delete _storage[i];
+                    changed = true;
+                }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();
+        }
+    }
+
+    ////////////////////////// PUBLIC INTERFACE /////////////////////////
+
+    $.jStorage = {
+        /* Version number */
+        version: "0.1.7.0",
+
+        /**
+         * 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
+         * @returns the used value
+         */
+        set: function(key, value, options){
+            _checkKey(key);
+
+            options = options || {};
+
+            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"){
+                // clone the object before saving to _storage tree
+                value = json_decode(json_encode(value));
+            }
+            _storage[key] = value;
+
+            if(!isNaN(options.TTL)){
+                this.setTTL(key, options.TTL);
+                // also handles saving
+            }else{
+                _save();
+            }
+            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.
+         * @returns 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.
+         * @returns 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" &&
+                  key in _storage.__jstorage_meta.TTL){
+                    delete _storage.__jstorage_meta.TTL[key];
+                }
+                _save();
+                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
+         * @returns 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){
+                    _storage.__jstorage_meta = {};
+                }
+                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();
+                return true;
+            }
+            return false;
+        },
+
+        /**
+         * Deletes everything in cache.
+         *
+         * @return true
+         */
+        flush: function(){
+            _storage = {};
+            _save();
+            return true;
+        },
+
+        /**
+         * Returns a read-only copy of _storage
+         *
+         * @returns Object
+        */
+        storageObj: function(){
+            function F() {}
+            F.prototype = _storage;
+            return new F();
+        },
+
+        /**
+         * Returns an index of all used keys as an array
+         * ['key1', 'key2',..'keyN']
+         *
+         * @returns Array
+        */
+        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?
+         *
+         * @returns Number
+         */
+        storageSize: function(){
+            return _storage_size;
+        },
+
+        /**
+         * Which backend is currently in use?
+         *
+         * @returns String
+         */
+        currentBackend: function(){
+            return _backend;
+        },
+
+        /**
+         * Test if storage is available
+         *
+         * @returns Boolean
+         */
+        storageAvailable: function(){
+            return !!_backend;
+        },
+
+        /**
+         * Reloads the data from browser storage
+         *
+         * @returns undefined
+         */
+        reInit: function(){
+            var new_storage_elm, data;
+            if(_storage_elm && _storage_elm.addBehavior){
+                new_storage_elm = document.createElement('link');
+
+                _storage_elm.parentNode.replaceChild(new_storage_elm, _storage_elm);
+                _storage_elm = new_storage_elm;
+
+                /* 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);
+
+                _storage_elm.load("jStorage");
+                data = "{}";
+                try{
+                    data = _storage_elm.getAttribute("jStorage");
+                }catch(E5){}
+                _storage_service.jStorage = data;
+                _backend = "userDataBehavior";
+            }
+
+            _load_storage();
+        }
+    };
+
+    // Initialize jStorage
+    _init();
+
+})(window.$ || window.jQuery);