]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/data/proxy/WebStorage.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / data / proxy / WebStorage.js
CommitLineData
6527f429
DM
1/**\r
2 * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage} and {@link\r
3 * Ext.data.proxy.SessionStorage SessionStorage} proxies. It uses the new HTML5 key/value client-side storage objects to\r
4 * save {@link Ext.data.Model model instances} for offline use.\r
5 * @private\r
6 */\r
7Ext.define('Ext.data.proxy.WebStorage', {\r
8 extend: 'Ext.data.proxy.Client',\r
9 alternateClassName: 'Ext.data.WebStorageProxy',\r
10 requires: [\r
11 'Ext.data.identifier.Sequential'\r
12 ],\r
13\r
14 config: {\r
15 /**\r
16 * @cfg {String} id\r
17 * The unique ID used as the key in which all record data are stored in the local storage object.\r
18 */\r
19 id: undefined\r
20 },\r
21\r
22 /**\r
23 * @cfg {Object} reader\r
24 * Not used by web storage proxy.\r
25 * @hide\r
26 */\r
27\r
28 /**\r
29 * @cfg {Object} writer\r
30 * Not used by web storage proxy.\r
31 * @hide\r
32 */\r
33\r
34 /**\r
35 * Creates the proxy, throws an error if local storage is not supported in the current browser.\r
36 * @param {Object} config (optional) Config object.\r
37 */\r
38 constructor: function(config) {\r
39 this.callParent(arguments);\r
40\r
41 /**\r
42 * @property {Object} cache\r
43 * Cached map of records already retrieved by this Proxy. Ensures that the same instance is always retrieved.\r
44 */\r
45 this.cache = {};\r
46\r
47 //<debug>\r
48 if (this.getStorageObject() === undefined) {\r
49 Ext.raise("Local Storage is not supported in this browser, please use another type of data proxy");\r
50 }\r
51 //</debug>\r
52\r
53 //<debug>\r
54 if (this.getId() === undefined) {\r
55 Ext.raise("No unique id was provided to the local storage proxy. See Ext.data.proxy.LocalStorage documentation for details");\r
56 }\r
57 //</debug>\r
58\r
59 this.initialize();\r
60 },\r
61\r
62 /**\r
63 * @inheritdoc\r
64 */\r
65 create: function(operation) {\r
66 var me = this,\r
67 records = operation.getRecords(),\r
68 length = records.length,\r
69 ids = me.getIds(),\r
70 id, record, i, identifier;\r
71\r
72 if (me.isHierarchical === undefined) {\r
73 // 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.\r
74 // it cannot be determined during initialization because the Model is not decorated with NodeInterface until it is used in a TreeStore\r
75 me.isHierarchical = !!records[0].isNode;\r
76 if(me.isHierarchical) {\r
77 me.getStorageObject().setItem(me.getTreeKey(), true);\r
78 }\r
79 }\r
80\r
81 for (i = 0; i < length; i++) {\r
82 record = records[i];\r
83\r
84 if (record.phantom) {\r
85 record.phantom = false;\r
86 identifier = record.identifier;\r
87 if (identifier && identifier.isUnique) {\r
88 id = record.getId();\r
89 } else {\r
90 id = me.getNextId();\r
91 }\r
92 } else {\r
93 id = record.getId();\r
94 }\r
95\r
96 me.setRecord(record, id);\r
97 record.commit();\r
98 ids.push(id);\r
99 }\r
100\r
101 me.setIds(ids);\r
102\r
103 operation.setSuccessful(true);\r
104 },\r
105\r
106 /**\r
107 * @inheritdoc\r
108 */\r
109 read: function(operation) {\r
110 var me = this,\r
111 allRecords,\r
112 records = [],\r
113 success = true,\r
114 Model = me.getModel(),\r
115 validCount = 0,\r
116 recordCreator = operation.getRecordCreator(),\r
117 filters, sorters, limit, filterLen, valid, record, ids, length, data, id, i, j;\r
118\r
119 if (me.isHierarchical) {\r
120 records = me.getTreeData();\r
121 } else {\r
122 ids = me.getIds();\r
123 length = ids.length;\r
124 id = operation.getId();\r
125 //read a single record\r
126 if (id) {\r
127 data = me.getRecord(id);\r
128 if (data !== null) {\r
129 record = recordCreator ? recordCreator(data, Model) : new Model(data);\r
130 }\r
131\r
132 if (record) {\r
133 records.push(record);\r
134 } else {\r
135 success = false;\r
136 }\r
137 } else {\r
138 sorters = operation.getSorters();\r
139 filters = operation.getFilters();\r
140 limit = operation.getLimit();\r
141 allRecords = [];\r
142\r
143 // build an array of all records first first so we can sort them before\r
144 // applying filters or limit. These are Model instances instead of raw\r
145 // data objects so that the sorter and filter Fn can use the Model API\r
146 for (i = 0; i < length; i++) {\r
147 data = me.getRecord(ids[i]);\r
148 record = recordCreator ? recordCreator(data, Model) : new Model(data);\r
149 allRecords.push(record);\r
150 }\r
151\r
152 if (sorters) {\r
153 Ext.Array.sort(allRecords, Ext.util.Sorter.createComparator(sorters));\r
154 }\r
155\r
156 for (i = operation.getStart() || 0; i < length; i++) {\r
157 record = allRecords[i];\r
158 valid = true;\r
159\r
160 if (filters) {\r
161 for (j = 0, filterLen = filters.length; j < filterLen; j++) {\r
162 valid = filters[j].filter(record);\r
163 }\r
164 }\r
165\r
166 if (valid) {\r
167 records.push(record);\r
168 validCount++;\r
169 }\r
170\r
171 if (limit && validCount === limit) {\r
172 break;\r
173 }\r
174 }\r
175 }\r
176\r
177 }\r
178\r
179 if (success) {\r
180 operation.setResultSet(new Ext.data.ResultSet({\r
181 records: records,\r
182 total : records.length,\r
183 loaded : true\r
184 }));\r
185 operation.setSuccessful(true);\r
186 } else {\r
187 operation.setException('Unable to load records');\r
188 }\r
189 },\r
190\r
191 /**\r
192 * @inheritdoc\r
193 */\r
194 update: function(operation) {\r
195 var records = operation.getRecords(),\r
196 length = records.length,\r
197 ids = this.getIds(),\r
198 record, id, i;\r
199\r
200 for (i = 0; i < length; i++) {\r
201 record = records[i];\r
202 this.setRecord(record);\r
203 record.commit();\r
204\r
205 //we need to update the set of ids here because it's possible that a non-phantom record was added\r
206 //to this proxy - in which case the record's id would never have been added via the normal 'create' call\r
207 id = record.getId();\r
208 if (id !== undefined && Ext.Array.indexOf(ids, id) === -1) {\r
209 ids.push(id);\r
210 }\r
211 }\r
212 this.setIds(ids);\r
213 operation.setSuccessful(true);\r
214 },\r
215\r
216 /**\r
217 * @inheritdoc\r
218 */\r
219 erase: function(operation) {\r
220 var me = this,\r
221 records = operation.getRecords(),\r
222 ids = me.getIds(),\r
223 idLength = ids.length,\r
224 newIds = [],\r
225 removedHash = {},\r
226 i = records.length,\r
227 id;\r
228\r
229 for (; i--;) {\r
230 Ext.apply(removedHash, me.removeRecord(records[i]));\r
231 }\r
232\r
233 for(i = 0; i < idLength; i++) {\r
234 id = ids[i];\r
235 if(!removedHash[id]) {\r
236 newIds.push(id);\r
237 }\r
238 }\r
239\r
240 me.setIds(newIds);\r
241 operation.setSuccessful(true);\r
242 },\r
243\r
244 /**\r
245 * @private\r
246 * Fetches record data from the Proxy by ID.\r
247 * @param {String} id The record's unique ID\r
248 * @return {Object} The record data\r
249 */\r
250 getRecord: function(id) {\r
251 var me = this,\r
252 cache = me.cache,\r
253 data = !cache[id] ? Ext.decode(me.getStorageObject().getItem(me.getRecordKey(id))) : cache[id];\r
254\r
255 if(!data) {\r
256 return null;\r
257 }\r
258\r
259 cache[id] = data;\r
260 data[me.getModel().prototype.idProperty] = id;\r
261\r
262 // In order to preserve the cache, we MUST copy it here because\r
263 // Models use the incoming raw data as their data object and convert/default values into that object\r
264 return Ext.merge({}, data);\r
265 },\r
266\r
267 /**\r
268 * Saves the given record in the Proxy.\r
269 * @param {Ext.data.Model} record The model instance\r
270 * @param {String} [id] The id to save the record under (defaults to the value of the record's getId() function)\r
271 */\r
272 setRecord: function(record, id) {\r
273 if (id) {\r
274 record.set('id', id, {\r
275 commit: true\r
276 });\r
277 } else {\r
278 id = record.getId();\r
279 }\r
280\r
281 var me = this,\r
282 rawData = record.getData(),\r
283 data = {},\r
284 model = me.getModel(),\r
285 fields = model.getFields(),\r
286 length = fields.length,\r
287 i = 0,\r
288 field, name, obj, key;\r
289\r
290 for (; i < length; i++) {\r
291 field = fields[i];\r
292 name = field.name;\r
293\r
294 if(field.persist) {\r
295 data[name] = rawData[name];\r
296 }\r
297 }\r
298\r
299 // no need to store the id in the data, since it is already stored in the record key\r
300 delete data[model.prototype.idProperty];\r
301\r
302 // if the record is a tree node and it's a direct child of the root node, do not store the parentId\r
303 if(record.isNode && record.get('depth') === 1) {\r
304 delete data.parentId;\r
305 }\r
306\r
307 obj = me.getStorageObject();\r
308 key = me.getRecordKey(id);\r
309\r
310 //keep the cache up to date\r
311 me.cache[id] = data;\r
312\r
313 //iPad bug requires that we remove the item before setting it\r
314 obj.removeItem(key);\r
315 obj.setItem(key, Ext.encode(data));\r
316 },\r
317\r
318 /**\r
319 * @private\r
320 * 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}.\r
321 * @param {Ext.data.Model} record The record to remove\r
322 * @return {Object} a hash with the ids of the records that were removed as keys and the records that were removed as values\r
323 */\r
324 removeRecord: function(record) {\r
325 var me = this,\r
326 id = record.getId(),\r
327 records = {},\r
328 i, childNodes;\r
329\r
330 records[id] = record;\r
331 me.getStorageObject().removeItem(me.getRecordKey(id));\r
332 delete me.cache[id];\r
333\r
334 if(record.childNodes) {\r
335 childNodes = record.childNodes;\r
336 for(i = childNodes.length; i--;) {\r
337 Ext.apply(records, me.removeRecord(childNodes[i]));\r
338 }\r
339 }\r
340\r
341 return records;\r
342 },\r
343\r
344 /**\r
345 * @private\r
346 * Given the id of a record, returns a unique string based on that id and the id of this proxy. This is used when\r
347 * storing data in the local storage object and should prevent naming collisions.\r
348 * @param {String/Number/Ext.data.Model} id The record id, or a Model instance\r
349 * @return {String} The unique key for this record\r
350 */\r
351 getRecordKey: function(id) {\r
352 if (id.isModel) {\r
353 id = id.getId();\r
354 }\r
355\r
356 return Ext.String.format("{0}-{1}", this.getId(), id);\r
357 },\r
358\r
359 /**\r
360 * @private\r
361 * Returns the unique key used to store the current record counter for this proxy. This is used internally when\r
362 * realizing models (creating them when they used to be phantoms), in order to give each model instance a unique id.\r
363 * @return {String} The counter key\r
364 */\r
365 getRecordCounterKey: function() {\r
366 return Ext.String.format("{0}-counter", this.getId());\r
367 },\r
368\r
369 /**\r
370 * @private\r
371 * Returns the unique key used to store the tree indicator. This is used internally to determine if the stored data is hierarchical\r
372 * @return {String} The counter key\r
373 */\r
374 getTreeKey: function() {\r
375 return Ext.String.format("{0}-tree", this.getId());\r
376 },\r
377\r
378 /**\r
379 * @private\r
380 * Returns the array of record IDs stored in this Proxy\r
381 * @return {Number[]} The record IDs. Each is cast as a Number\r
382 */\r
383 getIds: function() {\r
384 var me = this,\r
385 ids = (me.getStorageObject().getItem(me.getId()) || "").split(","),\r
386 length = ids.length,\r
387 isString = this.getIdField().isStringField,\r
388 i;\r
389\r
390 if (length === 1 && ids[0] === "") {\r
391 ids = [];\r
392 } else {\r
393 for (i = 0; i < length; i++) {\r
394 ids[i] = isString ? ids[i] : +ids[i];\r
395 }\r
396 }\r
397\r
398 return ids;\r
399 },\r
400 \r
401 getIdField: function() {\r
402 return this.getModel().prototype.idField;\r
403 },\r
404\r
405 /**\r
406 * @private\r
407 * Saves the array of ids representing the set of all records in the Proxy\r
408 * @param {Number[]} ids The ids to set\r
409 */\r
410 setIds: function(ids) {\r
411 var obj = this.getStorageObject(),\r
412 str = ids.join(","),\r
413 id = this.getId();\r
414\r
415 obj.removeItem(id);\r
416\r
417 if (!Ext.isEmpty(str)) {\r
418 obj.setItem(id, str);\r
419 }\r
420 },\r
421\r
422 /**\r
423 * @private\r
424 * Returns the next numerical ID that can be used when realizing a model instance (see getRecordCounterKey).\r
425 * Increments the counter.\r
426 * @return {Number} The id\r
427 */\r
428 getNextId: function() {\r
429 var me = this,\r
430 obj = me.getStorageObject(),\r
431 key = me.getRecordCounterKey(),\r
432 isString = me.getIdField().isStringField,\r
433 id;\r
434\r
435 id = me.idGenerator.generate();\r
436\r
437 obj.setItem(key, id);\r
438\r
439 if(isString) {\r
440 id = id + '';\r
441 }\r
442\r
443 return id;\r
444 },\r
445\r
446 /**\r
447 * Gets tree data and transforms it from key value pairs into a hierarchical structure.\r
448 * @private\r
449 * @return {Ext.data.NodeInterface[]}\r
450 */\r
451 getTreeData: function() {\r
452 var me = this,\r
453 ids = me.getIds(),\r
454 length = ids.length,\r
455 records = [],\r
456 recordHash = {},\r
457 root = [],\r
458 i = 0,\r
459 Model = me.getModel(),\r
460 idProperty = Model.prototype.idProperty,\r
461 rootLength, record, parent, parentId, children, id;\r
462\r
463 for(; i < length; i++) {\r
464 id = ids[i];\r
465 // get the record for each id\r
466 record = me.getRecord(id);\r
467 // push the record into the records array\r
468 records.push(record);\r
469 // add the record to the record hash so it can be easily retrieved by id later\r
470 recordHash[id] = record;\r
471 if(!record.parentId) {\r
472 // push records that are at the root level (those with no parent id) into the "root" array\r
473 root.push(record);\r
474 }\r
475 }\r
476\r
477 rootLength = root.length;\r
478\r
479 // 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\r
480 Ext.Array.sort(records, me.sortByParentId);\r
481\r
482 // append each record to its parent, starting after the root node(s), since root nodes do not need to be attached to a parent\r
483 for(i = rootLength; i < length; i++) {\r
484 record = records[i];\r
485 parentId = record.parentId;\r
486 if(!parent || parent[idProperty] !== parentId) {\r
487 // if this record has a different parent id from the previous record, we need to look up the parent by id.\r
488 parent = recordHash[parentId];\r
489 parent.children = children = [];\r
490 }\r
491\r
492 // push the record onto its parent's children array\r
493 children.push(record);\r
494 }\r
495\r
496 for(i = length; i--;) {\r
497 record = records[i];\r
498 if (!record.children && !record.leaf) {\r
499 // set non-leaf nodes with no children to loaded so the proxy won't try to dynamically load their contents when they are expanded\r
500 record.loaded = true;\r
501 }\r
502 }\r
503\r
504 // Create model instances out of all the "root-level" nodes.\r
505 for(i = rootLength; i--;) {\r
506 record = root[i];\r
507 root[i] = new Model(record);\r
508 }\r
509\r
510 return root;\r
511 },\r
512\r
513 /**\r
514 * Sorter function for sorting records by parentId\r
515 * @private\r
516 * @param {Object} node1\r
517 * @param {Object} node2\r
518 * @return {Number}\r
519 */\r
520 sortByParentId: function(node1, node2) {\r
521 return (node1.parentId || 0) - (node2.parentId || 0);\r
522 },\r
523\r
524 /**\r
525 * @private\r
526 * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique id of this Proxy. Called\r
527 * automatically by the constructor, this should not need to be called again unless {@link #clear} has been called.\r
528 */\r
529 initialize: function() {\r
530 var me = this,\r
531 storageObject = me.getStorageObject(),\r
532 lastId = +storageObject.getItem(me.getRecordCounterKey()),\r
533 id = me.getId();\r
534\r
535 storageObject.setItem(id, storageObject.getItem(id) || "");\r
536 if(storageObject.getItem(me.getTreeKey())) {\r
537 me.isHierarchical = true;\r
538 }\r
539\r
540 me.idGenerator = new Ext.data.identifier.Sequential({\r
541 seed: lastId ? lastId + 1 : 1\r
542 });\r
543 },\r
544\r
545 /**\r
546 * Destroys all records stored in the proxy and removes all keys and values used to support the proxy from the\r
547 * storage object.\r
548 */\r
549 clear: function() {\r
550 var me = this,\r
551 obj = me.getStorageObject(),\r
552 ids = me.getIds(),\r
553 len = ids.length,\r
554 i;\r
555\r
556 //remove all the records\r
557 for (i = 0; i < len; i++) {\r
558 obj.removeItem(me.getRecordKey(ids[i]));\r
559 }\r
560\r
561 //remove the supporting objects\r
562 obj.removeItem(me.getRecordCounterKey());\r
563 obj.removeItem(me.getTreeKey());\r
564 obj.removeItem(me.getId());\r
565\r
566 // clear the cache\r
567 me.cache = {};\r
568 },\r
569\r
570 /**\r
571 * @private\r
572 * Abstract function which should return the storage object that data will be saved to. This must be implemented\r
573 * in each subclass.\r
574 * @return {Object} The storage object\r
575 */\r
576 getStorageObject: function() {\r
577 //<debug>\r
578 Ext.raise("The getStorageObject function has not been defined in your Ext.data.proxy.WebStorage subclass");\r
579 //</debug>\r
580 }\r
581});