/** * This type of store is a replacement for BufferedStore at least for Modern. The primary * use of this store is to create and manage "active ranges" of records. * * For example: * * var range = store.createActiveRange({ * begin: 100, * end: 200, * prefetch: true, // allow prefetching beyond range * callback: 'onRangeUpdate', * scope: this * }); * * // Navigate to a different range. This will causes pages to load and * // the onRangeUpdate method will be called as the load(s) progress. * // This can change the length or number of records spanned on each * // call. * // * range.goto(300, 400); * * onRangeUpdate: function (range, begin, end) { * // Called when records appear in the range... * // We can check if all things are loaded: * * // Or we can use range.records (sparsely populated) * } * * @since 6.5.0 */Ext.define('Ext.data.virtual.Store', { extend: 'Ext.data.ProxyStore', alias: 'store.virtual', requires: [ 'Ext.util.SorterCollection', 'Ext.util.FilterCollection', 'Ext.data.virtual.PageMap', 'Ext.data.virtual.Range' ], uses: [ 'Ext.data.virtual.Group' ], isVirtualStore: true, config: { data: null, totalCount: null, /** * @cfg {Number} leadingBufferZone * The number of records to fetch beyond the active range in the direction of * movement. If the range is advancing forward, the additional records are beyond * `end`. If advancing backwards, they are before `begin`. */ leadingBufferZone: 200, /** * @cfg {Number} trailingBufferZone * The number of records to fetch beyond the active trailing the direction of * movement. If the range is advancing forward, the additional records are before * `begin`. If advancing backwards, they are beyond `end`. */ trailingBufferZone: 50 }, /** * @cfg remoteSort * @inheritdoc */ remoteSort: true, /** * @cfg remoteFilter * @inheritdoc */ remoteFilter: true, /** * @cfg sortOnLoad * @inheritdoc */ sortOnLoad: false, /** * @cfg trackRemoved * @inheritdoc */ trackRemoved: false, constructor: function(config) { var me = this; me.sortByPage = me.sortByPage.bind(me); me.activeRanges = []; me.pageMap = new Ext.data.virtual.PageMap({ store: me }); me.callParent([ config ]); }, doDestroy: function() { this.pageMap.destroy(); this.callParent(); }, applyGrouper: function(grouper) { this.group(grouper); return this.grouper; }, //----------------------------------------------------------------------- /** * @method contains * @inheritdoc */ contains: function(record) { return this.indexOf(record) > -1; }, /** * Create a `Range` instance to access records by their index. * * @param {Object/Ext.data.virtual.Range} [config] * @return {Ext.data.virtual.Range} * @since 6.5.0 */ createActiveRange: function(config) { var range = Ext.apply({ leadingBufferZone: this.getLeadingBufferZone(), trailingBufferZone: this.getTrailingBufferZone(), store: this }, config); return new Ext.data.virtual.Range(range); }, /** * @method getAt * @inheritdoc */ getAt: function(index) { var page = this.pageMap.getPageOf(index, /* autoCreate= */false), ret; if (page && page.records) { // if (page is loaded) ret = page.records[index - page.begin]; } return ret || null; }, /** * Get the Record with the specified id. * * This method is not affected by filtering, lookup will be performed from all records * inside the store, filtered or not. * * @param {Mixed} id The id of the Record to find. * @return {Ext.data.Model} The Record with the passed id. Returns null if not found. */ getById: function(id) { return this.pageMap.byId[id] || null; }, getCount: function() { return this.totalCount || 0; }, getGrouper: function() { return this.grouper; }, getGroups: function() { var me = this, groups = me.groupCollection; if (!groups) { me.groupCollection = groups = new Ext.util.Collection(); } return groups; }, getSummaryRecord: function() { return this.summaryRecord || null; }, isGrouped: function() { return !!this.grouper; }, group: function(grouper, direction) { var me = this; grouper = grouper || null; if (grouper) { if (typeof grouper === 'string') { grouper = { property: grouper, direction: direction || 'ASC' }; } if (!grouper.isGrouper) { grouper = new Ext.util.Grouper(grouper); } grouper.setRoot('data'); me.getGroups().getSorters().splice(0, 1, { property: 'id', direction: grouper.getDirection() }); } me.grouper = grouper; if (!me.isConfiguring) { me.reloadRanges(); me.fireGroupChange(grouper); } }, getByInternalId: function(internalId) { return this.pageMap.getByInternalId(internalId); }, /** * Get the index of the record within the virtual store. Because virtual stores only * load a partial set of records, not all records in the logically matching set will * have been loaded and will therefore return -1. * * @param {Ext.data.Model} record The record to find. * @return {Number} The index of the `record` or -1 if not found. */ indexOf: function(record) { return this.pageMap.indexOf(record); }, /** * Get the index within the store of the record with the passed id. Because virtual * stores only load a partial set of records, not all records in the logically * matching set will have been loaded and will therefore return -1. * * @param {String} id The id of the record to find. * @return {Number} The index of the record or -1 if not found. */ indexOfId: function(id) { var rec = this.getById(id); return rec ? this.indexOf(rec) : -1; }, /** * Returns `true` if the store has been loaded. * @return {Boolean} `true` if the store has been loaded. */ isLoaded: function() { return Ext.isNumber(this.totalCount); }, load: function(options) { options = options || {}; // Load is a strange method here, since typically we want the view to control // how our data is to be loaded. However, the list won't automatically load the range // unless the store is autoLoad: true or has already loaded. We also want to be // able to support autoLoad: true. As such, the easiest thing we can do here is to // just jump out of the normal locking procedeure and just go and get the first page // since this is typically a one-off operation. // eslint-disable-next-line vars-on-top var page = options.page || 1, p = this.pageMap.getPage(page - 1, true); if (!p.isLoading()) { p.load({ callback: options.callback, scope: options.scope }); } }, /** * This function is called only once per reload and after this page is loaded * it will call the handleReload callback */ reload: function(options) { var me = this, page; if (typeof options === 'function') { options = { callback: options }; } if (me.fireEvent('beforereload') === false) { return null; } options = Ext.apply({ internalScope: me, internalCallback: me.handleReload, page: 1 }, options); me.pageMap.clear(); // Resetting the pagecount of previously loaded data me.pageMap.setPageCount(null); me.getGroups().clear(); // Initializing Page to be loaded page = me.pageMap.getPage(options.page - 1, true); // Changing state to "loading" so that the page won't be put into loadQueues page.state = 'loading'; return me.loadInternal(options); }, reloadRanges: function() { var activeRanges = this.activeRanges, i; this.pageMap.clear(); this.getGroups().clear(); for (i = activeRanges.length; i-- > 0;) { activeRanges[i].reload(); } }, // TODO load? // TODO reload? removeAll: function() { var me = this, activeRanges = me.activeRanges, i; me.pageMap.clear(); for (i = activeRanges.length; i-- > 0;) { activeRanges[i].reset(); } me.fireEvent('clear', me); }, //--------------------------------------------------------------------- applyProxy: function(proxy) { proxy = this.callParent([proxy]); // This store asks for pages. // If used with a MemoryProxy, it must work if (proxy && proxy.setEnablePaging) { proxy.setEnablePaging(true); } return proxy; }, // createDataCollection: function () { // var result = new Ext.data.virtual.Data({ // store: this // }); // // return result; // }, createFiltersCollection: function() { return new Ext.util.FilterCollection(); }, createSortersCollection: function() { return new Ext.util.SorterCollection(); }, onFilterEndUpdate: function() { var me = this, filters = me.getFilters(false); // This is not affected by suppressEvent. if (!me.isConfiguring) { me.reload(); me.fireEvent('filterchange', me, filters.getRange()); } }, onSorterEndUpdate: function() { var me = this, sorters = me.getSorters().getRange(), fire = !me.isConfiguring; if (fire) { me.fireEvent('beforesort', me, sorters); } if (fire) { me.reloadRanges(); me.fireEvent('sort', me, sorters); } }, updatePageSize: function(pageSize) { var totalCount = this.totalCount; if (totalCount !== null) { this.pageMap.setPageCount(Math.ceil(totalCount / pageSize)); } }, updateTotalCount: function(totalCount, oldTotalCount) { var me = this, pageMap = me.pageMap; me.totalCount = totalCount; pageMap.setPageCount(Math.ceil(totalCount / me.getPageSize())); me.fireEvent('totalcountchange', me, totalCount, oldTotalCount); }, //-------------------------------------------------------- // Unsupported API's //<debug> add: function() { Ext.raise('Virtual stores do not support the add() method'); }, insert: function() { Ext.raise('Virtual stores do not support the insert() method'); }, filter: function() { if (!this.getRemoteFilter()) { Ext.raise('Virtual stores do not support local filtering'); } // Remote filtering forces a load. load clears the store's contents. this.callParent(arguments); }, filterBy: function() { Ext.raise('Virtual stores do not support local filtering'); }, loadData: function() { Ext.raise('Virtual stores do not support the loadData() method'); }, applyData: function() { Ext.raise('Virtual stores do not support direct data loading'); }, updateRemoteFilter: function(remoteFilter, oldRemoteFilter) { if (remoteFilter === false) { Ext.raise('Virtual stores are always remotely filtered.'); } this.callParent([remoteFilter, oldRemoteFilter]); }, updateRemoteSort: function(remoteSort, oldRemoteSort) { if (remoteSort === false) { Ext.raise('Virtual stores are always remotely sorted.'); } this.callParent([remoteSort, oldRemoteSort]); }, updateTrackRemoved: function(value) { if (value !== false) { Ext.raise('Virtual stores do not support trackRemoved.'); } this.callParent(arguments); }, //</debug> afterEdit: function(record, modifiedFieldNames) { var me = this; me.fireEvent('update', me, record, Ext.data.Model.EDIT, modifiedFieldNames); me.fireEvent('datachanged', me); }, privates: { attachSummaryData: function(resultSet) { var me = this, summary = resultSet.getSummaryData(), grouper, len, i, data, rec; if (summary) { me.summaryRecord = summary; } summary = resultSet.getGroupData(); if (summary) { grouper = me.getGrouper(); if (grouper) { me.groupSummaryData = data = {}; for (i = 0, len = summary.length; i < len; ++i) { rec = summary[i]; data[grouper.getGroupString(rec)] = rec; } } } }, handleReload: function(op) { var me = this, activeRanges = me.activeRanges, len = activeRanges.length, pageMap = me.pageMap, resultSet = op.getResultSet(), wasSuccessful = op.wasSuccessful(), pageNumber = op.config && op.config.page, rsRecords = [], i, range, page; if (wasSuccessful) { me.readTotalCount(resultSet); // Condition for a valid page if (me.pageMap.getPageCount() !== 0 && pageNumber) { page = me.pageMap.getPage(pageNumber - 1, false); if (page && !(page.error = op.getError())) { // Filling the page with records loaded from the operation // and marking the page as loaded page.records = op.getRecords(); page.state = 'loaded'; me.pageMap.onPageLoad(page); } } me.fireEvent('reload', me, op); for (i = 0; i < len; ++i) { range = activeRanges[i]; if (pageMap.canSatisfy(range)) { range.reload(); } } } if (resultSet) { rsRecords = resultSet.records; } me.fireEvent('load', me, rsRecords, wasSuccessful, op); }, loadInternal: function(options) { if (typeof options === 'function') { options = { callback: options }; } /* eslint-disable-next-line vars-on-top */ var me = this, page = (options && options.page) || 1, pageSize = me.getPageSize(), operation = me.createOperation('read', Ext.apply({ start: (page - 1) * pageSize, limit: pageSize, page: page, filters: me.getFilters().items, sorters: me.getSorters().items, grouper: me.getGrouper() }, options)); if (me.fireEvent('beforeload', me, operation) !== false) { me.onBeforeLoad(operation); operation.execute(); } else { operation.setCompleted(); } return operation; }, loadVirtualPage: function(page, callback, scope, loadOptions) { // loadOptions will only be provided in the case where a user // has called directly, so this will be pretty rare. var me = this, pageMapGeneration = me.pageMap.generation; return me.loadInternal(Ext.apply({ page: page.number + 1, // store loads are 1 based internalCallback: function(op) { var resultSet = op.getResultSet(), rsRecords = []; if (pageMapGeneration === me.pageMap.generation) { if (op.wasSuccessful()) { me.readTotalCount(resultSet); me.attachSummaryData(resultSet); } callback.call(scope || page, op); me.groupSummaryData = null; if (resultSet) { rsRecords = resultSet.records; } me.fireEvent('load', me, rsRecords, op.wasSuccessful(), op); } } }, loadOptions)); }, lockGroups: function(grouper, page) { var groups = this.getGroups(), groupInfo = page.groupInfo = {}, records = page.records, len = records.length, groupSummaryData = this.groupSummaryData, pageMap = this.pageMap, n = page.number, group, i, groupKey, summaryRec, rec, firstRecords, first; for (i = 0; i < len; ++i) { rec = records[i]; groupKey = grouper.getGroupString(rec); if (!groupInfo[groupKey]) { groupInfo[groupKey] = rec; group = groups.get(groupKey); if (!group) { group = new Ext.data.virtual.Group(groupKey); groups.add(group); } // We want to track the first known record in the group. // If we have a record that is before the first one we know // about, add it to the front. Otherwise, we don't care about // the order at this point, so just shift it on to the end. firstRecords = group.firstRecords; first = firstRecords[0]; if (first && n < pageMap.getPageIndex(first)) { firstRecords.unshift(rec); } else { firstRecords.push(rec); } summaryRec = groupSummaryData && groupSummaryData[groupKey]; if (summaryRec) { group.summaryRecord = summaryRec; } } } }, onPageDataAcquired: function(page) { var grouper = this.getGrouper(); if (grouper) { this.lockGroups(grouper, page); } }, onPageDestroy: function(page) { var ranges = this.activeRanges, len = ranges.length, i; for (i = 0; i < len; ++i) { ranges[i].onPageDestroy(page); } }, onPageEvicted: function(page) { var grouper = this.getGrouper(); if (grouper) { this.releaseGroups(grouper, page); } }, readTotalCount: function(resultSet) { var total = resultSet.getRemoteTotal(); if (!isNaN(total)) { this.setTotalCount(total); } }, releaseGroups: function(grouper, page) { var groups = this.getGroups(), groupInfo = page.groupInfo, first, firstRecords, key, group; for (key in groupInfo) { first = groupInfo[key]; group = groups.get(key); firstRecords = group.firstRecords; // If there is only 1 first record left, this must be it, which // means the group no longer has records if (firstRecords.length === 1) { groups.remove(group); } else if (firstRecords[0] === first) { firstRecords.shift(); firstRecords.sort(this.sortByPage); } else { Ext.Array.remove(firstRecords, first); } } }, sortByPage: function(rec1, rec2) { // Bound to this instance in the constructor var map = this.pageMap; return map.getPageIndex(rec1) - map.getPageIndex(rec2); } }});