/**
* This class provides a common API to LocalStorage with backwards compatibility for IE.
*
* The primary aspects of this API match the HTML5 standard API except that this class
* provides a scoping mechanism to isolate property values by instance. This scope is
* determined from the `id` property. Further, this class does not expose the number of
* keys in the store as a `length` property as this cannot be maintained reliably without
* undue cost. Instead there is a `getKeys` method that returns the cached array of keys
* which is lazily populated on first call.
*
* For example:
*
* var store = new Ext.util.LocalStorage({
* id: 'foo'
* });
*
* store.setItem('bar', 'stuff');
*
* // Equivalent to:
* window.localStorage.setItem('foo-bar', 'stuff');
*
* In all cases, the `id` property is only used by the underlying storage and should not
* be needed in item access calls or appear when enumerating keys.
*
* To continue with the previous example:
*
* var keys = store.getKeys();
* console.log(keys.length); // logs 1
* console.log(store.key(0)); // logs "bar"
*
* ## Sharing Instances
*
* The management of the underlying storage can be broken if multiple instances of this
* class are created with the same `id` simultaneously. To avoid creating multiple instances
* with the same `id`, use the `get` method and it will lazily create and share a single
* instance. When you are done with the shared instance, call `release`.
*
* var storage = Ext.util.LocalStorage.get('id');
*
* ...
*
* storage.release(); // do not call `destroy` as others may be using this object
*
* **IMPORTANT:** Do not mix direction instantiation and `get` with the same `id`.
*
* ## Legacy IE
*
* Older IE browsers (specifically IE7 and below) do not support `localStorage` so this
* class provides equivalent support using the IE proprietary persistence mechanism: the
* [`userData` behavior](http://msdn.microsoft.com/en-us/library/ms531424(VS.85).aspx). In
* this mode, the `id` serves as name passed to the `load` and `save` methods and as the
* suffix on the DOM element added to the `head`.
*
* In this mode, writes to the underlying storage are buffered and delayed for performance
* reasons. This can be managed using the `flushDelay` config or by directly calling the
* `save` method.
*
* @since 4.2.2
*/
Ext.define('Ext.util.LocalStorage', {
/**
* The unique identifier for this store. This config is required to scope this storage
* distinctly from others. Ultimately, this is used to set a prefix on all keys.
* @cfg {String} id
*/
id: null,
/**
* @property {Boolean} destroyed
* This property is set to `true` when the instance's `destroy` method is called.
* @readonly
*/
destroyed: false,
/**
* @cfg {Boolean} lazyKeys
* Determines if the keys collection is continuously maintained by this object. By
* default the keys array is lazily fetched from the underlying store and when keys
* are removed, the array is discarded. This heuristic tends to be safer than doing
* the linear removal and array rippling to remove keys from the array on each call
* to `removeItem`. If the cost of scanning `localStorage` for keys is high enough
* and if the keys are frequently needed, then this flag can be set to `false` to
* instruct this class to maintain the keys array once it has been determined.
*/
lazyKeys: true,
/**
* @cfg {String} prefix
* The prefix to apply to all `localStorage` keys manages by this instance. This does
* not apply to the legacy IE mechanism but only to the HTML5 `localStorage` keys. If
* not provided, the `id` property initializes this value with `"id-"`.
*/
prefix: '',
/**
* @cfg {Boolean} session
* Specify this as `true` to use `sessionStorage` instead of the default `localStoreage`.
* This option is not supported in legacy IE browsers (IE 6 and 7) and is ignored.
*/
session: false,
/**
* @property {String[]} _keys
* The array of all key names. This will be `null` if the keys need to be redetermined
* by the `getKeys` method.
* @private
* @readonly
*/
_keys: null,
/**
* @property _store
* The Storage instance used to store items. This is based on the `session` config.
* @private
* @readonly
*/
_store: null,
/**
* @property {Number} _users
* The number of users that have requested this instance using the `get` method and
* have not yet called `release`.
* @private
* @readonly
*/
_users: 0,
statics: {
cache: {},
/**
* Returns a shared instance of the desired local store given its `id`. When you
* are finished with the returned object call the `release` method:
*
* var store = Ext.util.LocalStorage.get('foo');
*
* // .. use store
*
* store.release();
*
* **NOTE:** Do not mix this call with direct instantiation of the same `id`.
* @param {String/Object} id The `id` of the desired instance or a config object
* with an `id` property at a minimum.
* @return {Ext.util.LocalStorage} The desired instance, created if needed.
*/
get: function(id) {
var me = this,
cache = me.cache,
config = {
_users: 1 // allow constructor to recognize us as the caller
},
instance;
if (Ext.isString(id)) {
config.id = id;
}
else {
Ext.apply(config, id);
}
if (!(instance = cache[config.id])) {
instance = new me(config);
}
else {
//<debug>
if (instance === true) {
Ext.raise('Creating a shared instance of private local store "' +
me.id + '".');
}
//</debug>
++instance._users;
}
return instance;
},
/**
* This will be `true` if some form of local storage is supported or `false` if not.
* @property {Boolean} supported
* @readonly
*/
supported: true
},
constructor: function(config) {
var me = this;
Ext.apply(me, config);
//<debug>
if (!me.hasOwnProperty('id')) {
Ext.raise("No id was provided to the local store.");
}
//</debug>
if (me._users) {
// When we are created by get() for shared use, the _users property is set to
// 1... so this means we are being created for shared use: add this instance
// to the cache.
Ext.util.LocalStorage.cache[me.id] = me;
}
//<debug>
else {
// else we are being created directly so check that this id is not also in the
// cache ... that would be "extraordinarily bad".
if (Ext.util.LocalStorage.cache[me.id]) {
Ext.raise('Cannot create duplicate instance of local store "' +
me.id + '". Use Ext.util.LocalStorage.get() to share instances.');
}
// We put true in the cache to be detectable by get() for diagnostic reasons
// but no "this" to avoid leaking the store:
Ext.util.LocalStorage.cache[me.id] = true;
}
//</debug>
me.init();
},
/**
* Initializes this instance.
* @private
*/
init: function() {
var me = this,
id = me.id;
if (!me.prefix && id) {
me.prefix = id + '-';
}
me._store = (me.session ? window.sessionStorage : window.localStorage);
},
/**
* Destroys this instance and for legacy IE, ensures data is flushed to persistent
* storage. This method should not be called directly on instances returned by the
* `get` method. Call `release` instead for such instances.
*
* *NOTE:* For non-legacy IE browsers, there is no harm in failing to call this
* method. In legacy IE, however, failing to call this method can result in memory
* leaks.
*/
destroy: function() {
var me = this;
//<debug>
if (me._users) {
Ext.log.warn('LocalStorage(id=' + me.id + ') destroyed while in use');
}
//</debug>
delete Ext.util.LocalStorage.cache[me.id];
me._store = me._keys = null;
me.callParent();
},
/**
* Returns the keys for this storage.
* @return {String[]} The keys for this storage. This array should be considered as
* readonly.
*/
getKeys: function() {
var me = this,
store = me._store,
prefix = me.prefix,
keys = me._keys,
n = prefix.length,
i, key;
if (!keys) {
me._keys = keys = [];
for (i = store.length; i--;) {
key = store.key(i);
if (key.length > n) {
if (prefix === key.substring(0, n)) {
keys.push(key.substring(n));
}
}
}
}
return keys;
},
/**
* Call this method when finished with an instance returned by `get` instead of calling
* `destroy`. When the last shared use of this instance calls `release`, the `destroy`
* method is called automatically.
*
* *NOTE:* Failing to call this method will result in memory leaks.
*/
release: function() {
if (! --this._users) {
this.destroy();
}
},
/**
* @method
* @static
* @private
*/
save: Ext.emptyFn,
/**
* Removes all of the keys of this storage.
* **NOTE:** This method conforms to the standard HTML5 Storage interface.
*/
clear: function() {
var me = this,
store = me._store,
prefix = me.prefix,
keys = me._keys || me.getKeys(),
i;
for (i = keys.length; i--;) {
store.removeItem(prefix + keys[i]);
}
keys.length = 0;
},
/**
* Returns the specified key given its `index`. These keys have the scoping prefix
* removed so they match what was passed to `setItem`.
* **NOTE:** This method conforms to the standard HTML5 Storage interface.
* @param {Number} index The index of the desired key.
* @return {String} The key.
*/
key: function(index) {
var keys = this._keys || this.getKeys();
return (0 <= index && index < keys.length) ? keys[index] : null;
},
/**
* Returns the value associated with the given `key`.
* **NOTE:** This method conforms to the standard HTML5 Storage interface.
* @param {String} key The key.
* @return {String} The value associated with the given `key`.
*/
getItem: function(key) {
var k = this.prefix + key;
return this._store.getItem(k);
},
/**
* Removes the value associated with the given `key`.
* **NOTE:** This method conforms to the standard HTML5 Storage interface.
* @param {String} key The key.
*/
removeItem: function(key) {
var me = this,
k = me.prefix + key,
store = me._store,
keys = me._keys,
length = store.length;
store.removeItem(k);
if (keys && length !== store.length) {
if (me.lazyKeys) {
me._keys = null;
}
else {
Ext.Array.remove(keys, key);
}
}
},
/**
* Sets the value associated with the given `key`.
* **NOTE:** This method conforms to the standard HTML5 Storage interface.
* @param {String} key The key.
* @param {String} value The new associated value for `key`.
*/
setItem: function(key, value) {
var me = this,
k = me.prefix + key,
store = me._store,
length = store.length,
keys = me._keys;
store.setItem(k, value);
if (keys && length !== store.length) {
// Good news here - maintaining the keys collection is easy in this case.
keys.push(key);
}
}
});