487 lines
14 KiB
JavaScript
487 lines
14 KiB
JavaScript
/**
|
|
* @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>
|
|
}
|
|
}); |