/**
* 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);
}
}
});