From 260c1f48ed724d05cd8016c3e4520403cb902081 Mon Sep 17 00:00:00 2001 From: Hoo man Date: Fri, 17 Aug 2012 20:36:27 +0200 Subject: [PATCH] Adding ResourceLoader module "jquery.jStorage" 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 | 1 + RELEASE-NOTES-1.20 | 1 + resources/Resources.php | 4 + resources/jquery/jquery.jStorage.js | 532 ++++++++++++++++++++++++++++ 4 files changed, 538 insertions(+) create mode 100644 resources/jquery/jquery.jStorage.js diff --git a/.jshintignore b/.jshintignore index 9534f97715..45f2da7f1f 100644 --- a/.jshintignore +++ b/.jshintignore @@ -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 diff --git a/RELEASE-NOTES-1.20 b/RELEASE-NOTES-1.20 index 92f050e68e..2ab67ed671 100644 --- a/RELEASE-NOTES-1.20 +++ b/RELEASE-NOTES-1.20 @@ -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. diff --git a/resources/Resources.php b/resources/Resources.php index 5a24355b0d..82ab718b1b 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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 index 0000000000..95959cf7a4 --- /dev/null +++ b/resources/jquery/jquery.jStorage.js @@ -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 + * + * 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 + */ + 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); -- 2.20.1