@coon-js/extjs-lib-comp
Version:
NPM package providing component library for ExtJS projects
666 lines (532 loc) • 19.8 kB
JavaScript
/**
* coon.js
* extjs-lib-comp
* Copyright (C) 2017-2021 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-comp
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* +--------------------------------------------------------------------+
* | LIVEGRID |
* +--------------------------------------------------------------------+
*
* Grid Feature which enhances the usability when used with a BufferedStore,
* allowing for more seamless update/remove/add operations without the need
* to completely reload the store, and instead delay reloading data to
* the prefetching mechanism of the BufferedStore.
*
*/
Ext.define("coon.comp.grid.feature.Livegrid", {
extend: "Ext.grid.feature.Feature",
alias: "feature.cn_comp-gridfeature-livegrid",
requires: [
"coon.core.data.pageMap.PageMapFeeder",
"coon.core.data.pageMap.RecordPosition",
"coon.core.data.pageMap.IndexRange",
"coon.core.data.pageMap.PageMapUtil"
],
/**
* @type {coon.core.data.pageMap.PageMapFeeder} pageMapFeeder
* @private
*/
pageMapFeeder: null,
/**
* A list of pages that where vetoed when the PageMapFeeder wanted to remove
* them. This is most likely due to the view still needing the page as being
* rendered. vetoedPages will get pruned in the #onScroll listener
*
* @type {Array}
* @private
*/
vetoedPages: null,
/**
* @inheritdoc
*/
init: function (grid) {
var me = this;
if (grid.multiColumnSort) {
Ext.raise({
msg: "Livegrid does not work with " +
"grid's \"multiColumnSort\"-functionality enabled",
multiColumnSort: grid.multiColumnSort
});
}
me.configure(grid.getStore());
grid.on("reconfigure", me.onGridReconfigure, me);
me.callParent(arguments);
me.swapSaveFocusState();
me.mon(me.grid.view.getScrollable(), "scroll", me.onScroll, me);
me.vetoedPages = [];
},
/**
* Returns true if this feature is configured to be used with a PageMapFeeder,
* otherwise false. This is so the API can check whether a bind has already
* occured, since relying on the rendered-state of the grid or its view
* isn't enough in most cases.
*
* @return {Boolean}
*/
isConfigured: function () {
return !!this.pageMapFeeder;
},
/**
* Helper function to find and return the record specified by the id.
* Returns the record-instance or null if not found.
*
* @param {String} the id or record to look for
*
* @return {Ext.data.Model|null}
*
* @see coon.core.data.pageMap.PageMapUtil#getRecordById
*/
getRecordById: function (id) {
const me = this;
return coon.core.data.pageMap.PageMapUtil.getRecordById(
id, me.pageMapFeeder
);
},
/**
* Adds the specified record to this Feature's BufferedStore.
* Make sure to join the record with the store afterwards if commit/edit/etc.
* events are required.
*
* @param {Ext.data.Model} record
*
* @returns {Boolean} Returns the result of #refreshView, which returns
* true in case the view was updated, otherwise false
*/
add: function (record) {
const me = this;
let op = me.pageMapFeeder.add(record),
pos = [], result;
if (op) {
result = op.getResult();
if (result.to) {
pos.push(result.to);
}
}
return me.refreshView(pos, false);
},
/**
* Callback for this feature's grid's scroll-event. Will check if the
* current-scroll position allows for finally removing vetoed pages out of the
* pageMap, so that they are reloaded when demanded.
*
* @param {Ext.scroll.Scroller} scroller
* @param {Number} x
* @param {Number} scrollTop
*
* @private
*/
onScroll: function (scroller, x, scrollTop) {
const me = this,
vetoedPages = me.vetoedPages,
len = vetoedPages.length;
if (!len) {
return;
}
const rowHeight = me.grid.view.bufferedRenderer.rowHeight,
pageMap = me.getPageMap(),
pageSize = pageMap.getPageSize(),
recordIndex = Math.round(scrollTop / rowHeight);
let idx, start, end;
for (var i = len -1; i >= 0; i--) {
idx = vetoedPages[i];
// compute the range for the vetoedPage
start = (idx - 1) * pageSize;
end = (start) + (pageSize - 1);
if (!(start <= recordIndex && recordIndex <= end)) {
pageMap.suspendEvents();
pageMap.removeAtKey(idx);
pageMap.resumeEvents();
vetoedPages.splice(i, 1);
}
}
},
/**
* Callback for the cn_core-pagemapfeeder-pageremoveveto-event of this
* Livegrid's PageMapFeeder.
* Will add the vetoed page to #vetoedPages, if it was not already registered
* as such.
*
* @param {Ext.data.PageMap} pageMapFeeder
* @param {Number} pageNumber
*
* @private
*/
onPageRemoveVeto: function (pageMapFeeder, pageNumber) {
const me = this,
vetoedPages = me.vetoedPages;
if (vetoedPages.indexOf(pageNumber) === -1) {
me.vetoedPages.push(pageNumber);
}
},
/**
* Method for removing a record, which will forward this call to this
* feature's PageMapFeeder#remove method.
* Selections will be updated in case the removed record was part of
* the selection-model.
*
* @param {Ext.data.Model} record
*
* @returns {Boolean} Returns the result of #refreshView, which returns
* true in case the view was updated, otherwise false
*/
remove: function (record) {
const me = this,
selModel = me.grid.getSelectionModel();
let op = me.pageMapFeeder.remove(record),
pos = [], result;
if (op) {
result = op.getResult();
if (selModel.isSelected(record)) {
selModel.deselect(record);
}
if (result.from) {
pos.push(result.from);
}
}
return me.refreshView(pos, false);
},
/**
* Callback for the store's update event. Will check if this
* grid's store sorter and sort order is affected by the update, and forward
* to this PageMapFeeder's #update method. If a coon.core.data.pageMap.Operation
* instance is returned by this method, either its from or to (or both)
* position(s) will be forwarded to #refreshView, which has the to decide
* if the view is re-rendered.
*
* @param {Ext.data.BufferedStore} store
* @param {Ext.data.Model} record
* @param {Ext.data.operation.Operation} operation
*
* @return {Boolean} false if no update in the view was triggered, otherwise
* true
*/
onStoreUpdate: function (store, record, operation) {
const me = this,
sorters = me.grid.store.getSorters(),
selModel = me.grid.getSelectionModel(),
property = sorters && sorters.getAt(0)
? sorters.getAt(0).getProperty()
: null,
prev = record.previousValues;
if (!property || !prev || !Object.prototype.hasOwnProperty.call(prev, property)) {
return false;
}
let op = me.pageMapFeeder.update(record),
pos = [], result;
if (op) {
result = op.getResult();
if (result.to) {
pos.push(result.to);
}
if (result.from) {
pos.push(result.from);
}
}
return me.refreshView(pos, selModel.isSelected(record));
},
/**
* Refreshes the view if any of the positions found in positions is contained
* in the current view.
*
* @param {Array|coon.core.data.pageMap.RecordPosition} positions
* @param {Boolean=true} ensureVisible true to call the grid's ensureVisible()
* method IF the grid has a selection
*
* @return {Boolean} true if refreshing the view was delegated to this grid's
* view, otherwise false
*
* @private
*/
refreshView: function (positions, ensureVisible = true) {
const me = this,
grid = me.grid,
view = grid.getView(),
pageMap = me.getPageMap(),
PageMapUtil = coon.core.data.pageMap.PageMapUtil,
viewRange = me.getCurrentViewRange();
let i, len, indexes = [], renderGuess = false;
for (i = 0, len = positions.length; i < len; i++) {
indexes.push(PageMapUtil.positionToStoreIndex(positions[i], pageMap));
}
// make a check if the view was rendered but contains NO elements yet
if (grid.getStore().isLoaded() && view.rendered && Ext.Object.isEmpty(view.all.elements)) {
renderGuess = true;
}
// if from or to is in the rendered rect, refresh view
if (renderGuess || viewRange.contains(positions)) {
indexes.push(view.all.startIndex);
view.refreshView(Math.min(...indexes));
if (ensureVisible !== false) {
let selection = grid.getSelection();
if (selection.length) {
grid.ensureVisible(selection[0]);
}
}
return true;
}
return false;
},
/**
* Returns the current range of rendered indexes.
* The end index of the computed range might not exist anymore in the
* store, since this method might get called after a record has been
* removed locally, but the view wasn't updated yet.
*
* @return {coon.core.data.pageMap.IndexRange} the current range or
* null if no range could be determined
*
* @private
*/
getCurrentViewRange: function () {
const me = this,
grid = me.grid,
view = grid.getView(),
rows = view.all,
PageMapUtil = coon.core.data.pageMap.PageMapUtil;
if (rows.endIndex === -1) {
return null;
}
let start = rows.startIndex,
end = rows.endIndex;
return PageMapUtil.storeIndexToRange(start, end, me.getPageMap(), true);
},
/**
* Returns the PageMap this feature works with.
*
* @return {Ext.data.PageMap}
*
* @private
*/
getPageMap: function () {
return this.grid.getStore().getData();
},
/**
* Last resort for BufferedStore is to trigger the cachemiss which signals
* that the pages between the start- and the end-index shoulc be loaded
* immediately.
* Since this feature might prevent Pages to be loaded in case they are
* currently being used as Feeds, the cachemiss event will remove all
* Feeds that would be replaced by the page(s) which are about to get
* loaded.
* This method will effectively release any "lock" that is considered by the
* beforepretch-listener in this feature,
*
* @param {Ext.data.BufferedStore} store
* @param {Number} start
* @param {Number} end
*/
onCacheMiss: function (store, start, end) {
const me = this,
pageMap = me.getPageMap(),
PageMapUtil = coon.core.data.pageMap.PageMapUtil,
pageMapFeeder = me.pageMapFeeder;
let startPos = PageMapUtil.storeIndexToPosition(start, pageMap),
endPos = PageMapUtil.storeIndexToPosition(end, pageMap),
startIdx = Math.min(startPos.getPage(), endPos.getPage()),
endIdx = Math.max(startPos.getPage(), endPos.getPage());
for (let i = startIdx; i <= endIdx; i++) {
if (!pageMap.peekPage(i) && pageMapFeeder.getFeedAt(i)) {
pageMapFeeder.removeFeedAt(i);
}
}
},
/**
* Callback for the pageadd-event.
* Delegates to #cleanFeedsAndVetoed with the pageNumber.
* When this callback is invoked, we assume the event was triggered for
* a valid cause.
*
* @param {Ext.data.PageMap} pageMap
* @param {Number} pageNumber
*
* @private
*
* @see #cleanFeedsAndVetoed
*/
onPageAdd: function (pageMap, pageNumber) {
this.cleanFeedsAndVetoed(pageNumber);
},
/**
* Callback for the pageremove-event.
* Delegates to #cleanFeedsAndVetoed with the pageNumber.
* When this callback is invoked, we assume the event was triggered for
* a valid cause.
* @param {Ext.data.PageMap} pageMap
* @param {Number} pageNumber
*
* @private
*
* @see #cleanFeedsAndVetoed
*/
onPageRemove: function (pageMap, pageNumber) {
this.cleanFeedsAndVetoed(pageNumber);
},
/**
* Cancels prefetch by checking whether the required page exists as a Feed,
* and, if that is the case, is currently not in the current view-range.
*
* @param {Ext.data.BufferedStore} store
* @param {Ext.data.Operation} operation
*
* @return {Boolean} if this feature is okay with prefetching the requested
* page, otherwise false
*
* @see #getCurrentViewRange
*/
onBeforePrefetch: function (store, operation) {
const me = this,
page = operation.getPage(),
pageMapFeeder = me.pageMapFeeder,
RecordPosition = coon.core.data.pageMap.RecordPosition;
if (pageMapFeeder.getFeedAt(page) &&
!me.getCurrentViewRange().contains(RecordPosition.create(page, 0))) {
return false;
}
return true;
},
/**
* Callback for the reconfigure event of the grid of this feature.
*
* @param {Ext.grid.Panel} grid
* @param {Ext.store.AbstractStore} store
* @param {Array} columns
* @param {Ext.store.AbstractStore} oldStore
* @param {Array} oldColumns
*
* @private
*/
onGridReconfigure: function (grid, store, columns, oldStore, oldColumns) {
var me = this;
if (store && store !== oldStore) {
me.configure(store);
}
},
/**
* Sets up needed requirements for this feature to work properly.
* We treat empty stores special, as we silently ignore them when this
* method is called with a store representing an empty store.
*
* @param {Ext.data.BufferedStore} store
*
* @return {Boolean} true if the requirements where successfully installed,
* otherwise false, e.g. due to an empty store
*
* @private
*
* @throws if store is not an empty store or a BufferedStore
*/
configure: function (store) {
const me = this;
if (store && store.isEmptyStore) {
return false;
}
if (!store || !(store instanceof Ext.data.BufferedStore)) {
Ext.raise({
msg: "'store' must be an instance of Ext.data.BufferedStore",
store: store
});
}
let pageMap = store.getData();
// this is not needed since Ext.util.Event#addListener already checks
// for duplicates and doesn't add one and the same event twice.
// however, we want to be on the save side, our tests consider this
// edge case
me.mun(store, "update", me.onStoreUpdate, me);
me.mon(store, "update", me.onStoreUpdate, me);
me.mun(store, "beforeprefetch", me.onBeforePrefetch, me);
me.mon(store, "beforeprefetch", me.onBeforePrefetch, me);
me.mun(store, "pageadd", me.onPageAdd, me);
me.mon(store, "pageadd", me.onPageAdd, me);
me.mun(store, "pageremove", me.onPageRemove, me);
me.mon(store, "pageremove", me.onPageRemove, me);
me.mun(store, "cachemiss", me.onCacheMiss, me);
me.mon(store, "cachemiss", me.onCacheMiss, me);
if (me.pageMapFeeder) {
me.mun(me.pageMapFeeder, "cn_core-pagemapfeeder-pageremoveveto", me.onPageRemoveVeto, me);
me.pageMapFeeder.destroy();
me.pageMapFeeder = null;
}
me.pageMapFeeder = Ext.create("coon.core.data.pageMap.PageMapFeeder", {
pageMap: pageMap
});
me.mon(me.pageMapFeeder, "cn_core-pagemapfeeder-pageremoveveto", me.onPageRemoveVeto, me);
return true;
},
/**
* Swaps the restoreFocus() method that is usually returned by the grid's
* view "saveFocusState()" with a custom method that considers all store
* data instead just a snapshot as assumed by the default implementation,
* by overriding the NodeCache's (view.all) getCount() method to return the
* totalCount of the store.
*
* @return {Boolean}
*
* @private
*/
swapSaveFocusState: function () {
const me = this,
view = me.view;
view.saveFocusState = coon.comp.grid.feature.Livegrid.prototype.saveFocusState;
return true;
},
/**
* Replacement-method for Ext.view.Table#saveFocusState.
* This method gets called in the scope of the grid's view.
*
* @return {Function}
*
* @private
*
* @see swapSaveFocusState
*/
saveFocusState: function () {
const me = this;
let tmp = me.all.getCount,
res = Ext.view.Table.prototype.saveFocusState.apply(me, arguments);
let func = function () {
me.all.getCount = function () {
return me.dataSource.getTotalCount();
};
res.apply(me);
me.all.getCount = tmp;
};
return func;
},
/**
* Will remove the Feed and the vetoedPage identified by pageNumber.
*
* @param {Number} pageNumber
*
* @private
*/
cleanFeedsAndVetoed: function (pageNumber) {
const me = this,
pageMapFeeder = me.pageMapFeeder,
vetoedPages = me.vetoedPages;
pageMapFeeder.removeFeedAt(pageNumber);
let idx = vetoedPages.indexOf(pageNumber);
if (idx !== -1) {
vetoedPages.splice(idx, 1);
}
}
});