/** * @author Ed Spencer * @class Ext.data.WebStorageProxy * @extends Ext.data.ClientProxy * *WebStorageProxy is simply a superclass for the {@link Ext.data.LocalStorageProxy localStorage} and * {@link Ext.data.SessionStorageProxy sessionStorage} proxies. It uses the new HTML5 key/value client-side storage * objects to save {@link Ext.data.Model model instances} for offline use.
* * @constructor * Creates the proxy, throws an error if local storage is not supported in the current browser * @param {Object} config Optional config object */ Ext.data.WebStorageProxy = Ext.extend(Ext.data.ClientProxy, { /** * @cfg {String} id The unique ID used as the key in which all record data are stored in the local storage object */ id: undefined, /** * @ignore */ constructor: function(config) { Ext.data.WebStorageProxy.superclass.constructor.call(this, config); /** * Cached map of records already retrieved by this Proxy - ensures that the same instance is always retrieved * @property cache * @type Object */ this.cache = {}; if (this.getStorageObject() == undefined) { throw "Local Storage is not supported in this browser, please use another type of data proxy"; } //if an id is not given, try to use the store's id instead this.id = this.id || (this.store ? this.store.storeId : undefined); if (this.id == undefined) { throw "No unique id was provided to the local storage proxy. See Ext.data.LocalStorageProxy documentation for details"; } this.initialize(); }, //inherit docs create: function(operation, callback, scope) { var records = operation.records, length = records.length, ids = this.getIds(), id, record, i; operation.setStarted(); for (i = 0; i < length; i++) { record = records[i]; if (record.phantom) { record.phantom = false; id = this.getNextId(); } else { id = record.getId(); } this.setRecord(record, id); 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) { //TODO: respect sorters, filters, start and limit options on the Operation var records = [], ids = this.getIds(), length = ids.length, i, recordData, record; //read a single record if (operation.id) { record = this.getRecord(operation.id); if (record) { records.push(record); operation.setSuccessful(); } } else { for (i = 0; i < length; i++) { records.push(this.getRecord(ids[i])); } operation.setSuccessful(); } operation.setCompleted(); operation.resultSet = new Ext.data.ResultSet({ records: records, total : records.length, loaded : true }); if (typeof callback == 'function') { callback.call(scope || this, operation); } }, //inherit docs update: function(operation, callback, scope) { var records = operation.records, 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 && ids.indexOf(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.records, length = records.length, ids = this.getIds(), //newIds is a copy of ids, from which we remove the destroyed records newIds = [].concat(ids), i; for (i = 0; i < length; i++) { newIds.remove(records[i].getId()); this.removeRecord(records[i], false); } this.setIds(newIds); 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 */ getRecord: function(id) { if (this.cache[id] == undefined) { var rawData = Ext.decode(this.getStorageObject().getItem(this.getRecordKey(id))), data = {}, Model = this.model, fields = Model.prototype.fields.items, length = fields.length, i, field, name, record; for (i = 0; i < length; i++) { field = fields[i]; name = field.name; if (typeof field.decode == 'function') { data[name] = field.decode(rawData[name]); } else { data[name] = rawData[name]; } } record = new Model(data); record.phantom = false; 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 rawData = record.data, data = {}, model = this.model, fields = model.prototype.fields.items, length = fields.length, i, field, name; for (i = 0; i < length; i++) { field = fields[i]; name = field.name; if (typeof field.encode == 'function') { data[name] = field.encode(rawData[name], record); } else { data[name] = rawData[name]; } } var obj = this.getStorageObject(), key = this.getRecordKey(id); //keep the cache up to date this.cache[id] = record; //iPad bug requires that we remove the item before setting it obj.removeItem(key); obj.setItem(key, Ext.encode(data)); }, /** * @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 */ removeRecord: function(id, updateIds) { if (id instanceof Ext.data.Model) { id = id.getId(); } if (updateIds !== false) { var ids = this.getIds(); ids.remove(id); this.setIds(ids); } this.getStorageObject().removeItem(this.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 instanceof Ext.data.Model) { id = id.getId(); } return Ext.util.Format.format("{0}-{1}", this.id, id); }, /** * @private * Returns the unique key used to store the current record counter for this proxy. This is used internally when * realizing models (creating them when they used to be phantoms), in order to give each model instance a unique id. * @return {String} The counter key */ getRecordCounterKey: function() { return Ext.util.Format.format("{0}-counter", this.id); }, /** * @private * Returns the array of record IDs stored in this Proxy * @return {Array} The record IDs. Each is cast as a Number */ getIds: function() { var ids = (this.getStorageObject().getItem(this.id) || "").split(","), length = ids.length, i; if (length == 1 && ids[0] == "") { ids = []; } else { for (i = 0; i < length; i++) { ids[i] = parseInt(ids[i], 10); } } return ids; }, /** * @private * Saves the array of ids representing the set of all records in the Proxy * @param {Array} ids The ids to set */ setIds: function(ids) { var obj = this.getStorageObject(), str = ids.join(","); obj.removeItem(this.id); if (!Ext.isEmpty(str)) { obj.setItem(this.id, str); } }, /** * @private * Returns the next numerical ID that can be used when realizing a model instance (see getRecordCounterKey). Increments * the counter. * @return {Number} The id */ getNextId: function() { var obj = this.getStorageObject(), key = this.getRecordCounterKey(), last = obj[key], ids, id; if (last == undefined) { ids = this.getIds(); last = ids[ids.length - 1] || 0; } id = parseInt(last, 10) + 1; obj.setItem(key, id); return id; }, /** * @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() { var storageObject = this.getStorageObject(); storageObject.setItem(this.id, storageObject.getItem(this.id) || ""); }, /** * 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]); } //remove the supporting objects obj.removeItem(this.getRecordCounterKey()); obj.removeItem(this.id); }, /** * @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() { throw new Error("The getStorageObject function has not been defined in your Ext.data.WebStorageProxy subclass"); } });