]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * A BufferedStore maintains a sparsely populated map of pages corresponding to an extremely large server-side dataset.\r | |
3 | *\r | |
4 | * Use a BufferedStore when the dataset size is so large that the database and network latency, and client memory requirements\r | |
5 | * preclude caching the entire dataset in a regular {@link Ext.data.Store Store}.\r | |
6 | *\r | |
7 | * When using a BufferedStore *not all of the dataset is present in the client*. Only pages which have been\r | |
8 | * requested by the UI (usually a {@link Ext.grid.Panel GridPanel}) and surrounding pages will be present. Retention\r | |
9 | * of viewed pages in the BufferedStore after they have been scrolled out of view is configurable. See {@link #leadingBufferZone},\r | |
10 | * {@link #trailingBufferZone} and {@link #purgePageCount}.\r | |
11 | *\r | |
12 | * To use a BufferedStore, initiate the loading process by loading the first page. The number of rows rendered are\r | |
13 | * determined automatically, and the range of pages needed to keep the cache primed for scrolling is\r | |
14 | * requested and cached.\r | |
15 | * Example:\r | |
16 | *\r | |
17 | * myBufferedStore.loadPage(1); // Load page 1\r | |
18 | *\r | |
19 | * A {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} is instantiated which will monitor the scrolling in the grid, and\r | |
20 | * refresh the view's rows from the page cache as needed. It will also pull new data into the page\r | |
21 | * cache when scrolling of the view draws upon data near either end of the prefetched data.\r | |
22 | *\r | |
23 | * The margins which trigger view refreshing from the prefetched data are {@link Ext.grid.plugin.BufferedRenderer#numFromEdge},\r | |
24 | * {@link Ext.grid.plugin.BufferedRenderer#leadingBufferZone} and {@link Ext.grid.plugin.BufferedRenderer#trailingBufferZone}.\r | |
25 | *\r | |
26 | * The margins which trigger loading more data into the page cache are, {@link #leadingBufferZone} and\r | |
27 | * {@link #trailingBufferZone}.\r | |
28 | *\r | |
29 | * By default, only 5 pages of data (in addition to the pages which over the visible region) are cached in the page cache,\r | |
30 | * with old pages being evicted from the cache as the view moves down through the dataset. This is controlled by the\r | |
31 | * {@link #purgePageCount} setting.\r | |
32 | *\r | |
33 | * Setting this value to zero means that no pages are *ever* scrolled out of the page cache, and\r | |
34 | * that eventually the whole dataset may become present in the page cache. This is sometimes desirable\r | |
35 | * as long as datasets do not reach astronomical proportions.\r | |
36 | *\r | |
37 | * Selection state may be maintained across page boundaries by configuring the SelectionModel not to discard\r | |
38 | * records from its collection when those Records cycle out of the Store's primary collection. This is done\r | |
39 | * by configuring the SelectionModel like this:\r | |
40 | *\r | |
41 | * selModel: {\r | |
42 | * pruneRemoved: false\r | |
43 | * }\r | |
44 | *\r | |
45 | */\r | |
46 | Ext.define('Ext.data.BufferedStore', {\r | |
47 | extend: 'Ext.data.ProxyStore',\r | |
48 | \r | |
49 | alias: 'store.buffered',\r | |
50 | \r | |
51 | requires: [\r | |
52 | 'Ext.data.PageMap',\r | |
53 | 'Ext.util.Filter',\r | |
54 | 'Ext.util.Sorter',\r | |
55 | 'Ext.util.Grouper'\r | |
56 | ],\r | |
57 | \r | |
58 | uses: [\r | |
59 | 'Ext.util.SorterCollection',\r | |
60 | 'Ext.util.FilterCollection',\r | |
61 | 'Ext.util.GroupCollection'\r | |
62 | ],\r | |
63 | \r | |
64 | /**\r | |
65 | * @property {Boolean} isBufferedStore\r | |
66 | * `true` in this class to identify an object as an instantiated BufferedStore, or subclass thereof.\r | |
67 | */\r | |
68 | isBufferedStore: true,\r | |
69 | \r | |
70 | // For backward compatibility with user code.\r | |
71 | buffered: true,\r | |
72 | \r | |
73 | config: {\r | |
74 | data: 0,\r | |
75 | pageSize: 25,\r | |
76 | remoteSort: true,\r | |
77 | remoteFilter: true,\r | |
78 | sortOnLoad: false,\r | |
79 | /**\r | |
80 | * @cfg {Number} purgePageCount\r | |
81 | *\r | |
82 | * The number of pages *in addition to twice the required buffered range* to keep in the prefetch cache before purging least recently used records.\r | |
83 | *\r | |
84 | * For example, if the height of the view area and the configured {@link #trailingBufferZone} and {@link #leadingBufferZone} require that there\r | |
85 | * are three pages in the cache, then a `purgePageCount` of 5 ensures that up to 11 pages can be in the page cache any any one time. This is enough\r | |
86 | * to allow the user to scroll rapidly between different areas of the dataset without evicting pages which are still needed.\r | |
87 | *\r | |
88 | * A value of 0 indicates to never purge the prefetched data.\r | |
89 | */\r | |
90 | purgePageCount: 5,\r | |
91 | \r | |
92 | /**\r | |
93 | * @cfg {Number} trailingBufferZone\r | |
94 | * The number of extra records to keep cached on the trailing side of scrolling buffer\r | |
95 | * as scrolling proceeds. A larger number means fewer replenishments from the server.\r | |
96 | */\r | |
97 | trailingBufferZone: 25,\r | |
98 | \r | |
99 | /**\r | |
100 | * @cfg {Number} leadingBufferZone\r | |
101 | * The number of extra rows to keep cached on the leading side of scrolling buffer\r | |
102 | * as scrolling proceeds. A larger number means fewer replenishments from the server.\r | |
103 | */\r | |
104 | leadingBufferZone: 200,\r | |
105 | \r | |
106 | /**\r | |
107 | * @cfg {Number} defaultViewSize The default view size to use until the {@link #viewSize} has been configured.\r | |
108 | * @private\r | |
109 | */\r | |
110 | defaultViewSize: 100, \r | |
111 | \r | |
112 | /**\r | |
113 | * @cfg {Number} viewSize The view size needed to fill the current view. Defaults to the {@link #defaultViewSize}.\r | |
114 | * This will typically be set by the underlying view.\r | |
115 | * @private\r | |
116 | */\r | |
117 | viewSize: 0,\r | |
118 | \r | |
119 | /**\r | |
120 | * @inheritdoc\r | |
121 | */\r | |
122 | trackRemoved: false\r | |
123 | },\r | |
124 | \r | |
125 | /**\r | |
126 | * We are using applyData so that we can return nothing and prevent the `this.data`\r | |
127 | * property to be overridden.\r | |
128 | * @param {Array/Object} data\r | |
129 | */\r | |
130 | applyData: function(data) {\r | |
131 | var dataCollection = this.data || (this.data = this.createDataCollection());\r | |
132 | \r | |
133 | //<debug>\r | |
134 | if (data && data !== true) {\r | |
135 | Ext.raise('Cannot load a buffered store with local data - the store is a map of remote data');\r | |
136 | }\r | |
137 | //</debug>\r | |
138 | \r | |
139 | return dataCollection;\r | |
140 | },\r | |
141 | \r | |
142 | applyProxy: function(proxy) {\r | |
143 | proxy = this.callParent([proxy]);\r | |
144 | \r | |
145 | // This store asks for pages.\r | |
146 | // If used with a MemoryProxy, it must work\r | |
147 | if (proxy && proxy.setEnablePaging) {\r | |
148 | proxy.setEnablePaging(true);\r | |
149 | }\r | |
150 | return proxy;\r | |
151 | },\r | |
152 | \r | |
153 | createFiltersCollection: function() {\r | |
154 | return new Ext.util.FilterCollection();\r | |
155 | },\r | |
156 | \r | |
157 | createSortersCollection: function() {\r | |
158 | return new Ext.util.SorterCollection();\r | |
159 | },\r | |
160 | \r | |
161 | //<debug>\r | |
162 | updateRemoteFilter: function(remoteFilter, oldRemoteFilter) {\r | |
163 | if (remoteFilter === false) {\r | |
164 | Ext.raise('Buffered stores are always remotely filtered.');\r | |
165 | }\r | |
166 | this.callParent([remoteFilter, oldRemoteFilter]);\r | |
167 | },\r | |
168 | \r | |
169 | updateRemoteSort: function(remoteSort, oldRemoteSort) {\r | |
170 | if (remoteSort === false) {\r | |
171 | Ext.raise('Buffered stores are always remotely sorted.');\r | |
172 | }\r | |
173 | this.callParent([remoteSort, oldRemoteSort]);\r | |
174 | },\r | |
175 | \r | |
176 | updateTrackRemoved: function(value) {\r | |
177 | if (value !== false) {\r | |
178 | Ext.raise('Cannot use trackRemoved with a buffered store.');\r | |
179 | }\r | |
180 | this.callParent(arguments);\r | |
181 | },\r | |
182 | //</debug>\r | |
183 | \r | |
184 | updateGroupField: function(field) {\r | |
185 | this.group(field);\r | |
186 | },\r | |
187 | \r | |
188 | getGrouper: function() {\r | |
189 | return this.grouper;\r | |
190 | },\r | |
191 | \r | |
192 | isGrouped: function() {\r | |
193 | return !!this.grouper;\r | |
194 | },\r | |
195 | \r | |
196 | createDataCollection: function() {\r | |
197 | var me = this,\r | |
198 | result = new Ext.data.PageMap({\r | |
199 | store: me,\r | |
200 | rootProperty: 'data',\r | |
201 | pageSize: me.getPageSize(),\r | |
202 | maxSize: me.getPurgePageCount(),\r | |
203 | listeners: {\r | |
204 | // Whenever PageMap gets cleared, it means we re no longer interested in \r | |
205 | // any outstanding page prefetches, so cancel tham all\r | |
206 | clear: me.onPageMapClear,\r | |
207 | scope: me\r | |
208 | }\r | |
209 | });\r | |
210 | \r | |
211 | // Allow view to veto prune if the old page is still in use by the view\r | |
212 | me.relayEvents(result, ['beforepageremove', 'pageadd', 'pageremove']);\r | |
213 | me.pageRequests = {};\r | |
214 | return result;\r | |
215 | },\r | |
216 | \r | |
217 | //<debug>\r | |
218 | add: function() {\r | |
219 | Ext.raise('add method may not be called on a buffered store - the store is a map of remote data');\r | |
220 | },\r | |
221 | \r | |
222 | insert: function() {\r | |
223 | Ext.raise('insert method may not be called on a buffered store - the store is a map of remote data');\r | |
224 | },\r | |
225 | //</debug>\r | |
226 | \r | |
227 | removeAll: function(silent) {\r | |
228 | var me = this,\r | |
229 | data = me.getData();\r | |
230 | \r | |
231 | if (data) {\r | |
232 | if (silent) {\r | |
233 | me.suspendEvent('clear');\r | |
234 | }\r | |
235 | data.clear();\r | |
236 | if (silent) {\r | |
237 | me.resumeEvent('clear');\r | |
238 | }\r | |
239 | } \r | |
240 | },\r | |
241 | \r | |
242 | flushLoad: function() {\r | |
243 | var me = this,\r | |
244 | options = me.pendingLoadOptions;\r | |
245 | \r | |
246 | // If it gets called programatically, the listener will need cancelling\r | |
247 | me.clearLoadTask();\r | |
248 | if (!options) {\r | |
249 | return;\r | |
250 | }\r | |
251 | \r | |
252 | // Buffered stores, a load operation means kick off a clean load from page 1\r | |
253 | me.getData().clear();\r | |
254 | options.page = 1;\r | |
255 | options.start = 0;\r | |
256 | options.limit = me.getViewSize() || me.getDefaultViewSize();\r | |
257 | \r | |
258 | // If we're prefetching, the arguments on the callback for getting the range is different\r | |
259 | // So we indicate that we need to fire a special "load" style callback\r | |
260 | options.loadCallback = options.callback;\r | |
261 | \r | |
262 | // options might be chained, with callback on a prototype; delete won't clear it.\r | |
263 | options.callback = null;\r | |
264 | return me.loadToPrefetch(options);\r | |
265 | },\r | |
266 | \r | |
267 | reload: function(options) {\r | |
268 | var me = this,\r | |
269 | data = me.getData(),\r | |
270 | // If we don't have a known totalCount, use a huge value\r | |
271 | lastTotal = Number.MAX_VALUE,\r | |
272 | startIdx, endIdx, startPage, endPage,\r | |
273 | i, waitForReload, bufferZone, records;\r | |
274 | \r | |
275 | if (!options) {\r | |
276 | options = {};\r | |
277 | }\r | |
278 | \r | |
279 | // Prevent re-entering the load process if we are already in a wait state for a batch of pages.\r | |
280 | if (me.loading || me.fireEvent('beforeload', me, options) === false) {\r | |
281 | return;\r | |
282 | }\r | |
283 | \r | |
284 | waitForReload = function() {\r | |
285 | var newCount = me.totalCount,\r | |
286 | oldRequestSize = endIdx - startIdx;\r | |
287 | \r | |
288 | // If the dataset has now shrunk leaving the calculated request zone unavailable,\r | |
289 | // re-evaluate the request zone. Start as close to the end as possible.\r | |
290 | if (endIdx >= newCount) {\r | |
291 | endIdx = newCount - 1;\r | |
292 | startIdx = Math.max(endIdx - oldRequestSize, 0);\r | |
293 | }\r | |
294 | if (me.rangeCached(startIdx, Math.min(endIdx, me.totalCount))) {\r | |
295 | me.loading = false;\r | |
296 | data.un('pageadd', waitForReload);\r | |
297 | records = data.getRange(startIdx, endIdx + 1);\r | |
298 | me.fireEvent('load', me, records, true);\r | |
299 | me.fireEvent('refresh', me);\r | |
300 | }\r | |
301 | };\r | |
302 | bufferZone = Math.ceil((me.getLeadingBufferZone() + me.getTrailingBufferZone()) / 2);\r | |
303 | \r | |
304 | // Decide what reload means.\r | |
305 | // If the View was configured preserveScrollOnReload, then it will\r | |
306 | // inject that setting here. This means that reload means\r | |
307 | // load the last requested range.\r | |
308 | if (me.lastRequestStart && me.preserveScrollOnReload) {\r | |
309 | startIdx = me.lastRequestStart;\r | |
310 | endIdx = me.lastRequestEnd;\r | |
311 | lastTotal = me.getTotalCount();\r | |
312 | }\r | |
313 | // Otherwise, reload means start from page 1\r | |
314 | else {\r | |
315 | startIdx = options.start || 0;\r | |
316 | endIdx = startIdx + (options.count || me.getPageSize()) - 1;\r | |
317 | }\r | |
318 | \r | |
319 | // Clear page cache\r | |
320 | data.clear(true);\r | |
321 | \r | |
322 | // So that prefetchPage does not consider the store to be fully loaded if the local count is equal to the total count\r | |
323 | delete me.totalCount;\r | |
324 | \r | |
325 | // Calculate a page range which encompasses the Store's loaded range plus both buffer zones\r | |
326 | startIdx = Math.max(startIdx - bufferZone, 0);\r | |
327 | endIdx = Math.min(endIdx + bufferZone, lastTotal);\r | |
328 | startPage = me.getPageFromRecordIndex(startIdx);\r | |
329 | endPage = me.getPageFromRecordIndex(endIdx);\r | |
330 | \r | |
331 | me.loading = true;\r | |
332 | options.waitForReload = waitForReload;\r | |
333 | \r | |
334 | // Wait for the requested range to become available in the page map\r | |
335 | // Load the range as soon as the whole range is available\r | |
336 | data.on('pageadd', waitForReload);\r | |
337 | \r | |
338 | // Recache the page range which encapsulates our visible records\r | |
339 | for (i = startPage; i <= endPage; i++) {\r | |
340 | me.prefetchPage(i, options);\r | |
341 | }\r | |
342 | },\r | |
343 | \r | |
344 | filter: function() {\r | |
345 | //<debug>\r | |
346 | if (!this.getRemoteFilter()) {\r | |
347 | Ext.raise('Local filtering may not be used on a buffered store - the store is a map of remote data');\r | |
348 | }\r | |
349 | //</debug>\r | |
350 | \r | |
351 | // Remote filtering forces a load. load clears the store's contents.\r | |
352 | this.callParent(arguments);\r | |
353 | },\r | |
354 | \r | |
355 | filterBy: function(fn, scope) {\r | |
356 | //<debug>\r | |
357 | Ext.raise('Local filtering may not be used on a buffered store - the store is a map of remote data');\r | |
358 | //</debug>\r | |
359 | },\r | |
360 | \r | |
361 | loadData: function(data, append) {\r | |
362 | //<debug>\r | |
363 | Ext.raise('LoadData may not be used on a buffered store - the store is a map of remote data');\r | |
364 | //</debug>\r | |
365 | },\r | |
366 | \r | |
367 | loadPage: function(page, options) {\r | |
368 | var me = this;\r | |
369 | options = options || {};\r | |
370 | \r | |
371 | options.page = me.currentPage = page;\r | |
372 | options.start = (page - 1) * me.getPageSize();\r | |
373 | options.limit = me.getViewSize() || me.getDefaultViewSize();\r | |
374 | options.loadCallback = options.callback;\r | |
375 | \r | |
376 | // options might be chained, with callback on a prototype; delete won't clear it.\r | |
377 | options.callback = null;\r | |
378 | return me.loadToPrefetch(options);\r | |
379 | },\r | |
380 | \r | |
381 | clearData: function(isLoad) {\r | |
382 | var me = this,\r | |
383 | data = me.getData();\r | |
384 | \r | |
385 | if (data) {\r | |
386 | data.clear();\r | |
387 | }\r | |
388 | },\r | |
389 | \r | |
390 | /**\r | |
391 | * @private\r | |
392 | * A BufferedStore always reports that it contains the full dataset.\r | |
393 | * The number of records that happen to be cached at any one time is never useful.\r | |
394 | */\r | |
395 | getCount: function() {\r | |
396 | return this.totalCount || 0;\r | |
397 | },\r | |
398 | \r | |
399 | getRange: function(start, end, options) {\r | |
400 | var me = this,\r | |
401 | maxIndex = me.totalCount - 1,\r | |
402 | lastRequestStart = me.lastRequestStart,\r | |
403 | result = [],\r | |
404 | data = me.getData(),\r | |
405 | pageAddHandler,\r | |
406 | requiredStart, requiredEnd,\r | |
407 | requiredStartPage, requiredEndPage;\r | |
408 | \r | |
409 | options = Ext.apply({\r | |
410 | prefetchStart: start,\r | |
411 | prefetchEnd: end\r | |
412 | }, options);\r | |
413 | \r | |
414 | // Sanity check end point to be within dataset range\r | |
415 | end = (end >= me.totalCount) ? maxIndex : end;\r | |
416 | \r | |
417 | // We must wait for a slightly wider range to be cached.\r | |
418 | // This is to allow grouping features to peek at the two surrounding records\r | |
419 | // when rendering a *range* of records to see whether the start of the range\r | |
420 | // really is a group start and the end of the range really is a group end.\r | |
421 | requiredStart = start === 0 ? 0 : start - 1;\r | |
422 | requiredEnd = end === maxIndex ? end : end + 1;\r | |
423 | \r | |
424 | // Keep track of range we are being asked for so we can track direction of movement through the dataset\r | |
425 | me.lastRequestStart = start;\r | |
426 | me.lastRequestEnd = end;\r | |
427 | \r | |
428 | // If data request can be satisfied from the page cache\r | |
429 | if (me.rangeCached(requiredStart, requiredEnd)) {\r | |
430 | me.onRangeAvailable(options);\r | |
431 | result = data.getRange(start, end + 1);\r | |
432 | }\r | |
433 | // At least some of the requested range needs loading from server\r | |
434 | else {\r | |
435 | // Private event used by the LoadMask class to perform masking when the range required for rendering is not found in the cache\r | |
436 | me.fireEvent('cachemiss', me, start, end);\r | |
437 | \r | |
438 | requiredStartPage = me.getPageFromRecordIndex(requiredStart);\r | |
439 | requiredEndPage = me.getPageFromRecordIndex(requiredEnd);\r | |
440 | \r | |
441 | // Add a pageadd listener, and as soon as the requested range is loaded, call onRangeAvailable to call the callback.\r | |
442 | pageAddHandler = function(pageMap, page, records) {\r | |
443 | if (page >= requiredStartPage && page <= requiredEndPage && me.rangeCached(requiredStart, requiredEnd)) {\r | |
444 | // Private event used by the LoadMask class to unmask when the range required for rendering has been loaded into the cache\r | |
445 | me.fireEvent('cachefilled', me, start, end);\r | |
446 | data.un('pageadd', pageAddHandler);\r | |
447 | me.onRangeAvailable(options);\r | |
448 | }\r | |
449 | };\r | |
450 | data.on('pageadd', pageAddHandler);\r | |
451 | \r | |
452 | // Prioritize the request for the *exact range that the UI is asking for*.\r | |
453 | // When a page request is in flight, it will not be requested again by checking the me.pageRequests hash,\r | |
454 | // so the request after this will only request the *remaining* unrequested pages .\r | |
455 | me.prefetchRange(start, end);\r | |
456 | \r | |
457 | }\r | |
458 | // Load the pages around the requested range required by the leadingBufferZone and trailingBufferZone.\r | |
459 | me.primeCache(start, end, start < lastRequestStart ? -1 : 1);\r | |
460 | \r | |
461 | return result;\r | |
462 | },\r | |
463 | \r | |
464 | /**\r | |
465 | * Get the Record with the specified id.\r | |
466 | *\r | |
467 | * This method is not affected by filtering, lookup will be performed from all records\r | |
468 | * inside the store, filtered or not.\r | |
469 | *\r | |
470 | * @param {Mixed} id The id of the Record to find.\r | |
471 | * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.\r | |
472 | */\r | |
473 | getById: function(id) {\r | |
474 | var result = this.data.findBy(function(record) {\r | |
475 | return record.getId() === id;\r | |
476 | });\r | |
477 | return result;\r | |
478 | },\r | |
479 | \r | |
480 | /**\r | |
481 | * @inheritdoc\r | |
482 | */\r | |
483 | getAt: function(index) {\r | |
484 | var data = this.getData();\r | |
485 | \r | |
486 | if (data.hasRange(index, index)) {\r | |
487 | return data.getAt(index);\r | |
488 | }\r | |
489 | },\r | |
490 | \r | |
491 | /**\r | |
492 | * @private\r | |
493 | * Get the Record with the specified internalId.\r | |
494 | *\r | |
495 | * This method is not effected by filtering, lookup will be performed from all records\r | |
496 | * inside the store, filtered or not.\r | |
497 | *\r | |
498 | * @param {Mixed} internalId The id of the Record to find.\r | |
499 | * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.\r | |
500 | */\r | |
501 | getByInternalId: function(internalId) {\r | |
502 | return this.data.getByInternalId(internalId);\r | |
503 | },\r | |
504 | \r | |
505 | // Inherit docs\r | |
506 | contains: function(record) {\r | |
507 | return this.indexOf(record) > -1;\r | |
508 | },\r | |
509 | \r | |
510 | /**\r | |
511 | * Get the index of the record within the store.\r | |
512 | *\r | |
513 | * When store is filtered, records outside of filter will not be found.\r | |
514 | *\r | |
515 | * @param {Ext.data.Model} record The Ext.data.Model object to find.\r | |
516 | * @return {Number} The index of the passed Record. Returns -1 if not found.\r | |
517 | */\r | |
518 | indexOf: function(record) {\r | |
519 | return this.getData().indexOf(record);\r | |
520 | },\r | |
521 | \r | |
522 | /**\r | |
523 | * Get the index within the store of the Record with the passed id.\r | |
524 | *\r | |
525 | * Like #indexOf, this method is effected by filtering.\r | |
526 | *\r | |
527 | * @param {String} id The id of the Record to find.\r | |
528 | * @return {Number} The index of the Record. Returns -1 if not found.\r | |
529 | */\r | |
530 | indexOfId: function(id) {\r | |
531 | return this.indexOf(this.getById(id));\r | |
532 | },\r | |
533 | \r | |
534 | group: function(grouper, direction) {\r | |
535 | var me = this,\r | |
536 | oldGrouper;\r | |
537 | \r | |
538 | if (grouper && typeof grouper === 'string') {\r | |
539 | oldGrouper = me.grouper;\r | |
540 | \r | |
541 | if (!oldGrouper) {\r | |
542 | me.grouper = new Ext.util.Grouper({\r | |
543 | property : grouper,\r | |
544 | direction: direction || 'ASC',\r | |
545 | root: 'data'\r | |
546 | });\r | |
547 | } else if (direction === undefined) {\r | |
548 | oldGrouper.toggle();\r | |
549 | } else {\r | |
550 | oldGrouper.setDirection(direction);\r | |
551 | }\r | |
552 | } else {\r | |
553 | me.grouper = grouper ? me.getSorters().decodeSorter(grouper, 'Ext.util.Grouper') : null;\r | |
554 | }\r | |
555 | \r | |
556 | me.getData().clear();\r | |
557 | me.loadPage(1, {\r | |
558 | callback: function() {\r | |
559 | me.fireEvent('groupchange', me, me.getGrouper());\r | |
560 | }\r | |
561 | });\r | |
562 | },\r | |
563 | \r | |
564 | /**\r | |
565 | * Determines the page from a record index\r | |
566 | * @param {Number} index The record index\r | |
567 | * @return {Number} The page the record belongs to\r | |
568 | */\r | |
569 | getPageFromRecordIndex: function(index) {\r | |
570 | return Math.floor(index / this.getPageSize()) + 1;\r | |
571 | },\r | |
572 | \r | |
573 | calculatePageCacheSize: function(rangeSizeRequested) {\r | |
574 | var me = this,\r | |
575 | purgePageCount = me.getPurgePageCount();\r | |
576 | \r | |
577 | // Calculate the number of pages that the cache will keep before purging as follows:\r | |
578 | // TWO full rendering zones (in case of rapid teleporting by dragging the scroller) plus configured purgePageCount.\r | |
579 | // Ensure we never reduce the count. It always uses the largest requested block as the basis for the calculated size.\r | |
580 | return purgePageCount ? Math.max(me.getData().getMaxSize() || 0, Math.ceil((rangeSizeRequested + me.getTrailingBufferZone() + me.getLeadingBufferZone()) / me.getPageSize()) * 2 + purgePageCount) : 0;\r | |
581 | },\r | |
582 | \r | |
583 | loadToPrefetch: function(options) {\r | |
584 | var me = this,\r | |
585 | prefetchOptions = options,\r | |
586 | i,\r | |
587 | records,\r | |
588 | dataSetSize,\r | |
589 | \r | |
590 | // Get the requested record index range in the dataset\r | |
591 | startIdx = options.start,\r | |
592 | endIdx = options.start + options.limit - 1,\r | |
593 | rangeSizeRequested = (me.getViewSize() || options.limit),\r | |
594 | \r | |
595 | // The end index to load into the store's live record collection\r | |
596 | loadEndIdx = Math.min(endIdx, options.start + rangeSizeRequested - 1),\r | |
597 | \r | |
598 | // Calculate a page range which encompasses the requested range plus both buffer zones.\r | |
599 | // The endPage will be adjusted to be in the dataset size range as soon as the first data block returns.\r | |
600 | startPage = me.getPageFromRecordIndex(Math.max(startIdx - me.getTrailingBufferZone(), 0)),\r | |
601 | endPage = me.getPageFromRecordIndex(endIdx + me.getLeadingBufferZone()),\r | |
602 | \r | |
603 | data = me.getData(),\r | |
604 | callbackFn = function () {\r | |
605 | // See comments in load() for why we need this.\r | |
606 | records = records || [];\r | |
607 | \r | |
608 | if (options.loadCallback) {\r | |
609 | options.loadCallback.call(options.scope || me, records, operation, true);\r | |
610 | }\r | |
611 | \r | |
612 | if (options.callback) {\r | |
613 | options.callback.call(options.scope || me, records, startIdx || 0, endIdx || 0, options);\r | |
614 | }\r | |
615 | },\r | |
616 | fireEventsFn = function () {\r | |
617 | me.fireEvent('datachanged', me);\r | |
618 | me.fireEvent('refresh', me);\r | |
619 | me.fireEvent('load', me, records, true);\r | |
620 | },\r | |
621 | // Wait for the viewable range to be available.\r | |
622 | waitForRequestedRange = function() {\r | |
623 | if (me.rangeCached(startIdx, loadEndIdx)) {\r | |
624 | me.loading = false;\r | |
625 | records = data.getRange(startIdx, loadEndIdx + 1);\r | |
626 | data.un('pageadd', waitForRequestedRange);\r | |
627 | \r | |
628 | // If there is a listener for guaranteedrange then fire that event\r | |
629 | if (me.hasListeners.guaranteedrange) {\r | |
630 | me.guaranteeRange(startIdx, loadEndIdx, options.callback, options.scope);\r | |
631 | }\r | |
632 | \r | |
633 | callbackFn();\r | |
634 | fireEventsFn();\r | |
635 | }\r | |
636 | }, operation;\r | |
637 | \r | |
638 | //<debug>\r | |
639 | if (isNaN(me.pageSize) || !me.pageSize) {\r | |
640 | Ext.raise('Buffered store configured without a pageSize', me);\r | |
641 | }\r | |
642 | //</debug>\r | |
643 | \r | |
644 | // Ensure that the purgePageCount allows enough pages to be kept cached to cover the\r | |
645 | // requested range. If the pageSize is very small we might need a lot of pages.\r | |
646 | data.setMaxSize(me.calculatePageCacheSize(rangeSizeRequested));\r | |
647 | \r | |
648 | if (me.fireEvent('beforeload', me, options) !== false) {\r | |
649 | \r | |
650 | // So that prefetchPage does not consider the store to be fully loaded if the local count is equal to the total count\r | |
651 | delete me.totalCount;\r | |
652 | \r | |
653 | me.loading = true;\r | |
654 | \r | |
655 | // Any configured callback is handled in waitForRequestedRange above.\r | |
656 | // It should not be processed by onProxyPrefetch.\r | |
657 | if (options.callback) {\r | |
658 | prefetchOptions = Ext.apply({}, options);\r | |
659 | delete prefetchOptions.callback;\r | |
660 | }\r | |
661 | \r | |
662 | // Load the first page in the range, which will give us the initial total count.\r | |
663 | // Once it is loaded, go ahead and prefetch any subsequent pages, if necessary.\r | |
664 | // The prefetchPage has a check to prevent us loading more than the totalCount,\r | |
665 | // so we don't want to blindly load up <n> pages where it isn't required.\r | |
666 | me.on('prefetch', function(store, records, successful, op) {\r | |
667 | // Capture operation here so it can be used in the loadCallback above\r | |
668 | operation = op;\r | |
669 | if (successful) {\r | |
670 | // If there is data in the dataset, we can go ahead and add the pageadd listener which waits for the visible range\r | |
671 | // and we can also issue the requests to fill the surrounding buffer zones.\r | |
672 | if ((dataSetSize = me.getTotalCount())) {\r | |
673 | \r | |
674 | // Wait for the requested range to become available in the page map\r | |
675 | data.on('pageadd', waitForRequestedRange);\r | |
676 | \r | |
677 | // As soon as we have the size of the dataset, ensure we are not waiting for more than can ever arrive,\r | |
678 | loadEndIdx = Math.min(loadEndIdx, dataSetSize - 1);\r | |
679 | \r | |
680 | // And make sure we never ask for pages beyond the end of the dataset.\r | |
681 | endPage = me.getPageFromRecordIndex(Math.min(loadEndIdx + me.getLeadingBufferZone(), dataSetSize - 1));\r | |
682 | \r | |
683 | for (i = startPage + 1; i <= endPage; ++i) {\r | |
684 | me.prefetchPage(i, prefetchOptions);\r | |
685 | }\r | |
686 | } else {\r | |
687 | callbackFn();\r | |
688 | fireEventsFn();\r | |
689 | }\r | |
690 | }\r | |
691 | // Unsuccessful prefetch: fire a load event with success false.\r | |
692 | else {\r | |
693 | me.loading = false;\r | |
694 | callbackFn();\r | |
695 | me.fireEvent('load', me, records, false);\r | |
696 | }\r | |
697 | }, null, {single: true});\r | |
698 | \r | |
699 | me.prefetchPage(startPage, prefetchOptions);\r | |
700 | }\r | |
701 | },\r | |
702 | \r | |
703 | // Buffering\r | |
704 | /**\r | |
705 | * Prefetches data into the store using its configured {@link #proxy}.\r | |
706 | * @param {Object} options (Optional) config object, passed into the Ext.data.operation.Operation object before loading.\r | |
707 | * See {@link #method-load}\r | |
708 | */\r | |
709 | prefetch: function(options) {\r | |
710 | var me = this,\r | |
711 | pageSize = me.getPageSize(),\r | |
712 | data = me.getData(),\r | |
713 | operation,\r | |
714 | existingPageRequest;\r | |
715 | \r | |
716 | // Check pageSize has not been tampered with. That would break page caching\r | |
717 | if (pageSize) {\r | |
718 | if (me.lastPageSize && pageSize != me.lastPageSize) {\r | |
719 | Ext.raise("pageSize cannot be dynamically altered");\r | |
720 | }\r | |
721 | if (!data.getPageSize()) {\r | |
722 | data.setPageSize(pageSize);\r | |
723 | }\r | |
724 | }\r | |
725 | \r | |
726 | // Allow first prefetch call to imply the required page size.\r | |
727 | else {\r | |
728 | me.pageSize = data.setPageSize(pageSize = options.limit);\r | |
729 | }\r | |
730 | \r | |
731 | // So that we can check for tampering next time through\r | |
732 | me.lastPageSize = pageSize;\r | |
733 | \r | |
734 | // Always get whole pages.\r | |
735 | if (!options.page) {\r | |
736 | options.page = me.getPageFromRecordIndex(options.start);\r | |
737 | options.start = (options.page - 1) * pageSize;\r | |
738 | options.limit = Math.ceil(options.limit / pageSize) * pageSize;\r | |
739 | }\r | |
740 | \r | |
741 | // Currently not requesting this page, or the request was for the last\r | |
742 | // generation of the data cache (clearing it changes generations)\r | |
743 | // then request it...\r | |
744 | existingPageRequest = me.pageRequests[options.page];\r | |
745 | if (!existingPageRequest || existingPageRequest.getOperation().pageMapGeneration !== data.pageMapGeneration) {\r | |
746 | // Copy options into a new object so as not to mutate passed in objects\r | |
747 | options = Ext.apply({\r | |
748 | action : 'read',\r | |
749 | filters: me.getFilters().items,\r | |
750 | sorters: me.getSorters().items,\r | |
751 | grouper: me.getGrouper(),\r | |
752 | internalCallback: me.onProxyPrefetch,\r | |
753 | internalScope: me\r | |
754 | }, options);\r | |
755 | \r | |
756 | operation = me.createOperation('read', options);\r | |
757 | \r | |
758 | // Generation # of the page map to which the requested records belong.\r | |
759 | // If page map is cleared while this request is in flight, the pageMapGeneration will increment and the payload will be rejected\r | |
760 | operation.pageMapGeneration = data.pageMapGeneration;\r | |
761 | \r | |
762 | if (me.fireEvent('beforeprefetch', me, operation) !== false) {\r | |
763 | me.pageRequests[options.page] = operation.execute();\r | |
764 | if (me.getProxy().isSynchronous) {\r | |
765 | delete me.pageRequests[options.page];\r | |
766 | }\r | |
767 | }\r | |
768 | }\r | |
769 | \r | |
770 | return me;\r | |
771 | },\r | |
772 | \r | |
773 | /**\r | |
774 | * @private\r | |
775 | * Cancels all pending prefetch requests.\r | |
776 | *\r | |
777 | * This is called when the page map is cleared.\r | |
778 | *\r | |
779 | * Any requests which still make it through will be for the previous pageMapGeneration\r | |
780 | * (pageMapGeneration is incremented upon clear), and so will be rejected upon arrival.\r | |
781 | */\r | |
782 | onPageMapClear: function() {\r | |
783 | var me = this,\r | |
784 | loadingFlag = me.wasLoading,\r | |
785 | reqs = me.pageRequests,\r | |
786 | data = me.getData(),\r | |
787 | page;\r | |
788 | \r | |
789 | // If any requests return, we no longer respond to them.\r | |
790 | data.clearListeners();\r | |
791 | \r | |
792 | // replace the listeners we need.\r | |
793 | data.on('clear', me.onPageMapClear, me);\r | |
794 | me.relayEvents(data, ['beforepageremove', 'pageadd', 'pageremove']);\r | |
795 | \r | |
796 | // If the page cache gets cleared it's because a full reload is in progress.\r | |
797 | // Setting the loading flag prevents linked Views from displaying the empty text\r | |
798 | // during a load... we don't know whether ther dataset is empty or not.\r | |
799 | me.loading = true;\r | |
800 | me.totalCount = 0;\r | |
801 | \r | |
802 | // Abort all outstanding requests.\r | |
803 | // onProxyPrefetch will reject them as being for the previous data generation\r | |
804 | // anyway, if they do return.\r | |
805 | // because of the pageMapGeneration mismatch.\r | |
806 | for (page in reqs) {\r | |
807 | if (reqs.hasOwnProperty(page)) {\r | |
808 | reqs[page].getOperation().abort();\r | |
809 | }\r | |
810 | }\r | |
811 | \r | |
812 | // This will update any views. \r | |
813 | me.fireEvent('clear', me);\r | |
814 | \r | |
815 | // Restore loading flag. The beforeload event could still veto the process.\r | |
816 | // The flag does not get set for real until we pass the beforeload event.\r | |
817 | me.loading = loadingFlag;\r | |
818 | },\r | |
819 | \r | |
820 | /**\r | |
821 | * Prefetches a page of data.\r | |
822 | * @param {Number} page The page to prefetch\r | |
823 | * @param {Object} options (Optional) config object, passed into the Ext.data.operation.Operation object before loading.\r | |
824 | * See {@link #method-load}\r | |
825 | */\r | |
826 | prefetchPage: function(page, options) {\r | |
827 | var me = this,\r | |
828 | pageSize = me.getPageSize(),\r | |
829 | start = (page - 1) * pageSize,\r | |
830 | total = me.totalCount;\r | |
831 | \r | |
832 | // No more data to prefetch.\r | |
833 | if (total !== undefined && me.data.getCount() === total) {\r | |
834 | return;\r | |
835 | }\r | |
836 | \r | |
837 | // Copy options into a new object so as not to mutate passed in objects\r | |
838 | me.prefetch(Ext.applyIf({\r | |
839 | page : page,\r | |
840 | start : start,\r | |
841 | limit : pageSize\r | |
842 | }, options));\r | |
843 | },\r | |
844 | \r | |
845 | /**\r | |
846 | * Called after the configured proxy completes a prefetch operation.\r | |
847 | * @private\r | |
848 | * @param {Ext.data.operation.Operation} operation The operation that completed\r | |
849 | */\r | |
850 | onProxyPrefetch: function(operation) {\r | |
851 | if (this.destroyed) {\r | |
852 | return;\r | |
853 | }\r | |
854 | \r | |
855 | var me = this,\r | |
856 | resultSet = operation.getResultSet(),\r | |
857 | records = operation.getRecords(),\r | |
858 | successful = operation.wasSuccessful(),\r | |
859 | page = operation.getPage(),\r | |
860 | waitForReload = operation.waitForReload,\r | |
861 | oldTotal = me.totalCount,\r | |
862 | requests = me.pageRequests,\r | |
863 | key, op;\r | |
864 | \r | |
865 | // Only cache the data if the operation was invoked for the current pageMapGeneration.\r | |
866 | // If the pageMapGeneration has changed since the request was fired off, it will have been cancelled.\r | |
867 | if (operation.pageMapGeneration === me.getData().pageMapGeneration) {\r | |
868 | \r | |
869 | if (resultSet) {\r | |
870 | me.totalCount = resultSet.getTotal();\r | |
871 | if (me.totalCount !== oldTotal) {\r | |
872 | me.fireEvent('totalcountchange', me.totalCount);\r | |
873 | }\r | |
874 | }\r | |
875 | \r | |
876 | // Remove the loaded page from the outstanding pages hash\r | |
877 | if (page !== undefined) {\r | |
878 | delete me.pageRequests[page];\r | |
879 | }\r | |
880 | \r | |
881 | // Prefetch is broadcast before the page is cached\r | |
882 | me.loading = false;\r | |
883 | me.fireEvent('prefetch', me, records, successful, operation);\r | |
884 | \r | |
885 | // Add the page into the page map.\r | |
886 | // pageadd event may trigger the onRangeAvailable\r | |
887 | if (successful) {\r | |
888 | if (me.totalCount === 0) {\r | |
889 | if (waitForReload) {\r | |
890 | for (key in requests) {\r | |
891 | op = requests[key].getOperation();\r | |
892 | // Created in the same batch, clear the waitForReload so this\r | |
893 | // won't be run again\r | |
894 | if (op.waitForReload === waitForReload) {\r | |
895 | delete op.waitForReload;\r | |
896 | }\r | |
897 | }\r | |
898 | me.getData().un('pageadd', waitForReload);\r | |
899 | me.fireEvent('load', me, [], true);\r | |
900 | me.fireEvent('refresh', me);\r | |
901 | }\r | |
902 | } else {\r | |
903 | me.cachePage(records, operation.getPage());\r | |
904 | }\r | |
905 | }\r | |
906 | \r | |
907 | //this is a callback that would have been passed to the 'read' function and is optional\r | |
908 | Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);\r | |
909 | }\r | |
910 | },\r | |
911 | \r | |
912 | /**\r | |
913 | * Caches the records in the prefetch and stripes them with their server-side\r | |
914 | * index.\r | |
915 | * @private\r | |
916 | * @param {Ext.data.Model[]} records The records to cache\r | |
917 | * @param {Ext.data.operation.Operation} page The associated operation\r | |
918 | */\r | |
919 | cachePage: function(records, page) {\r | |
920 | var me = this,\r | |
921 | len = records.length, i;\r | |
922 | \r | |
923 | if (!Ext.isDefined(me.totalCount)) {\r | |
924 | me.totalCount = records.length;\r | |
925 | me.fireEvent('totalcountchange', me.totalCount);\r | |
926 | }\r | |
927 | \r | |
928 | // Add the fetched page into the pageCache\r | |
929 | for (i = 0; i < len; i++) {\r | |
930 | records[i].join(me);\r | |
931 | }\r | |
932 | me.getData().addPage(page, records);\r | |
933 | },\r | |
934 | \r | |
935 | /**\r | |
936 | * Determines if the passed range is available in the page cache.\r | |
937 | * @private\r | |
938 | * @param {Number} start The start index\r | |
939 | * @param {Number} end The end index in the range\r | |
940 | */\r | |
941 | rangeCached: function(start, end) {\r | |
942 | return this.getData().hasRange(start, end);\r | |
943 | },\r | |
944 | \r | |
945 | /**\r | |
946 | * Determines if the passed page is available in the page cache.\r | |
947 | * @private\r | |
948 | * @param {Number} page The page to find in the page cache.\r | |
949 | */\r | |
950 | pageCached: function(page) {\r | |
951 | return this.getData().hasPage(page);\r | |
952 | },\r | |
953 | \r | |
954 | /**\r | |
955 | * Determines if a request for a page is currently running\r | |
956 | * @private\r | |
957 | * @param {Number} page The page to check for\r | |
958 | */\r | |
959 | pagePending: function(page) {\r | |
960 | return !!this.pageRequests[page];\r | |
961 | },\r | |
962 | \r | |
963 | /**\r | |
964 | * Determines if the passed range is available in the page cache.\r | |
965 | * @private\r | |
966 | * @deprecated 4.1.0 use {@link #rangeCached} instead\r | |
967 | * @param {Number} start The start index\r | |
968 | * @param {Number} end The end index in the range\r | |
969 | * @return {Boolean}\r | |
970 | */\r | |
971 | rangeSatisfied: function(start, end) {\r | |
972 | return this.rangeCached(start, end);\r | |
973 | },\r | |
974 | \r | |
975 | /**\r | |
976 | * Handles the availability of a requested range that was not previously available\r | |
977 | * @private\r | |
978 | */\r | |
979 | onRangeAvailable: function(options) {\r | |
980 | var me = this,\r | |
981 | totalCount = me.getTotalCount(),\r | |
982 | start = options.prefetchStart,\r | |
983 | end = (options.prefetchEnd > totalCount - 1) ? totalCount - 1 : options.prefetchEnd,\r | |
984 | range;\r | |
985 | \r | |
986 | end = Math.max(0, end);\r | |
987 | \r | |
988 | //<debug>\r | |
989 | if (start > end) {\r | |
990 | Ext.log({\r | |
991 | level: 'warn',\r | |
992 | msg: 'Start (' + start + ') was greater than end (' + end +\r | |
993 | ') for the range of records requested (' + start + '-' +\r | |
994 | options.prefetchEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')\r | |
995 | });\r | |
996 | }\r | |
997 | //</debug>\r | |
998 | \r | |
999 | range = me.getData().getRange(start, end + 1);\r | |
1000 | if (options.fireEvent !== false) {\r | |
1001 | me.fireEvent('guaranteedrange', range, start, end, options);\r | |
1002 | }\r | |
1003 | if (options.callback) {\r | |
1004 | options.callback.call(options.scope || me, range, start, end, options);\r | |
1005 | }\r | |
1006 | },\r | |
1007 | \r | |
1008 | /**\r | |
1009 | * Guarantee a specific range, this will load the store with a range (that\r | |
1010 | * must be the `pageSize` or smaller) and take care of any loading that may\r | |
1011 | * be necessary.\r | |
1012 | * @deprecated Use {@link #getRange}\r | |
1013 | */\r | |
1014 | guaranteeRange: function(start, end, callback, scope, options) {\r | |
1015 | options = Ext.apply({\r | |
1016 | callback: callback,\r | |
1017 | scope: scope\r | |
1018 | }, options);\r | |
1019 | this.getRange(start, end + 1, options);\r | |
1020 | },\r | |
1021 | \r | |
1022 | /**\r | |
1023 | * Ensures that the specified range of rows is present in the cache.\r | |
1024 | *\r | |
1025 | * Converts the row range to a page range and then only load pages which are not already\r | |
1026 | * present in the page cache.\r | |
1027 | */\r | |
1028 | prefetchRange: function(start, end) {\r | |
1029 | var me = this,\r | |
1030 | startPage, endPage, page,\r | |
1031 | data = me.getData();\r | |
1032 | \r | |
1033 | if (!me.rangeCached(start, end)) {\r | |
1034 | startPage = me.getPageFromRecordIndex(start);\r | |
1035 | endPage = me.getPageFromRecordIndex(end);\r | |
1036 | \r | |
1037 | // Ensure that the page cache's max size is correct.\r | |
1038 | // Our purgePageCount is the number of additional pages *outside of the required range* which\r | |
1039 | // may be kept in the cache. A purgePageCount of zero means unlimited.\r | |
1040 | data.setMaxSize(me.calculatePageCacheSize(end - start + 1));\r | |
1041 | \r | |
1042 | // We have the range, but ensure that we have a "buffer" of pages around it.\r | |
1043 | for (page = startPage; page <= endPage; page++) {\r | |
1044 | if (!me.pageCached(page)) {\r | |
1045 | me.prefetchPage(page);\r | |
1046 | }\r | |
1047 | }\r | |
1048 | }\r | |
1049 | },\r | |
1050 | \r | |
1051 | primeCache: function(start, end, direction) {\r | |
1052 | var me = this,\r | |
1053 | leadingBufferZone = me.getLeadingBufferZone(),\r | |
1054 | trailingBufferZone = me.getTrailingBufferZone(),\r | |
1055 | pageSize = me.getPageSize(),\r | |
1056 | totalCount = me.totalCount;\r | |
1057 | \r | |
1058 | // Scrolling up\r | |
1059 | if (direction === -1) {\r | |
1060 | start = Math.max(start - leadingBufferZone, 0);\r | |
1061 | end = Math.min(end + trailingBufferZone, totalCount - 1);\r | |
1062 | }\r | |
1063 | // Scrolling down\r | |
1064 | else if (direction === 1) {\r | |
1065 | start = Math.max(Math.min(start - trailingBufferZone, totalCount - pageSize), 0);\r | |
1066 | end = Math.min(end + leadingBufferZone, totalCount - 1);\r | |
1067 | }\r | |
1068 | // Teleporting\r | |
1069 | else {\r | |
1070 | start = Math.min(Math.max(Math.floor(start - ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - me.pageSize);\r | |
1071 | end = Math.min(Math.max(Math.ceil (end + ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - 1);\r | |
1072 | }\r | |
1073 | me.prefetchRange(start, end);\r | |
1074 | },\r | |
1075 | \r | |
1076 | sort: function(field, direction, mode) {\r | |
1077 | if (arguments.length === 0) {\r | |
1078 | this.clearAndLoad();\r | |
1079 | } else {\r | |
1080 | this.getSorters().addSort(field, direction, mode);\r | |
1081 | }\r | |
1082 | },\r | |
1083 | \r | |
1084 | onSorterEndUpdate: function() {\r | |
1085 | var me = this,\r | |
1086 | sorters = me.getSorters().getRange();\r | |
1087 | \r | |
1088 | // Only load or sort if there are sorters\r | |
1089 | if (sorters.length) {\r | |
1090 | me.fireEvent('beforesort', me, sorters);\r | |
1091 | me.clearAndLoad({\r | |
1092 | callback: function() {\r | |
1093 | me.fireEvent('sort', me, sorters);\r | |
1094 | }\r | |
1095 | });\r | |
1096 | } else {\r | |
1097 | // Sort event must fire when sorters collection is updated to empty.\r | |
1098 | me.fireEvent('sort', me, sorters);\r | |
1099 | }\r | |
1100 | },\r | |
1101 | \r | |
1102 | clearAndLoad: function (options) {\r | |
1103 | this.getData().clear();\r | |
1104 | this.loadPage(1, options);\r | |
1105 | },\r | |
1106 | \r | |
1107 | privates: {\r | |
1108 | isLast: function(record) {\r | |
1109 | return this.indexOf(record) === this.getTotalCount() - 1;\r | |
1110 | },\r | |
1111 | \r | |
1112 | isMoving: function () {\r | |
1113 | return false;\r | |
1114 | }\r | |
1115 | }\r | |
1116 | });\r |