/** * 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, /** * This property is set to `true` when the instance's `destroy` method is called. * @property {Boolean} destroyed * @readonly */ destroyed: false, /** * 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. * @cfg {Boolean} [lazyKeys=true] */ lazyKeys: true, /** * 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-"`. * @cfg {String} [prefix] */ prefix: '', /** * 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. * @cfg {Boolean} [session=false] */ session: false, /** * The array of all key names. This will be `null` if the keys need to be redetermined * by the `getKeys` method. * @property {String[]} _keys * @private * @readonly */ _keys: null, /** * The Storage instance used to store items. This is based on the `session` config. * @property _store * @private * @readonly */ _store: null, /** * The number of users that have requested this instance using the `get` method and * have not yet called `release`. * @property {Number} _users * @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 "extraordinaly 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); } }}, function () { var LocalStorage = this; if ('localStorage' in window) { return; } if (!Ext.isIE) { LocalStorage.supported = false; //<debug> LocalStorage.prototype.init = function () { Ext.raise("Local storage is not supported on this browser"); }; //</debug> return; } // The legacy IE polyfill has to store values as attributes which have strict rules on // valid names. Rather than handle that per-key, we just JSON enocde an object and use // just the "xdata" attribute. // LocalStorage.override({ /** * The parsed data object. This is JSON encoded and saved in storage as the `xdata` * attribute. * @property {Object} data * @private * @readonly */ data: null, /** * The IE `userData` enabled element. This property is used to support legacy IE * mode. * @property {HTMLElement} el * @private * @readonly */ /** * The number of milliseconds to delay writing changes to the underlying store. * This applies only to legacy IE mode and helps batch multiple writes into one * flush to storage. * @cfg {Number} [flushDelay=1] */ flushDelay: 1, init: function () { var me = this, data = me.data, el; me.el = el = document.createElement('div'); el.id = (me.id || (me.id = 'extjs-localstore')); el.addBehavior('#default#userdata'); // Add to <head> to ensure the div is invisible Ext.getHead().dom.appendChild(el); el.load(me.id); data = el.getAttribute('xdata'); me.data = data = (data ? Ext.decode(data) : {}); me._flushFn = function () { me._timer = null; me.save(0); }; }, destroy: function () { var me = this, el = me.el; if (el) { // If we have a save pending, flush that now: if (me._timer) { me.save(); } el.parentNode.removeChild(el); me.data = me.el = null; me.callParent(); } }, getKeys: function () { var me = this, keys = me._keys; if (!keys) { me._keys = keys = Ext.Object.getKeys(me.data); } return keys; }, /** * This method ensures the content of the store is saved to the underlying storage. * This applies only to legacy IE. This is not normally called by user code but can * be called to ensure storage is saved. * @param {Number} [delay=0] */ save: function (delay) { var me = this; if (!delay) { me._timer = Ext.undefer(me._timer); me.el.setAttribute('xdata', Ext.encode(me.data)); me.el.save(me.id); } else if (!me._timer) { me._timer = Ext.defer(me._flushFn, delay); } }, clear: function () { var me = this; me.data = {}; me._keys = null; me.save(me.flushDelay); }, getItem: function (key) { var data = this.data; return (key in data) ? data[key] : null; }, removeItem: function (key) { var me = this, keys = me._keys, data = me.data; if (key in data) { delete data[key]; if (keys) { if (me.lazyKeys) { me._keys = null; } else { Ext.Array.remove(keys, key); } } me.save(me.flushDelay); } }, setItem: function (key, value) { var me = this, data = me.data, keys = me._keys; if (keys && !(key in data)) { keys.push(key); } data[key] = value; me.save(me.flushDelay); } });});