]>
Commit | Line | Data |
---|---|---|
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 | |
7 | Ext.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 | }); |