/** * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage} * and {@link Ext.data.proxy.SessionStorage SessionStorage} proxies. 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.data.identifier.Sequential' ], 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 }, /** * @cfg {Object} reader * Not used by web storage proxy. * @hide */ /** * @cfg {Object} writer * Not used by web storage proxy. * @hide */ /** * 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.raise("Local Storage is not supported in this browser, please use another type " + "of data proxy"); } //</debug> //<debug> if (this.getId() === undefined) { Ext.raise("No unique id was provided to the local storage proxy. " + "See Ext.data.proxy.LocalStorage documentation for details"); } //</debug> this.initialize(); }, /** * @method create * @inheritdoc */ create: function(operation) { var me = this, records = operation.getRecords(), length = records.length, ids = me.getIds(), id, record, i, identifier; if (me.isHierarchical === undefined) { // if the storage object does not yet contain any data, this is the first point // at which we can determine whether or not this proxy deals with hierarchical data. // it cannot be determined during initialization because the Model is not decorated // with NodeInterface until it is used in a TreeStore me.isHierarchical = !!records[0].isNode; if (me.isHierarchical) { me.getStorageObject().setItem(me.getTreeKey(), true); } } for (i = 0; i < length; i++) { record = records[i]; if (record.phantom) { identifier = record.identifier; if (identifier && identifier.isUnique) { id = record.getId(); } else { id = me.getNextId(); } } else { id = record.getId(); } me.setRecord(record, id); record.commit(); ids.push(id); } me.setIds(ids); operation.setSuccessful(true); }, /** * @method read * @inheritdoc */ read: function(operation) { var me = this, allRecords, records = [], success = true, Model = me.getModel(), validCount = 0, recordCreator = operation.getRecordCreator(), filters, sorters, limit, filterLen, valid, record, ids, length, data, id, i, j; if (me.isHierarchical) { records = me.getTreeData(); } else { ids = me.getIds(); length = ids.length; id = operation.getId(); // read a single record if (id) { data = me.getRecord(id); if (data !== null) { record = recordCreator ? recordCreator(data, Model) : new Model(data); } if (record) { records.push(record); } else { success = false; } } else { sorters = operation.getSorters(); filters = operation.getFilters(); limit = operation.getLimit(); allRecords = []; // build an array of all records first first so we can sort them before // applying filters or limit. These are Model instances instead of raw // data objects so that the sorter and filter Fn can use the Model API for (i = 0; i < length; i++) { data = me.getRecord(ids[i]); record = recordCreator ? recordCreator(data, Model) : new Model(data); allRecords.push(record); } if (sorters) { Ext.Array.sort(allRecords, Ext.util.Sorter.createComparator(sorters)); } for (i = operation.getStart() || 0; i < length; i++) { record = allRecords[i]; valid = true; if (filters) { for (j = 0, filterLen = filters.length; j < filterLen; j++) { valid = filters[j].filter(record); } } if (valid) { records.push(record); validCount++; } if (limit && validCount === limit) { break; } } } } if (success) { operation.setResultSet(new Ext.data.ResultSet({ records: records, total: records.length, loaded: true })); operation.setSuccessful(true); } else { operation.setException('Unable to load records'); } }, /** * @method update * @inheritdoc */ update: function(operation) { var records = operation.getRecords(), length = records.length, ids = this.getIds(), record, id, i; for (i = 0; i < length; i++) { record = records[i]; this.setRecord(record); record.commit(); // 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.setSuccessful(true); }, /** * @method erase * @inheritdoc */ erase: function(operation) { var me = this, records = operation.getRecords(), ids = me.getIds(), idLength = ids.length, newIds = [], removedHash = {}, i = records.length, id; for (; i--;) { Ext.apply(removedHash, me.removeRecord(records[i])); } for (i = 0; i < idLength; i++) { id = ids[i]; if (!removedHash[id]) { newIds.push(id); } } me.setIds(newIds); operation.setSuccessful(true); }, /** * @private * Fetches record data from the Proxy by ID. * @param {String} id The record's unique ID * @return {Object} The record data */ getRecord: function(id) { var me = this, cache = me.cache, data; data = !cache[id] ? Ext.decode(me.getStorageObject().getItem(me.getRecordKey(id))) : cache[id]; if (!data) { return null; } cache[id] = data; data[me.getModel().prototype.idProperty] = id; // In order to preserve the cache, we MUST copy it here because // Models use the incoming raw data as their data object and convert/default values // into that object return Ext.merge({}, data); }, /** * Saves the given record in the Proxy. * @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.set('id', id, { commit: true }); } else { id = record.getId(); } /* eslint-disable-next-line vars-on-top */ var me = this, rawData = record.getData(), data = {}, model = me.getModel(), fields = model.getFields(), length = fields.length, i = 0, field, name, obj, key, value; for (; i < length; i++) { field = fields[i]; name = field.name; if (field.persist) { value = rawData[name]; if (field.isDateField && field.dateFormat && Ext.isDate(value)) { value = Ext.Date.format(value, field.dateFormat); } else if (field.serialize) { value = field.serialize(value, record); } data[name] = value; } } // no need to store the id in the data, since it is already stored in the record key delete data[model.prototype.idProperty]; // if the record is a tree node and it's a direct child of the root node, do not store // the parentId if (record.isNode && record.get('depth') === 1) { delete data.parentId; } obj = me.getStorageObject(); key = me.getRecordKey(id); // keep the cache up to date me.cache[id] = data; // 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 and recursively removes children * if the record is a tree node. Used internally by {@link #destroy}. * @param {Ext.data.Model} record The record to remove * @return {Object} a hash with the ids of the records that were removed as keys and the * records that were removed as values */ removeRecord: function(record) { var me = this, id = record.getId(), records = {}, i, childNodes; records[id] = record; me.getStorageObject().removeItem(me.getRecordKey(id)); delete me.cache[id]; if (record.childNodes) { childNodes = record.childNodes; for (i = childNodes.length; i--;) { Ext.apply(records, me.removeRecord(childNodes[i])); } } return records; }, /** * @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 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.String.format("{0}-counter", this.getId()); }, /** * @private * Returns the unique key used to store the tree indicator. This is used internally to * determine if the stored data is hierarchical * @return {String} The counter key */ getTreeKey: function() { return Ext.String.format("{0}-tree", this.getId()); }, /** * @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 me = this, ids = (me.getStorageObject().getItem(me.getId()) || "").split(","), length = ids.length, isString = this.getIdField().isStringField, i; if (length === 1 && ids[0] === "") { ids = []; } else { for (i = 0; i < length; i++) { ids[i] = isString ? ids[i] : (Ext.isNumber(+ids[i]) ? +ids[i] : ids[i]); } } return ids; }, getIdField: function() { return this.getModel().prototype.idField; }, /** * @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)) { obj.setItem(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 me = this, obj = me.getStorageObject(), key = me.getRecordCounterKey(), isString = me.getIdField().isStringField, id; id = me.idGenerator.generate(); obj.setItem(key, id); if (isString) { id = id + ''; } return id; }, /** * Gets tree data and transforms it from key value pairs into a hierarchical structure. * @private * @return {Ext.data.NodeInterface[]} */ getTreeData: function() { var me = this, ids = me.getIds(), length = ids.length, records = [], recordHash = {}, root = [], i = 0, Model = me.getModel(), idProperty = Model.prototype.idProperty, rootLength, record, parent, parentId, children, id; for (; i < length; i++) { id = ids[i]; // get the record for each id record = me.getRecord(id); // push the record into the records array records.push(record); // add the record to the record hash so it can be easily retrieved by id later recordHash[id] = record; if (!record.parentId) { // push records that are at the root level (those with no parent id) into the // "root" array root.push(record); } } rootLength = root.length; // sort the records by parent id for greater efficiency, so that each parent record only // has to be found once for all of its children Ext.Array.sort(records, me.sortByParentId); // append each record to its parent, starting after the root node(s), since root nodes // do not need to be attached to a parent for (i = rootLength; i < length; i++) { record = records[i]; parentId = record.parentId; if (!parent || parent[idProperty] !== parentId) { // if this record has a different parent id from the previous record, we need to // look up the parent by id. parent = recordHash[parentId]; parent.children = children = []; } // push the record onto its parent's children array children.push(record); } for (i = length; i--;) { record = records[i]; if (!record.children && !record.leaf) { // set non-leaf nodes with no children to loaded so the proxy won't try to // dynamically load their contents when they are expanded record.loaded = true; } } // Create model instances out of all the "root-level" nodes. for (i = rootLength; i--;) { record = root[i]; root[i] = new Model(record); } return root; }, /** * Sorter function for sorting records by parentId * @private * @param {Object} node1 * @param {Object} node2 * @return {Number} */ sortByParentId: function(node1, node2) { return (node1.parentId || 0) - (node2.parentId || 0); }, /** * @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 me = this, storageObject = me.getStorageObject(), lastId = +storageObject.getItem(me.getRecordCounterKey()), id = me.getId(); storageObject.setItem(id, storageObject.getItem(id) || ""); if (storageObject.getItem(me.getTreeKey())) { me.isHierarchical = true; } me.idGenerator = new Ext.data.identifier.Sequential({ seed: lastId ? lastId + 1 : 1 }); }, /** * 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 me = this, obj = me.getStorageObject(), ids = me.getIds(), len = ids.length, i; // remove all the records for (i = 0; i < len; i++) { obj.removeItem(me.getRecordKey(ids[i])); } // remove the supporting objects obj.removeItem(me.getRecordCounterKey()); obj.removeItem(me.getTreeKey()); obj.removeItem(me.getId()); // clear the cache me.cache = {}; }, /** * @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.raise("The getStorageObject function has not been defined in your " + "Ext.data.proxy.WebStorage subclass"); //</debug> }});