Hi This would be a welcome feature addition, but why not store the treeview state in the configuration database? I'd prefer not to add another storage mechanism without a very compelling reason.
If you need info on how it works, please feel free to ask questions. Thanks. On Tue, Jul 18, 2017 at 9:42 AM, Versus Void <chaoskee...@mail.ru> wrote: > --- > web/pgadmin/browser/__init__.py | 13 +- > .../browser/static/vendor/jStorage/jstorage.js | 996 > +++++++++++++++++++++ > .../browser/static/vendor/jStorage/jstorage.min.js | 16 + > .../browser/templates/browser/js/browser.js | 6 +- > 4 files changed, 1029 insertions(+), 2 deletions(-) > create mode 100644 web/pgadmin/browser/static/vendor/jStorage/jstorage.js > create mode 100644 web/pgadmin/browser/static/ > vendor/jStorage/jstorage.min.js > > diff --git a/web/pgadmin/browser/__init__.py > b/web/pgadmin/browser/__init__.py > index 77e052f0..ecf3afba 100644 > --- a/web/pgadmin/browser/__init__.py > +++ b/web/pgadmin/browser/__init__.py > @@ -100,13 +100,24 @@ class BrowserModule(PgAdminModule): > 'preloaded': True > }) > scripts.append({ > + 'name': 'jStorage', > + 'path': url_for( > + 'browser.static', > + filename='vendor/jStorage/jstorage' if > + current_app.debug else 'vendor/jStorage/jstorage.min' > + ), > + 'deps': ['jquery'], > + 'exports': 'jStorage', > + 'preloaded': True > + }) > + scripts.append({ > 'name': 'jquery.acitree', > 'path': url_for( > 'browser.static', > filename='vendor/aciTree/jquery.aciTree' if > current_app.debug else 'vendor/aciTree/jquery. > aciTree.min' > ), > - 'deps': ['jquery', 'jquery.aciplugin'], > + 'deps': ['jquery', 'jquery.aciplugin', 'jStorage'], > 'exports': 'aciPluginClass.plugins.aciTree', > 'preloaded': True > }) > diff --git a/web/pgadmin/browser/static/vendor/jStorage/jstorage.js > b/web/pgadmin/browser/static/vendor/jStorage/jstorage.js > new file mode 100644 > index 00000000..1ac8fccc > --- /dev/null > +++ b/web/pgadmin/browser/static/vendor/jStorage/jstorage.js > @@ -0,0 +1,996 @@ > +/* > + * ----------------------------- 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.rein...@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.co...@gmail.com'>Gary Court</a> > + * @see http://github.com/garycourt/murmurhash-js > + * @author <a href='mailto:aappl...@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(); > + > +})(); > \ No newline at end of file > diff --git a/web/pgadmin/browser/static/vendor/jStorage/jstorage.min.js > b/web/pgadmin/browser/static/vendor/jStorage/jstorage.min.js > new file mode 100644 > index 00000000..ecde658e > --- /dev/null > +++ b/web/pgadmin/browser/static/vendor/jStorage/jstorage.min.js > @@ -0,0 +1,16 @@ > +(function(){function C(){var a="{}";if("userDataBehavior"== > f){g.load("jStorage");try{a=g.getAttribute("jStorage")}catch(b){}try{r=g. > getAttribute("jStorage_update")}catch(c){}h.jStorage=a}D();x();E()}function > u(){var a;clearTimeout(F);F=setTimeout(function(){if("localStorage"==f||" > globalStorage"==f)a=h.jStorage_update;else if("userDataBehavior"==f){g. > load("jStorage");try{a=g.getAttribute("jStorage_update" > )}catch(b){}}if(a&&a!=r){r=a;var l=p.parse(p.stringify(c.__ > jstorage_meta.CRC32)),k;C();k=p.parse(p.stringify(c.__ > jstorage_meta.CRC32)); > +var d,n=[],e=[];for(d in l)l.hasOwnProperty(d)&&(k[d]? > l[d]!=k[d]&&"2."==String(l[d]).substr(0,2)&&n.push(d):e.push(d));for(d in > k)k.hasOwnProperty(d)&&(l[d]||n.push(d));s(n,"updated");s(e,"deleted")}},25)}function > s(a,b){a=[].concat(a||[]);var c,k,d,n;if("flushed"==b){a=[];for(c in > m)m.hasOwnProperty(c)&&a.push(c);b="deleted"}c=0;for(d=a. > length;c<d;c++){if(m[a[c]])for(k=0,n=m[a[c]].length;k<n; > k++)m[a[c]][k](a[c],b);if(m["*"])for(k=0,n=m["*"].length;k< > n;k++)m["*"][k](a[c],b)}}function v(){var a=(+new Date).toString(); > +if("localStorage"==f||"globalStorage"==f)try{h. > jStorage_update=a}catch(b){f=!1}else"userDataBehavior"==f&&( > g.setAttribute("jStorage_update",a),g.save("jStorage"));u()}function > D(){if(h.jStorage)try{c=p.parse(String(h.jStorage))}catch(a){h.jStorage="{}"}else > h.jStorage="{}";z=h.jStorage?String(h.jStorage).length:0;c. > __jstorage_meta||(c.__jstorage_meta={});c.__jstorage_meta.CRC32||(c.__ > jstorage_meta.CRC32={})}function w(){if(c.__jstorage_meta.PubSub){for(var > a=+new Date-2E3,b=0,l=c.__jstorage_meta.PubSub.length;b< > +l;b++)if(c.__jstorage_meta.PubSub[b][0]<=a){c.__jstorage_ > meta.PubSub.splice(b,c.__jstorage_meta.PubSub.length-b) > ;break}c.__jstorage_meta.PubSub.length||delete > c.__jstorage_meta.PubSub}try{h.jStorage=p.stringify(c),g&&( > g.setAttribute("jStorage",h.jStorage),g.save("jStorage")), > z=h.jStorage?String(h.jStorage).length:0}catch(k){}}function > q(a){if("string"!=typeof a&&"number"!=typeof a)throw new TypeError("Key > name must be string or numeric");if("__jstorage_meta"==a)throw new > TypeError("Reserved key name"); > +return!0}function x(){var a,b,l,k,d=Infinity,n=!1,e=[]; > clearTimeout(G);if(c.__jstorage_meta&&"object"==typeof > c.__jstorage_meta.TTL){a=+new Date;l=c.__jstorage_meta.TTL; > k=c.__jstorage_meta.CRC32;for(b in l)l.hasOwnProperty(b)&&(l[b]<=a?(delete > l[b],delete k[b],delete c[b],n=!0,e.push(b)):l[b]<d&&( > d=l[b]));Infinity!=d&&(G=setTimeout(x,Math.min(d-a, > 2147483647)));n&&(w(),v(),s(e,"deleted"))}}function E(){var > a;if(c.__jstorage_meta.PubSub){var b,l=A,k=[];for(a=c.__jstorage_ > meta.PubSub.length-1;0<=a;a--)b= > +c.__jstorage_meta.PubSub[a],b[0]>A&&(l=b[0],k.unshift(b)); > for(a=k.length-1;0<=a;a--){b=k[a][1];var d=k[a][2];if(t[b])for(var > n=0,e=t[b].length;n<e;n++)try{t[b][n](b,p.parse(p.stringify(d)))}catch(g){}}A=l}}var > y=window.jQuery||window.$||(window.$={}),p={parse:window. > JSON&&(window.JSON.parse||window.JSON.decode)||String. > prototype.evalJSON&&function(a){return String(a).evalJSON()}||y. > parseJSON||y.evalJSON,stringify:Object.toJSON||window.JSON&&(window.JSON. > stringify||window.JSON.encode)||y.toJSON};if("function"!== > +typeof p.parse||"function"!==typeof p.stringify)throw Error("No JSON > support found, include //cdnjs.cloudflare.com/ajax/ > libs/json2/20110223/json2.js to page");var c={__jstorage_meta:{CRC32:{}}} > ,h={jStorage:"{}"},g=null,z=0,f=!1,m={},F=!1,r=0,t={},A=+new > Date,G,B={isXML:function(a){return(a=(a?a.ownerDocument|| > a:0).documentElement)?"HTML"!==a.nodeName:!1},encode: > function(a){if(!this.isXML(a))return!1;try{return(new XMLSerializer). > serializeToString(a)}catch(b){try{return a.xml}catch(c){}}return!1}, > +decode:function(a){var b="DOMParser"in window&&(new > DOMParser).parseFromString||window.ActiveXObject&&function(a){var b=new > ActiveXObject("Microsoft.XMLDOM");b.async="false";b.loadXML(a);return > b};if(!b)return!1;a=b.call("DOMParser"in window&&new > DOMParser||window,a,"text/xml");return this.isXML(a)?a:!1}};y. > jStorage={version:"0.4.12",set:function(a,b,l){q(a);l=l||{};if("undefined"==typeof > b)return this.deleteKey(a),b;if(B.isXML(b))b={_is_xml:!0,xml:B. > encode(b)};else{if("function"==typeof b)return; > +b&&"object"==typeof b&&(b=p.parse(p.stringify(b)))}c[a]=b;for(var > k=c.__jstorage_meta.CRC32,d=p.stringify(b),g=d.length,e= > 2538058380^g,h=0,f;4<=g;)f=d.charCodeAt(h)&255|(d. > charCodeAt(++h)&255)<<8|(d.charCodeAt(++h)&255)<<16|(d. > charCodeAt(++h)&255)<<24,f=1540483477*(f&65535)+(( > 1540483477*(f>>>16)&65535)<<16),f^=f>>>24,f=1540483477*(f& > 65535)+((1540483477*(f>>>16)&65535)<<16),e=1540483477*(e& > 65535)+((1540483477*(e>>>16)&65535)<<16)^f,g-=4,++h;switch(g){case > 3:e^=(d.charCodeAt(h+2)&255)<<16;case 2:e^= > +(d.charCodeAt(h+1)&255)<<8;case 1:e^=d.charCodeAt(h)&255,e= > 1540483477*(e&65535)+((1540483477*(e>>>16)&65535)<< > 16)}e^=e>>>13;e=1540483477*(e&65535)+((1540483477*(e>>>16)& > 65535)<<16);k[a]="2."+((e^e>>>15)>>>0);this.setTTL(a,l.TTL||0);s(a,"updated");return > b},get:function(a,b){q(a);return a in c?c[a]&&"object"==typeof > c[a]&&c[a]._is_xml?B.decode(c[a].xml):c[a]:"undefined"==typeof > b?null:b},deleteKey:function(a){q(a);return a in c?(delete > c[a],"object"==typeof c.__jstorage_meta.TTL&&a in c.__jstorage_meta.TTL&& > +delete c.__jstorage_meta.TTL[a],delete c.__jstorage_meta.CRC32[a],w() > ,v(),s(a,"deleted"),!0):!1},setTTL:function(a,b){var l=+new > Date;q(a);b=Number(b)||0;return a in c?(c.__jstorage_meta.TTL||(c._ > _jstorage_meta.TTL={}),0<b?c.__jstorage_meta.TTL[a]=l+b:delete > c.__jstorage_meta.TTL[a],w(),x(),v(),!0):!1},getTTL:function(a){var > b=+new Date;q(a);return a in c&&c.__jstorage_meta.TTL&&c.__ > jstorage_meta.TTL[a]?(a=c.__jstorage_meta.TTL[a]-b)||0:0}, > flush:function(){c={__jstorage_meta:{CRC32:{}}};w();v();s(null, > +"flushed");return!0},storageObj:function(){function > a(){}a.prototype=c;return new a},index:function(){var a=[],b;for(b in > c)c.hasOwnProperty(b)&&"__jstorage_meta"!=b&&a.push(b);return > a},storageSize:function(){return z},currentBackend:function(){return > f},storageAvailable:function(){return!!f},listenKeyChange: > function(a,b){q(a);m[a]||(m[a]=[]);m[a].push(b)}, > stopListening:function(a,b){q(a);if(m[a])if(b)for(var > c=m[a].length-1;0<=c;c--)m[a][c]==b&&m[a].splice(c,1);else delete > m[a]},subscribe:function(a, > +b){a=(a||"").toString();if(!a)throw new TypeError("Channel not > defined");t[a]||(t[a]=[]);t[a].push(b)},publish:function(a, > b){a=(a||"").toString();if(!a)throw new TypeError("Channel not > defined");c.__jstorage_meta||(c.__jstorage_meta={});c.__ > jstorage_meta.PubSub||(c.__jstorage_meta.PubSub=[]);c.__ > jstorage_meta.PubSub.unshift([+new Date,a,b]);w();v()},reInit: > function(){C()},noConflict:function(a){delete > window.$.jStorage;a&&(window.jStorage=this);return this}};(function(){var > a=!1;if("localStorage"in > +window)try{window.localStorage.setItem("_tmptest","tmpval"),a=!0, > window.localStorage.removeItem("_tmptest")}catch(b){}if(a)try{window. > localStorage&&(h=window.localStorage,f="localStorage", > r=h.jStorage_update)}catch(c){}else if("globalStorage"in > window)try{window.globalStorage&&(h="localhost"==window.location.hostname? > window.globalStorage["localhost.localdomain"]:window.globalStorage[window. > location.hostname],f="globalStorage",r=h.jStorage_update)}catch(k){}else > if(g=document.createElement("link"), > +g.addBehavior){g.style.behavior="url(#default#userData)";document. > getElementsByTagName("head")[0].appendChild(g);try{g.load(" > jStorage")}catch(d){g.setAttribute("jStorage","{}"), > g.save("jStorage"),g.load("jStorage")}a="{}";try{a=g. > getAttribute("jStorage")}catch(m){}try{r=g.getAttribute("jStorage_update" > )}catch(e){}h.jStorage=a;f="userDataBehavior"}else{g=null; > return}D();x();"localStorage"==f||"globalStorage"==f?"addEventListener"in > window?window.addEventListener("storage",u,!1):document.attachEvent(" > onstorage", > +u):"userDataBehavior"==f&&setInterval(u,1E3);E();"addEventListener"in > window&&window.addEventListener("pageshow",function(a){a.persisted&&u()}, > !1)})()})(); > \ No newline at end of file > diff --git a/web/pgadmin/browser/templates/browser/js/browser.js > b/web/pgadmin/browser/templates/browser/js/browser.js > index 6b260dc8..a38ed764 100644 > --- a/web/pgadmin/browser/templates/browser/js/browser.js > +++ b/web/pgadmin/browser/templates/browser/js/browser.js > @@ -68,6 +68,9 @@ define( > if (n) > settings.url = n.generate_url(item, 'children', d, true); > } > + if (item != null && settings.url == url_for('browser.nodes')) { > + settings.url = null; > + } > }, > loaderDelay: 100, > show: { > @@ -78,7 +81,8 @@ define( > }, > view: { > duration: 75 > - } > + }, > + persist: 'nodes' > }); > > b.tree = $('#tree').aciTree('api'); > -- > 2.13.3 > > > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company