/** * @class Ext.data.PageMap * @extends Ext.util.LruCache * Private class for use by only Store when configured `buffered: true`. * @private */Ext.define('Ext.data.PageMap', { extend: 'Ext.util.LruCache', config: { store: null, /** * @cfg {Number} pageSize * The size of pages in this map. */ pageSize: 0, /** * @cfg {String} rootProperty * The root property to use for aggregation, filtering and sorting. By default * this is `null` but when containing things like {@link Ext.data.Model records} * this config would likely be set to "data" so that property names are applied * to the fields of each record. */ rootProperty: '' }, // Maintain a generation counter, so that the Store can reject incoming pages destined for the previous generation clear: function(initial) { var me = this; me.pageMapGeneration = (me.pageMapGeneration || 0) + 1; // Map of internalId to recordIndex me.indexMap = {}; me.callParent([initial]); }, //<debug> updatePageSize: function(value, oldValue) { if (oldValue != null) { throw "pageMap page size may not be changed"; } }, //</debug> forEach: function(fn, scope) { var me = this, pageNumbers = Ext.Object.getKeys(me.map), pageCount = pageNumbers.length, pageSize = me.getPageSize(), i, j, pageNumber, page, len; for (i = 0; i < pageCount; i++) { pageNumbers[i] = +pageNumbers[i]; } Ext.Array.sort(pageNumbers, Ext.Array.numericSortFn); scope = scope || me; for (i = 0; i < pageCount; i++) { pageNumber = pageNumbers[i]; page = me.getPage(pageNumber); len = page.length; for (j = 0; j < len; j++) { if (fn.call(scope, page[j], (pageNumber - 1) * pageSize + j) === false) { return; } } } }, /** * Returns the first record in this page map which elicits a true return value from the * passed selection function. * * **IMPORTANT** * * **This can ONLY find records which happen to be cached in the page cache. This will be parts of the dataset around the currently * visible zone, or recently visited zones if the pages have not yet been purged from the cache.** * * **This CAN NOT find records which have not been loaded into the cache.** * * If full client side searching is required, do not use a buffered store, instead use a regular, fully loaded store and * use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} plugin to minimize DOM footprint. * @param {Function} fn The selection function to execute for each item. * @param {Mixed} fn.rec The record. * @param {Mixed} fn.index The index in the total dataset of the record. * @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to this PageMap. * @return {Object} The first record in this page map which returned true from the selection * function, or null if none was found. */ findBy: function(fn, scope) { var me = this, result = null; scope = scope || me; me.forEach(function(rec, index) { if (fn.call(scope, rec, index)) { result = rec; return false; } }); return result; }, /** * Returns the index *in the whole dataset* of the first record in this page map which elicits a true return value from the * passed selection function. * * **IMPORTANT** * * **This can ONLY find records which happen to be cached in the page cache. This will be parts of the dataset around the currently * visible zone, or recently visited zones if the pages have not yet been purged from the cache.** * * **This CAN NOT find records which have not been loaded into the cache.** * * If full client side searching is required, do not use a buffered store, instead use a regular, fully loaded store and * use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} plugin to minimize DOM footprint. * @param {Function} fn The selection function to execute for each item. * @param {Mixed} fn.rec The record. * @param {Mixed} fn.index The index in the total dataset of the record. * @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to this PageMap. * @return {Number} The index first record in this page map which returned true from the selection * function, or -1 if none was found. */ findIndexBy: function(fn, scope) { var me = this, result = -1; scope = scope || me; me.forEach(function(rec, index) { if (fn.call(scope, rec)) { result = index; return false; } }); return result; }, find: function (property, value, start, startsWith, endsWith, ignoreCase) { if (Ext.isEmpty(value, false)) { return null; } var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase), root = this.getRootProperty(); return this.findBy(function (item) { return item && regex.test((root ? item[root] : item)[property]); }, null, start); }, findIndex: function (property, value, start, startsWith, endsWith, ignoreCase) { if (Ext.isEmpty(value, false)) { return null; } var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase), root = this.getRootProperty(); return this.findIndexBy(function (item) { return item && regex.test((root ? item[root] : item)[property]); }, null, start); }, getPageFromRecordIndex: function(index) { return Math.floor(index / this.getPageSize()) + 1; }, addAll: function(records) { //<debug> if (this.getCount()) { Ext.raise('Cannot addAll to a non-empty PageMap'); } //</debug> this.addPage(1, records); }, addPage: function(pageNumber, records) { var me = this, pageSize = me.getPageSize(), lastPage = pageNumber + Math.floor((records.length - 1) / pageSize), storeIndex = (pageNumber - 1) * pageSize, indexMap = me.indexMap, page, i, len, startIdx; // Account for being handed a block of records spanning several pages. // This can happen when loading from a MemoryProxy before a viewSize has been determined. for (startIdx = 0; pageNumber <= lastPage; pageNumber++, startIdx += pageSize) { page = Ext.Array.slice(records, startIdx, startIdx + pageSize); // Maintain the indexMap so that we can implement indexOf(record) for (i = 0, len = page.length; i < len; i++) { indexMap[page[i].internalId] = storeIndex++; } me.add(pageNumber, page); me.fireEvent('pageadd', me, pageNumber, page); } }, getCount: function() { var result = this.callParent(); if (result) { result = (result - 1) * this.getPageSize() + this.last.value.length; } return result; }, getByInternalId: function(internalId) { var index = this.indexMap[internalId]; if (index != null) { return this.getAt(index); } }, indexOf: function(record) { var result = -1; if (record) { result = this.indexMap[record.internalId]; if (result == null) { result = -1; } } return result; }, insert: function() { //<debug> Ext.raise('insert operation not suppported into buffered Store'); //</debug> }, remove: function() { //<debug> Ext.raise('remove operation not suppported from buffered Store'); //</debug> }, removeAt: function() { //<debug> Ext.raise('removeAt operation not suppported from buffered Store'); //</debug> }, removeAtKey: function (page) { // Allow observers to veto var me = this, thePage = me.getPage(page), len, i, result; if (thePage) { if (me.fireEvent('beforepageremove', me, page, thePage) !== false) { len = thePage.length; for (i = 0; i < len; i++) { delete me.indexMap[thePage[i].internalId]; } result = me.callParent(arguments); me.fireEvent('pageremove', me, page, thePage); // Empty the page array *after* informing observers that the records have exited. thePage.length = 0; } } return result; }, getPage: function(pageNumber) { return this.get(pageNumber); }, hasRange: function(start, end) { var me = this, pageNumber = me.getPageFromRecordIndex(start), endPageNumber = me.getPageFromRecordIndex(end); for (; pageNumber <= endPageNumber; pageNumber++) { if (!me.hasPage(pageNumber)) { return false; } } // Check that the last page is filled enough to encapsulate the range. return (endPageNumber - 1) * me._pageSize + me.getPage(endPageNumber).length > end; }, hasPage: function(pageNumber) { // We must use this.get to trigger an access so that the page which is checked for presence is not eligible for pruning return !!this.get(pageNumber); }, peekPage: function(pageNumber) { return this.map[pageNumber]; }, getAt: function(index) { return this.getRange(index, index + 1)[0]; }, getRange: function(start, end) { // Store's backing Collection now uses EXCLUSIVE endIndex // So store will always pass the endIndex+1 end--; if (!this.hasRange(start, end)) { Ext.raise('PageMap asked for range which it does not have'); } var me = this, Array = Ext.Array, pageSize = me.getPageSize(), startPageNumber = me.getPageFromRecordIndex(start), endPageNumber = me.getPageFromRecordIndex(end), dataStart = (startPageNumber - 1) * pageSize, dataEnd = (endPageNumber * pageSize) - 1, pageNumber = startPageNumber, result = [], sliceBegin, sliceEnd, doSlice; for (; pageNumber <= endPageNumber; pageNumber++) { // First and last pages *may* need slicing to cut into the actual wanted records if (pageNumber === startPageNumber) { sliceBegin = start - dataStart; doSlice = sliceBegin > 0; } else { sliceBegin = 0; doSlice = false; } if (pageNumber === endPageNumber) { sliceEnd = pageSize - (dataEnd - end); doSlice = doSlice || sliceEnd < pageSize; } // First and last pages will need slicing if (doSlice) { Array.push(result, Array.slice(me.getPage(pageNumber), sliceBegin, sliceEnd)); } else { Array.push(result, me.getPage(pageNumber)); } } return result; }});