/** * @author Ed Spencer * * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage} proxy. It uses the * new HTML5 key/value client-side storage objects to save {@link Ext.data.Model model instances} for offline use. * @private */ Ext.define('Ext.data.proxy.WebStorage', { extend: 'Ext.data.proxy.Client', alternateClassName: 'Ext.data.WebStorageProxy', requires: 'Ext.Date', config: { /** * @cfg {String} id * The unique ID used as the key in which all record data are stored in the local storage object. */ id: undefined, // WebStorage proxies dont use readers and writers /** * @cfg * @hide */ reader: null, /** * @cfg * @hide */ writer: null, /** * @cfg {Boolean} enablePagingParams This can be set to true if you want the webstorage proxy to comply * to the paging params set on the store. */ enablePagingParams: false, defaultDateFormat: 'Y-m-d H:i:s.u' }, /** * Creates the proxy, throws an error if local storage is not supported in the current browser. * @param {Object} config (optional) Config object. */ constructor: function(config) { this.callParent(arguments); /** * @property {Object} cache * Cached map of records already retrieved by this Proxy. Ensures that the same instance is always retrieved. */ this.cache = {}; //<debug> if (this.getStorageObject() === undefined) { Ext.Logger.error("Local Storage is not supported in this browser, please use another type of data proxy"); } //</debug> }, updateModel: function(model) { if (!this.getId()) { this.setId(model.modelName); } this.callParent(arguments); }, //inherit docs create: function(operation, callback, scope) { var records = operation.getRecords(), length = records.length, ids = this.getIds(), id, record, i; operation.setStarted(); for (i = 0; i < length; i++) { record = records[i]; // <debug> if (!this.getModel().getIdentifier().isUnique) { Ext.Logger.warn('Your identifier generation strategy for the model does not ensure unique id\'s. Please use the UUID strategy, or implement your own identifier strategy with the flag isUnique.'); } // </debug> id = record.getId(); this.setRecord(record); ids.push(id); } this.setIds(ids); operation.setCompleted(); operation.setSuccessful(); if (typeof callback == 'function') { callback.call(scope || this, operation); } }, //inherit docs read: function(operation, callback, scope) { var records = [], ids = this.getIds(), model = this.getModel(), idProperty = model.getIdProperty(), params = operation.getParams() || {}, sorters = operation.getSorters(), filters = operation.getFilters(), start = operation.getStart(), limit = operation.getLimit(), length = ids.length, i, record, collection; //read a single record if (params[idProperty] !== undefined) { record = this.getRecord(params[idProperty]); if (record) { records.push(record); operation.setSuccessful(); } } else { for (i = 0; i < length; i++) { record = this.getRecord(ids[i]); if (record) { records.push(record); } } collection = Ext.create('Ext.util.Collection'); // First we comply to filters if (filters && filters.length) { collection.setFilters(filters); } // Then we comply to sorters if (sorters && sorters.length) { collection.setSorters(sorters); } collection.addAll(records); if (this.getEnablePagingParams() && start !== undefined && limit !== undefined) { records = collection.items.slice(start, start + limit); } else { records = collection.items.slice(); } operation.setSuccessful(); } operation.setCompleted(); operation.setResultSet(Ext.create('Ext.data.ResultSet', { records: records, total : records.length, loaded : true })); operation.setRecords(records); if (typeof callback == 'function') { callback.call(scope || this, operation); } }, //inherit docs update: function(operation, callback, scope) { var records = operation.getRecords(), length = records.length, ids = this.getIds(), record, id, i; operation.setStarted(); for (i = 0; i < length; i++) { record = records[i]; this.setRecord(record); //we need to update the set of ids here because it's possible that a non-phantom record was added //to this proxy - in which case the record's id would never have been added via the normal 'create' call id = record.getId(); if (id !== undefined && Ext.Array.indexOf(ids, id) == -1) { ids.push(id); } } this.setIds(ids); operation.setCompleted(); operation.setSuccessful(); if (typeof callback == 'function') { callback.call(scope || this, operation); } }, //inherit destroy: function(operation, callback, scope) { var records = operation.getRecords(), length = records.length, ids = this.getIds(), //newIds is a copy of ids, from which we remove the destroyed records newIds = [].concat(ids), i; operation.setStarted(); for (i = 0; i < length; i++) { Ext.Array.remove(newIds, records[i].getId()); this.removeRecord(records[i], false); } this.setIds(newIds); operation.setCompleted(); operation.setSuccessful(); if (typeof callback == 'function') { callback.call(scope || this, operation); } }, /** * @private * Fetches a model instance from the Proxy by ID. Runs each field's decode function (if present) to decode the data. * @param {String} id The record's unique ID * @return {Ext.data.Model} The model instance or undefined if the record did not exist in the storage. */ getRecord: function(id) { if (this.cache[id] === undefined) { var recordKey = this.getRecordKey(id), item = this.getStorageObject().getItem(recordKey), data = {}, Model = this.getModel(), fields = Model.getFields().items, length = fields.length, i, field, name, record, rawData, rawValue; if (!item) { return undefined; } rawData = Ext.decode(item); for (i = 0; i < length; i++) { field = fields[i]; name = field.getName(); rawValue = rawData[name]; if (typeof field.getDecode() == 'function') { data[name] = field.getDecode()(rawValue); } else { if (field.getType().type == 'date') { data[name] = this.readDate(field, rawValue); } else { data[name] = rawValue; } } } record = new Model(data, id); this.cache[id] = record; } return this.cache[id]; }, /** * Saves the given record in the Proxy. Runs each field's encode function (if present) to encode the data. * @param {Ext.data.Model} record The model instance * @param {String} [id] The id to save the record under (defaults to the value of the record's getId() function) */ setRecord: function(record, id) { if (id) { record.setId(id); } else { id = record.getId(); } var me = this, rawData = record.getData(), data = {}, Model = me.getModel(), fields = Model.getFields().items, length = fields.length, i = 0, rawValue, field, name, obj, key; for (; i < length; i++) { field = fields[i]; name = field.getName(); rawValue = rawData[name]; if (field.getPersist() === false) { continue; } if (typeof field.getEncode() == 'function') { data[name] = field.getEncode()(rawValue, record); } else { if (field.getType().type == 'date' && Ext.isDate(rawValue)) { data[name] = this.writeDate(field, rawValue); } else { data[name] = rawValue; } } } obj = me.getStorageObject(); key = me.getRecordKey(id); //keep the cache up to date me.cache[id] = record; //iPad bug requires that we remove the item before setting it obj.removeItem(key); try { obj.setItem(key, Ext.encode(data)); } catch(e){ this.fireEvent('exception', this, e); } record.commit(); }, /** * @private * Physically removes a given record from the local storage. Used internally * by {@link #destroy}, which you should use instead because it updates the * list of currently-stored record ids. * @param {String/Number/Ext.data.Model} id The id of the record to remove, * or an Ext.data.Model instance. * @param {Boolean} [updateIds] False to skip saving the array of ids * representing the set of all records in the Proxy. */ removeRecord: function(id, updateIds) { var me = this, ids; if (id.isModel) { id = id.getId(); } if (updateIds !== false) { ids = me.getIds(); Ext.Array.remove(ids, id); me.setIds(ids); } delete this.cache[id]; me.getStorageObject().removeItem(me.getRecordKey(id)); }, /** * @private * Given the id of a record, returns a unique string based on that id and the id of this proxy. This is used when * storing data in the local storage object and should prevent naming collisions. * @param {String/Number/Ext.data.Model} id The record id, or a Model instance * @return {String} The unique key for this record */ getRecordKey: function(id) { if (id.isModel) { id = id.getId(); } return Ext.String.format("{0}-{1}", this.getId(), id); }, /** * @private * Returns the array of record IDs stored in this Proxy * @return {Number[]} The record IDs. Each is cast as a Number */ getIds: function() { var ids = (this.getStorageObject().getItem(this.getId()) || "").split(","), length = ids.length, i; if (length == 1 && ids[0] === "") { ids = []; } return ids; }, /** * @private * Saves the array of ids representing the set of all records in the Proxy * @param {Number[]} ids The ids to set */ setIds: function(ids) { var obj = this.getStorageObject(), str = ids.join(","), id = this.getId(); obj.removeItem(id); if (!Ext.isEmpty(str)) { try { obj.setItem(id, str); } catch(e){ this.fireEvent('exception', this, e); } } }, writeDate: function(field, date) { if (Ext.isEmpty(date)) { return null; } var dateFormat = field.getDateFormat() || this.getDefaultDateFormat(); switch (dateFormat) { case 'timestamp': return date.getTime() / 1000; case 'time': return date.getTime(); default: return Ext.Date.format(date, dateFormat); } }, readDate: function(field, date) { if (Ext.isEmpty(date)) { return null; } var dateFormat = field.getDateFormat() || this.getDefaultDateFormat(); switch (dateFormat) { case 'timestamp': return new Date(date * 1000); case 'time': return new Date(date); default: return Ext.isDate(date) ? date : Ext.Date.parse(date, dateFormat); } }, /** * @private * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique id of this Proxy. Called * automatically by the constructor, this should not need to be called again unless {@link #clear} has been called. */ initialize: function() { this.callParent(arguments); var storageObject = this.getStorageObject(); try { storageObject.setItem(this.getId(), storageObject.getItem(this.getId()) || ""); } catch(e){ this.fireEvent('exception', this, e); } }, /** * Destroys all records stored in the proxy and removes all keys and values used to support the proxy from the * storage object. */ clear: function() { var obj = this.getStorageObject(), ids = this.getIds(), len = ids.length, i; //remove all the records for (i = 0; i < len; i++) { this.removeRecord(ids[i], false); } //remove the supporting objects obj.removeItem(this.getId()); }, /** * @private * Abstract function which should return the storage object that data will be saved to. This must be implemented * in each subclass. * @return {Object} The storage object */ getStorageObject: function() { //<debug> Ext.Logger.error("The getStorageObject function has not been defined in your Ext.data.proxy.WebStorage subclass"); //</debug> } });