/**
* @private
* Implements buffered rendering of a grid, allowing users to scroll
* through thousands of records without the performance penalties of
* rendering all the records into the DOM at once.
*
* The number of rows rendered outside the visible area can be controlled by configuring the plugin.
*
* Users should not instantiate this class. It is instantiated automatically
* and applied to all grids.
*
* ## Implementation notes
*
* This class monitors scrolling of the {@link Ext.view.Table
* TableView} within a {@link Ext.grid.Panel GridPanel} to render a small section of
* the dataset.
*
*/
Ext.define('Ext.grid.plugin.BufferedRenderer', {
extend: 'Ext.plugin.Abstract',
requires: [
'Ext.grid.locking.RowSynchronizer'
],
alias: 'plugin.bufferedrenderer',
/**
* @property {Boolean} isBufferedRenderer
* `true` in this class to identify an object as an instantiated BufferedRenderer,
* or subclass thereof.
*/
isBufferedRenderer: true,
lockableScope: 'both',
/**
* @cfg {Number}
* The zone which causes new rows to be appended to the view. As soon as the edge
* of the rendered grid is this number of rows from the edge of the viewport, the view is moved.
*/
numFromEdge: 2,
/**
* @cfg {Number}
* The number of extra rows to render on the trailing side of scrolling
* **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
*/
trailingBufferZone: 10,
/**
* @cfg {Number}
* The number of extra rows to render on the leading side of scrolling
* **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
*/
leadingBufferZone: 20,
/**
* @cfg {Boolean} [synchronousRender=true]
* By default, on detection of a scroll event which brings the end of the rendered table within
* `{@link #numFromEdge}` rows of the grid viewport, if the required rows are available
* in the Store, the BufferedRenderer will render rows from the Store *immediately* before
* returning from the event handler.
* This setting helps avoid the impression of whitespace appearing during scrolling.
*
* Set this to `false` to defer the render until the scroll event handler exits.
* This allows for faster scrolling, but also allows whitespace to be more easily scrolled
* into view.
*
*/
synchronousRender: true,
/**
* @cfg {Number}
* This is the time in milliseconds to buffer load requests when the store is a
* {@link Ext.data.BufferedStore buffered store} and a page required for rendering
* is not present in the store's cache and needs loading.
*/
scrollToLoadBuffer: 200,
/**
* @private
*/
viewSize: 100,
/**
* @private
*/
rowHeight: 21,
/**
* @property {Number} position
* Current pixel scroll position of the associated {@link Ext.view.Table View}.
*/
position: 0,
scrollTop: 0,
lastScrollDirection: 1,
bodyTop: 0,
scrollHeight: 0,
loadId: 0,
newFocusTarget: null,
lastFocusedMultiRange: null,
colHeader: null,
lastFocusedElement: null,
// Initialize this as a plugin
init: function(grid) {
var me = this,
view = grid.view,
viewListeners = {
beforerefresh: me.onViewBeforeRefresh,
refresh: me.onViewRefresh,
columnschanged: me.checkVariableRowHeight,
scope: me,
destroyable: true
},
scrollerListeners = {
scroll: me.onViewScroll,
scope: me
},
initialConfig = view.initialConfig;
me.scroller = view.lockingPartner ? view.ownerGrid.scrollable : view.getScrollable();
// If we are going to be handling a NodeStore then it's driven by node addition and removal,
// *not* refreshing. The view overrides required above change the view's onAdd and onRemove
// behaviour to call onDataRefresh when necessary.
if (grid.isTree || (grid.ownerLockable && grid.ownerLockable.isTree)) {
view.blockRefresh = false;
// Set a load mask if undefined in the view config.
if (initialConfig && initialConfig.loadMask === undefined) {
view.loadMask = true;
}
}
if (view.positionBody) {
viewListeners.refresh = me.onViewRefresh;
}
me.grid = grid;
me.view = view;
me.isRTL = view.getInherited().rtl;
view.bufferedRenderer = me;
view.preserveScrollOnRefresh = true;
view.animate = false;
// It doesn't matter if it's a FeatureStore or a DataStore.
// The important thing is to only bind the same Type of store in future operations!
me.bindStore(view.dataSource);
// Use a configured rowHeight in the view
if (view.hasOwnProperty('rowHeight')) {
me.rowHeight = view.rowHeight;
}
me.position = 0;
me.viewListeners = view.on(viewListeners);
// Listen to the correct scroller. Either the view's one, or of it is
// in a lockable assembly, the y scroller which scrolls them both.
// If the view is not scrollable, this will be falsy.
if (me.scroller) {
me.scrollListeners = me.scroller.on(scrollerListeners);
}
},
// Keep the variableRowHeight property correct WRT variable row heights being possible.
checkVariableRowHeight: function() {
var hadVariableRowHeight = this.variableRowHeight;
this.variableRowHeight = this.view.hasVariableRowHeight();
// Next time we refresh size, row height will also be recalculated
if (!!this.variableRowHeight !== !!hadVariableRowHeight) {
delete this.rowHeight;
}
},
bindStore: function(newStore) {
var me = this,
currentStore = me.store;
// If the grid was configured with a feature such as Grouping that binds a FeatureStore
// (GroupStore, in its case) as the view's dataSource, we must continue to use
// the same Type of store.
//
// Note that reconfiguring the grid can call into here.
if (currentStore && currentStore.isFeatureStore) {
return;
}
if (currentStore) {
me.unbindStore();
}
me.storeListeners = newStore.on({
scope: me,
groupschange: me.onStoreGroupChange,
groupchange: me.onStoreGroupChange,
clear: me.onStoreClear,
beforeload: me.onBeforeStoreLoad,
load: me.onStoreLoad,
destroyable: true
});
me.store = newStore;
me.setBodyTop(me.position = me.scrollTop = 0);
// Delete whatever our last viewSize might have been, and fall back
// to the prototype's default.
delete me.viewSize;
delete me.rowHeight;
if (newStore.isBufferedStore) {
newStore.setViewSize(me.viewSize);
}
},
unbindStore: function() {
this.storeListeners.destroy();
this.storeListeners = this.store = null;
},
// Disable handling of scroll events until the load is finished
onBeforeStoreLoad: function(store) {
var me = this,
view = me.view;
if (view && view.refreshCounter) {
// Unless we are loading tree nodes, or have preserveScrollOnReload,
// set scroll position and row range back to zero.
if (store.isTreeStore || view.preserveScrollOnReload) {
me.nextRefreshStartIndex = view.all.startIndex;
}
else {
if (me.scrollTop !== 0) {
// Zero position tracker so that next scroll event will not trigger any action
// eslint-disable-next-line max-len
me.setBodyTop(me.bodyTop = me.scrollTop = me.position = me.scrollHeight = me.nextRefreshStartIndex = 0);
me.scroller.scrollTo(null, 0);
}
}
me.lastScrollDirection = me.scrollOffset = null;
}
me.disable();
},
// Re-enable scroll event handling on load.
onStoreLoad: function() {
this.isStoreLoading = true;
this.enable();
},
onStoreClear: function() {
var me = this,
view = me.view;
// Do not do anything if view is not rendered, or if the reason for cache clearing
// is store destruction
if (view.rendered && !me.store.destroyed) {
if (me.scrollTop !== 0) {
// Zero position tracker so that next scroll event will not trigger any action
me.bodyTop = me.scrollTop = me.position = me.scrollHeight = 0;
me.nextRefreshStartIndex = null;
me.scroller.scrollTo(null, 0);
}
// TableView does not add a Store Clear listener if there's a BufferedRenderer
// We handle that here.
view.refresh();
me.lastScrollDirection = me.scrollOffset = null;
}
},
// If the store is not grouped, we can switch to fixed row height mode
onStoreGroupChange: function(store) {
this.refreshSize();
},
/**
* BeforeRefresh listener needed by normal grids (non-locking) to capture
* the startIndex before a refresh is done.
*/
onViewBeforeRefresh: function(view) {
this.beforeRefreshStartIndex = view.all.startIndex;
},
onViewRefresh: function(view, records) {
var me = this,
rows = view.all,
height, beforeRefreshStartIndex;
// Recheck the variability of row height in the view.
me.checkVariableRowHeight();
// The first refresh on the leading edge of the initial layout will mean that the
// View has not had the sizes of flexed columns calculated and flushed yet.
// So measurement of DOM height for calculation of an approximation of the variableRowHeight
// would be premature.
// And measurement of the body width would be premature because of uncalculated flexes.
// eslint-disable-next-line max-len
if (!view.componentLayoutCounter && (view.headerCt.down('{flex}') || me.variableRowHeight)) {
view.on({
boxready: Ext.Function.pass(me.onViewRefresh, [view, records], me),
single: true
});
// AbstractView will call refreshSize() immediately after firing the 'refresh'
// event; we need to skip that run for the reasons stated above.
me.skipNextRefreshSize = true;
return;
}
me.skipNextRefreshSize = false;
// If we are instigating the refresh, we will have already called refreshSize
// in doRefreshView
if (me.refreshing) {
return;
}
me.refreshSize();
if (me.scroller) {
if (me.scrollTop !== me.scroller.getPosition().y) {
// The view may have refreshed and scrolled to the top, for example
// on a sort. If so, it's as if we scrolled to the top, so we'll simulate
// it here.
me.onViewScroll();
}
else {
if (!me.hasOwnProperty('bodyTop')) {
me.bodyTop = rows.startIndex * me.rowHeight;
me.scroller.scrollTo(null, me.bodyTop);
}
me.setBodyTop(me.bodyTop);
// With new data, the height may have changed, so recalculate the rowHeight
// and viewSize. This will either add or remove some rows.
height = view.lastBox && view.lastBox.height;
if (height && rows.getCount()) {
me.onViewResize(view, null, height);
// If we repaired the view by adding or removing records, then keep the records
// array consistent with what is there for subsequent listeners.
// For example the WidgetColumn listener which post-processes all rows:
// https://sencha.jira.com/browse/EXTJS-13942
if (records && (rows.getCount() !== records.length)) {
records.length = 0;
records.push.apply(
records, me.store.getRange(rows.startIndex, rows.endIndex)
);
}
}
}
}
// Determine what the previously captured startIndex was based on
// whether the grid was a normal grid or a locked grid.
if (view.lockingPartner) {
beforeRefreshStartIndex = view.beforeRefreshStartIndex;
view.beforeRefreshStartIndex = null;
}
else {
beforeRefreshStartIndex = me.beforeRefreshStartIndex;
me.beforeRefreshStartIndex = null;
}
// Call refreshView() with this previously captured startIndex which
// is done in the onViewBeforeRefresh method for normal grids and in
// the Ext.grid.locking.View#refresh method for locked grids.
// Without this logic, a call to view.refresh() followed by a scroll up
// will result in blank lines above the rendered range and the rendered
// range will not be the correct range of data that should be displayed.
if (beforeRefreshStartIndex) {
me.refreshView(beforeRefreshStartIndex);
}
},
/**
* @private
* @param {Ext.layout.ContextItem} ownerContext The view's layout context
* Called before the start of a view's layout run
*/
beforeTableLayout: function(ownerContext) {
var dom = this.view.body.dom,
size;
if (dom) {
size = this.grid.getElementSize(dom);
ownerContext.bodyHeight = size.height;
ownerContext.bodyWidth = size.width;
}
},
/**
* @private
* @param {Ext.layout.ContextItem} ownerContext The view's layout context
* Called when a view's layout run is complete.
*/
afterTableLayout: function(ownerContext) {
var me = this,
view = me.view,
renderedBlockHeight;
// The rendered block has changed height.
// This could happen if a cellWrap: true column has changed width.
// We need to recalculate row height and scroll range
if (ownerContext.bodyHeight && view.body.dom) {
delete me.rowHeight;
me.refreshSize();
renderedBlockHeight = me.grid.getElementHeight(view.body.dom);
if (renderedBlockHeight !== ownerContext.bodyHeight) {
me.onViewResize(view, null, view.el.lastBox.height);
// The view resize might have added or removed rows
renderedBlockHeight = me.bodyHeight;
// The layout has caused the rendered block to shrink in height.
// This could happen if a cellWrap: true column has increased in width.
// It could cause the bottom of the rendered view to zoom upwards
// out of sight.
if (renderedBlockHeight < ownerContext.bodyHeight) {
if (me.viewSize >= me.store.getCount()) {
me.setBodyTop(0);
}
// Column got wider causing scroll range to shrink, leaving the view
// stranded above the fold. Scroll up to bring it into view.
// eslint-disable-next-line max-len
else if (me.bodyTop > me.scrollTop || me.bodyTop + renderedBlockHeight < me.scrollTop + me.viewClientHeight) {
me.setBodyTop(me.scrollTop - me.trailingBufferZone * me.rowHeight);
}
}
// If the rendered block is the last lines in the dataset,
// ensure the scroll range exactly encapsuates it.
if (view.all.endIndex === (view.dataSource.getCount()) - 1) {
me.stretchView(view, me.scrollHeight = me.bodyTop + renderedBlockHeight - 1);
}
}
}
},
refreshSize: function() {
var me = this,
view = me.view,
// If we have been told to skip the next refresh, or there is going to be
// an upcoming layout, skip this op.
skipNextRefreshSize =
me.skipNextRefreshSize ||
(Ext.Component.pendingLayouts && Ext.Component.layoutSuspendCount) ||
!view.body.dom;
// We only want to skip ONE time.
me.skipNextRefreshSize = false;
if (skipNextRefreshSize) {
return;
}
// Cache the rendered block height.
me.bodyHeight = me.grid.getElementHeight(view.body.dom);
// Calculates scroll range.
// Also calculates rowHeight if we do not have an own rowHeight property.
me.scrollHeight = me.getScrollHeight();
me.stretchView(view, me.scrollHeight);
},
/**
* Called directly from {@link Ext.view.Table#onResize}. Reacts to View changing height by
* recalculating the size of the rendered block, and either trimming it or adding to it.
* @param {Ext.view.Table} view The Table view.
* @param {Number} width The new Width.
* @param {Number} height The new height.
* @param {Number} oldWidth The old width.
* @param {Number} oldHeight The old height.
* @private
*/
onViewResize: function(view, width, height, oldWidth, oldHeight) {
var me = this,
newViewSize;
me.refreshSize();
// Only process first layout (the boxready event) or height resizes.
if (!oldHeight || height !== oldHeight) {
// Changing the content height may trigger multiple layouts for locked grids.
// Ensure they are coalesced.
Ext.suspendLayouts();
// Recalculate the view size in rows now that the grid view has changed height
me.viewClientHeight = height || view.el.dom.clientHeight;
// Use the theme's default rowHeight unless the measured row height is smaller
// when calculating the view size. If the rows are *larger*, that doesn't really matter.
// We just need to cover the visible range with some scrolling range extra.
newViewSize = Math.ceil(height / Math.min(me.getThemeRowHeight(), me.rowHeight)) +
me.trailingBufferZone + me.leadingBufferZone;
me.viewSize = me.setViewSize(newViewSize);
Ext.resumeLayouts(true);
}
},
stretchView: function(view, scrollRange) {
var me = this,
newY;
// Ensure that both the scroll range AND the positioned view body are in the viewable area.
if (me.scrollTop > scrollRange) {
newY = me.nextRefreshStartIndex == null ? me.bodyHeight : scrollRange - me.bodyHeight;
me.position = me.scrollTop = Math.max(newY, 0);
me.scroller.scrollTo(null, me.scrollTop);
}
if (me.bodyTop > scrollRange) {
view.body.translate(null, me.bodyTop = me.position);
}
// Tell the scroller what the scroll size is.
if (view.getScrollable()) {
me.refreshScroller(view, scrollRange);
}
},
refreshScroller: function(view, scrollRange) {
var scroller = view.getScrollable();
if (scroller) {
// Ensure the scroller viewport element size is up to date if it needs to be told
// (touch scroller)
if (scroller.setElementSize) {
scroller.setElementSize();
}
// Ensure the scroller knows about content size
scroller.setSize({
x: view.headerCt.getTableWidth(),
// No Y range in the view's scroller if we're in a locking assembly.
// The LockingScroller stretches the views.
y: view.lockingPartner ? null : scrollRange
});
// In a locking assembly, stretch the yScroller
if (view.lockingPartner) {
this.scroller.setSize({
x: 0,
y: scrollRange
});
}
}
},
setViewSize: function(viewSize, fromLockingPartner) {
var me = this,
store = me.store,
view = me.view,
ownerGrid,
rows = view.all,
elCount = rows.getCount(),
storeCount = store.getCount(),
start, end,
lockingPartner = me.view.lockingPartner && me.view.lockingPartner.bufferedRenderer,
diff = elCount - viewSize,
oldTop = 0,
maxIndex = Math.max(0, storeCount - 1),
// This is which end is closer to being visible therefore must be the first
// to have rows added or the opposite end from which rows get removed
// if shrinking the view.
pointyEnd = Ext.Number.sign(
(me.getFirstVisibleRowIndex() - rows.startIndex) -
(rows.endIndex - me.getLastVisibleRowIndex())
);
// Synchronize view sizes
if (lockingPartner && !fromLockingPartner) {
lockingPartner.setViewSize(viewSize, true);
}
diff = elCount - viewSize;
if (diff) {
// Must be set for getFirstVisibleRowIndex to work
me.scrollTop = me.scroller ? me.scroller.getPosition().y : 0;
me.viewSize = viewSize;
if (store.isBufferedStore) {
store.setViewSize(viewSize);
}
// If a store loads before we have calculated a viewSize, it loads me.defaultViewSize
// records. This may be larger or smaller than the final viewSize so the store needs
// adjusting when the view size is calculated.
if (elCount) {
// New start index should be current start index unless that's now too close
// to the end of the store to yield a full view, in which case work back
// from the end of the store. Ensure we don't go negative.
start = Math.max(0, Math.min(rows.startIndex, storeCount - viewSize));
// New end index works forward from the new start index ensuring
// we don't walk off the end
end = Math.min(start + viewSize - 1, maxIndex);
// Only do expensive adding or removal if range is not already correct
if (start === rows.startIndex && end === rows.endIndex) {
// Needs rows adding to or bottom depending on which end is closest
// to being visible (The pointy end)
if (diff < 0) {
me.handleViewScroll(pointyEnd);
}
}
else {
// While changing our visible range, the locking partner must not sync
if (lockingPartner) {
lockingPartner.disable();
}
// View must expand
if (diff < 0) {
// If it's *possible* to add rows...
if (storeCount > viewSize && storeCount > elCount) {
// Grab the render range with a view to appending and prepending
// nodes to the top and bottom as necessary.
// Store's getRange API always has been inclusive of endIndex.
store.getRange(start, end, {
callback: function(newRecords, start, end) {
ownerGrid = view.ownerGrid;
// Append if necessary
if (end > rows.endIndex) {
// eslint-disable-next-line max-len
rows.scroll(Ext.Array.slice(newRecords, rows.endIndex + 1, Infinity), 1, 0);
}
// Prepend if necessary
if (start < rows.startIndex) {
oldTop = rows.first(true);
// eslint-disable-next-line max-len
rows.scroll(Ext.Array.slice(newRecords, 0, rows.startIndex - start), -1, 0);
// We just added some rows to the top of the rendered block
// We have to bump it up to keep the view stable.
me.bodyTop -= oldTop.offsetTop;
}
me.setBodyTop(me.bodyTop);
// The newly added rows must sync the row heights
// eslint-disable-next-line max-len
if (lockingPartner && !fromLockingPartner && (ownerGrid.syncRowHeight || ownerGrid.syncRowHeightOnNextLayout)) {
lockingPartner.setViewSize(viewSize, true);
ownerGrid.syncRowHeights();
}
}
});
}
// If not possible just refresh
else {
me.refreshView(0);
}
}
// View size is contracting
else {
// If removing from top, we have to bump the rendered block downwards
// by the height of the removed rows.
if (pointyEnd === 1) {
oldTop = rows.item(rows.startIndex + diff, true).offsetTop;
}
// Clip the rows off the required end
rows.clip(pointyEnd, diff);
me.setBodyTop(me.bodyTop + oldTop);
// And clip the rows of the lockingPartner
if (lockingPartner && lockingPartner.view.all.count > 0) {
lockingPartner.view.all.clip(pointyEnd, diff);
lockingPartner.setBodyTop(lockingPartner.bodyTop + oldTop);
}
}
if (lockingPartner) {
lockingPartner.enable();
}
}
}
// Update scroll range
me.refreshSize();
}
return viewSize;
},
/**
* @private
* TableView's getViewRange delegates the operation to this method
* if buffered rendering is present.
*/
getViewRange: function() {
var me = this,
view = me.view,
rows = view.all,
rowCount = rows.getCount(),
lockingPartnerRows = view.lockingPartner && view.lockingPartner.all,
store = me.store,
startIndex = 0,
endIndex;
// Get a best guess at the number of rows to fill the view
if (!me.hasOwnProperty('viewSize') && me.ownerCt && me.ownerCt.height) {
me.viewSize = Math.ceil(me.ownerCt.height / me.rowHeight);
}
if (!store.data.getCount()) {
return [];
}
// We're starting from nothing, but there's a locking partner with the range info,
// so match that
if (!rowCount && lockingPartnerRows && lockingPartnerRows.getCount()) {
startIndex = lockingPartnerRows.startIndex;
endIndex = Math.min(
lockingPartnerRows.endIndex, startIndex + me.viewSize - 1, store.getCount() - 1
);
}
else {
// If there already is a view range, then the startIndex from that
if (rowCount) {
startIndex = rows.startIndex;
}
// Otherwise use start index of current page.
// https://sencha.jira.com/browse/EXTJSIV-10724
// Buffered store may be primed with loadPage(n) call rather than autoLoad
// which starts at index 0.
else if (store.isBufferedStore) {
if (!store.currentPage) {
store.currentPage = 1;
}
startIndex = rows.startIndex = (store.currentPage - 1) * (store.pageSize || 1);
// The RowNumberer uses the current page to offset the record index,
// so when buffered, it must always be on page 1
store.currentPage = 1;
}
endIndex = startIndex + (me.viewSize || store.defaultViewSize) - 1;
}
return store.getRange(startIndex, endIndex);
},
/**
* @private
* Handles the Store replace event, producing a correct buffered view
* after the replace operation.
*/
onReplace: function(store, startIndex, oldRecords, newRecords) {
var me = this,
scroller = me.scroller,
view = me.view,
rows = view.all,
oldStartIndex,
renderedSize = rows.getCount(),
lastAffectedIndex = startIndex + oldRecords.length - 1,
recordIncrement = newRecords.length - oldRecords.length,
scrollIncrement = recordIncrement * me.rowHeight,
preserveScrollOnRefresh;
// All replacement activity is past the end of a full-sized rendered block;
// do nothing except update scroll range
if (startIndex >= rows.startIndex + me.viewSize) {
me.refreshSize();
return;
}
// If the change is all above the rendered block and the rendered block is its maximum size,
// update the scroll range and ensure the buffer zone above is filled if possible.
if (renderedSize && lastAffectedIndex < rows.startIndex && rows.getCount() >= me.viewSize) {
// Move the index-based NodeCache up or down depending on whether it's a net
// adding or removal above.
rows.moveBlock(recordIncrement);
me.refreshSize();
// If the change above us was an addition, pretend that we just scrolled upwards
// which will ensure that there is at least this.numFromEdge rows above the fold.
oldStartIndex = rows.startIndex;
if (recordIncrement > 0) {
// Do not allow this operation to mirror to the partner side.
me.doNotMirror = true;
me.handleViewScroll(-1);
me.doNotMirror = false;
}
// If the handleViewScroll did nothing, we just have to ensure the rendered block
// is the correct amount down the scroll range, and then readjust the top
// of the rendered block to keep the visuals the same.
if (rows.startIndex === oldStartIndex) {
// If inserting or removing invisible records above the start of the rendered block,
// the visible block must then be moved up or down the scroll range.
if (rows.startIndex) {
me.setBodyTop(me.bodyTop += scrollIncrement);
view.suspendEvent('scroll');
view.scrollBy(0, scrollIncrement);
view.resumeEvent('scroll');
me.position = me.scrollTop = me.scroller.getPosition().y;
}
}
// The handleViewScroll added rows, so we must scroll to keep the visuals the same;
else {
view.suspendEvent('scroll');
view.scrollBy(0, (oldStartIndex - rows.startIndex) * me.rowHeight);
view.resumeEvent('scroll');
}
view.refreshSize(rows.getCount() !== renderedSize);
return;
}
// If the change is all below the rendered block, update the scroll range
// and ensure the buffer zone below us is filled if possible.
if (renderedSize && startIndex > rows.endIndex) {
me.refreshSize();
// If the change below us was an addition, ask for <viewSize>
// rows to be rendered starting from the current startIndex.
// If more rows need to be scrolled onto the bottom of the rendered
// block to achieve this, that will do it.
if (recordIncrement > 0) {
me.onRangeFetched(
null, rows.startIndex,
Math.min(store.getCount(), rows.startIndex + me.viewSize) - 1
);
}
view.refreshSize(rows.getCount() !== renderedSize);
return;
}
// Cut into rendered block from above
if (startIndex < rows.startIndex && lastAffectedIndex <= rows.endIndex) {
preserveScrollOnRefresh = view.preserveScrollOnRefresh;
view.preserveScrollOnRefresh = false;
me.refreshView(rows.startIndex - oldRecords.length + newRecords.length);
view.preserveScrollOnRefresh = preserveScrollOnRefresh;
return;
}
if (startIndex < rows.startIndex && lastAffectedIndex <= rows.endIndex && scrollIncrement) {
me.doVerticalScroll(scroller, me.scrollTop += scrollIncrement, true);
}
// Only need to change display if the view is currently empty, or
// change intersects the rendered view.
me.refreshView(rows.startIndex, scrollIncrement);
},
doVerticalScroll: function(scroller, pos, supressEvents) {
var me = this;
if (!scroller) {
return;
}
if (supressEvents) {
scroller.suspendEvent('scroll');
}
scroller.scrollTo(null, me.position = pos);
if (supressEvents) {
scroller.resumeEvent('scroll');
}
},
/**
* @private
* Scrolls to and optionally selects the specified row index **in the total dataset**.
*
* This is a private method for internal usage by the framework.
*
* Use the grid's {@link Ext.panel.Table#ensureVisible ensureVisible} method to scroll
* a particular record or record index into view.
*
* @param {Number/Ext.data.Model} recordIdx The record, or the zero-based position
* in the dataset to scroll to.
* @param {Object} [options] An object containing options to modify the operation.
* @param {Boolean} [options.animate] Pass `true` to animate the row into view.
* @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
* when it is in view.
* @param {Boolean} [options.select] Pass as `true` to select the specified row.
* @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
* @param {Function} [options.callback] A function to call when the row has been scrolled to.
* @param {Number} options.callback.recordIdx The resulting record index (may have changed
* if the passed index was outside the valid range).
* @param {Ext.data.Model} options.callback.record The resulting record from the store.
* @param {HTMLElement} options.callback.node The resulting view row element.
* @param {Object} [options.scope] The scope (`this` reference) in which to execute
* the callback. Defaults to this BufferedRenderer.
* @param {Ext.grid.column.Column/Number} [options.column] The column, or column index
* to scroll into view.
*
*/
scrollTo: function(recordIdx, options) {
var args = arguments,
me = this,
view = me.view,
lockingPartner = view.lockingPartner && view.lockingPartner.grid.isVisible() &&
view.lockingPartner.bufferedRenderer,
store = me.store,
total = store.getCount(),
startIdx, endIdx, targetRow, tableTop, groupingFeature, metaGroup, record, direction;
// New option object API
if (options !== undefined && !(options instanceof Object)) {
options = {
select: args[1],
callback: args[2],
scope: args[3]
};
}
if (view.dataSource.isMultigroupStore) {
if (recordIdx.isEntity) {
record = recordIdx;
}
else {
// eslint-disable-next-line max-len
record = view.store.getAt(Math.min(Math.max(recordIdx, 0), view.store.getCount() - 1));
}
// eslint-disable-next-line max-len
if (!view.dataSource.isExpandingOrCollapsing && view.dataSource.isInCollapsedGroup(record)) {
// we need to make sure all groups the record belongs to are expanded
view.dataSource.expandToRecord(record);
}
recordIdx = view.dataSource.indexOf(record);
}
// If we have a grouping summary feature rendering the view in groups,
// first, ensure that the record's group is expanded,
// then work out which record in the groupStore the record is at.
// eslint-disable-next-line max-len
else if ((groupingFeature = view.dataSource.groupingFeature) && (groupingFeature.collapsible)) {
if (recordIdx.isEntity) {
record = recordIdx;
}
else {
record = view.store.getAt(
Math.min(Math.max(recordIdx, 0), view.store.getCount() - 1)
);
}
metaGroup = groupingFeature.getMetaGroup(record);
if (metaGroup && metaGroup.isCollapsed) {
if (!groupingFeature.isExpandingOrCollapsing && record !== metaGroup.placeholder) {
groupingFeature.expand(groupingFeature.getGroup(record).getGroupKey());
total = store.getCount();
recordIdx = groupingFeature.indexOf(record);
}
else {
// If we've just been collapsed, then the only record we have is
// the wrapped placeholder
record = metaGroup.placeholder;
recordIdx = groupingFeature.indexOfPlaceholder(record);
}
}
else {
recordIdx = groupingFeature.indexOf(record);
}
}
else {
if (recordIdx.isEntity) {
record = recordIdx;
recordIdx = store.indexOf(record);
// Currently loaded pages do not contain the passed record, we cannot proceed.
if (recordIdx === -1) {
//<debug>
Ext.raise('Unknown record passed to BufferedRenderer#scrollTo');
//</debug>
return;
}
}
else {
// Sanitize the requested record index
recordIdx = Math.min(Math.max(recordIdx, 0), total - 1);
record = store.getAt(recordIdx);
}
}
// See if the required row for that record happens to be within the rendered range.
if (record && (targetRow = view.getNode(record))) {
view.grid.ensureVisible(record, options);
// Keep the view immediately replenished when we scroll an existing element into view.
// DOM scroll events fire asynchronously, and we must not leave subsequent code
// without a valid buffered row block.
me.onViewScroll();
return;
}
// Calculate view start index.
// If the required record is above the fold...
if (recordIdx < view.all.startIndex) {
// The startIndex of the new rendered range is a little less
// than the target record index.
direction = -1;
// eslint-disable-next-line max-len
startIdx = Math.max(Math.min(recordIdx - (Math.floor((me.leadingBufferZone + me.trailingBufferZone) / 2)), total - me.viewSize + 1), 0);
endIdx = Math.min(startIdx + me.viewSize - 1, total - 1);
}
// If the required record is below the fold...
else {
// The endIndex of the new rendered range is a little greater
// than the target record index.
direction = 1;
// eslint-disable-next-line max-len
endIdx = Math.min(recordIdx + (Math.floor((me.leadingBufferZone + me.trailingBufferZone) / 2)), total - 1);
startIdx = Math.max(endIdx - (me.viewSize - 1), 0);
}
tableTop = Math.max(startIdx * me.rowHeight, 0);
store.getRange(startIdx, endIdx, {
callback: function(range, start, end) {
// Render the range.
// Pass synchronous flag so that it does it inline, not on a timer.
// Pass fromLockingPartner flag so that it does not inform the lockingPartner.
me.renderRange(start, end, true);
record = store.data.getRange(recordIdx, recordIdx + 1)[0];
targetRow = view.getNode(record);
// bodyTop property must track the translated position of the body
view.body.translate(null, me.bodyTop = tableTop);
// Ensure the scroller knows about the range if we're going down
if (direction === 1 && view.hasVariableRowHeight()) {
me.refreshSize();
}
// Locking partner must render the same range
if (lockingPartner) {
lockingPartner.renderRange(start, end, true);
// Sync all row heights
me.syncRowHeights();
// bodyTop property must track the translated position of the body
lockingPartner.view.body.translate(null, lockingPartner.bodyTop = tableTop);
// Ensure the scroller knows about the range if we're going down
if (direction === 1) {
lockingPartner.refreshSize();
}
}
// The target does not map to a view node.
// Cannot scroll to it.
if (!targetRow) {
return;
}
view.grid.ensureVisible(record, options);
me.scrollTop = me.position = me.scroller.getPosition().y;
if (lockingPartner) {
lockingPartner.position = lockingPartner.scrollTop = me.scrollTop;
}
}
});
},
onViewScroll: function(scroller, x, scrollTop) {
var me = this,
bodyDom = me.view.body.dom,
store = me.store,
totalCount = (store.getCount()),
distance, direction;
// May be directly called with no args, as well as from the Scroller's scroll event
if (scrollTop == null) {
scrollTop = me.scroller.getPosition().y;
}
me.scrollTop = scrollTop;
// Because lockable assemblies now only have one Y scroller,
// initially hidden grids (one side may begin with all the columns)
// still get the scroll notification, but may not have any DOM
// to scroll.
if (bodyDom) {
// Only check for nearing the edge if we are enabled, and if there is overflow
// beyond our view bounds. If there is no paging to be done
// (Store's dataset is all in memory) we will be disabled.
if (!(me.disabled || totalCount < me.viewSize)) {
distance = scrollTop - me.position;
direction = distance > 0 ? 1 : -1;
// Moved at least 20 pixels, or changed direction, so test whether the numFromEdge
// is triggered
if (Math.abs(distance) >= 20 || (direction !== me.lastScrollDirection)) {
me.lastScrollDirection = direction;
me.handleViewScroll(me.lastScrollDirection, distance);
}
}
}
},
handleViewScroll: function(direction, vscrollDistance) {
var me = this,
rows = me.view.all,
store = me.store,
storeCount = store.getCount(),
viewSize = me.viewSize,
lastItemIndex = storeCount - 1,
maxRequestStart = Math.max(0, storeCount - viewSize),
requestStart,
requestEnd;
// We're scrolling up
if (direction === -1) {
// If table starts at record zero, we have nothing to do
if (rows.startIndex) {
if (me.topOfViewCloseToEdge()) {
requestStart = Math.max(0, me.getLastVisibleRowIndex() + me.trailingBufferZone -
viewSize);
// If, having scrolled up, a variableRowHeight calculation based
// upon scrolTop/rowHeight yields an obviously wrong value,
// then constrain it to a calculated value.
// We CANNOT just Math.min it with maxRequestStart, because we may already
// be at maxRequestStart, and asking to render the same block
// will have no effect.
// We calculate a start value a few rows above the current startIndex.
if (requestStart > rows.startIndex) {
requestStart = Math.max(
0, rows.startIndex + Math.floor(vscrollDistance / me.rowHeight)
);
}
}
}
}
// We're scrolling down
else {
// If table ends at last record, we have nothing to do
if (rows.endIndex < lastItemIndex) {
if (me.bottomOfViewCloseToEdge()) {
// eslint-disable-next-line max-len
requestStart = Math.max(0, Math.min(me.getFirstVisibleRowIndex() - me.trailingBufferZone, maxRequestStart));
}
}
}
// View is OK at this scroll. Advance loadId so that any load requests in flight do not
// result in rendering upon their return.
if (requestStart == null) {
// View is still valid at this scroll position.
// Do not trigger a handleViewScroll call until *ANOTHER* 20 pixels have scrolled by.
me.position = me.scrollTop;
me.loadId++;
}
// We scrolled close to the edge and the Store needs reloading
else {
requestEnd = Math.min(requestStart + viewSize - 1, lastItemIndex);
// viewSize was calculated too small due to small sample row count with some skewed
// item height in there such as a tall group header item. Bump range
// down by one in this case.
if (me.variableRowHeight && requestEnd === rows.endIndex &&
requestEnd < lastItemIndex) {
requestEnd++;
requestStart++;
}
// If calculated view range has moved, then render it and return the fact
// that the scroll was handled.
if (requestStart !== rows.startIndex || requestEnd !== rows.endIndex) {
me.scroller.trackingScrollTop = me.scrollTop;
me.renderRange(requestStart, requestEnd);
return true;
}
}
},
bottomOfViewCloseToEdge: function() {
var me = this;
if (me.variableRowHeight) {
return me.bodyTop + me.bodyHeight < me.scrollTop + me.view.lastBox.height +
(me.numFromEdge * me.rowHeight);
}
else {
return (me.view.all.endIndex - me.getLastVisibleRowIndex()) < me.numFromEdge;
}
},
topOfViewCloseToEdge: function() {
var me = this;
if (me.variableRowHeight) {
// The body top position is within the numFromEdge zone
return me.bodyTop > me.scrollTop - (me.numFromEdge * me.rowHeight);
}
else {
return (me.getFirstVisibleRowIndex() - me.view.all.startIndex) < me.numFromEdge;
}
},
/**
* @private
* Refreshes the current rendered range if possible.
* Optionally refreshes starting at the specified index.
*/
refreshView: function(startIndex, scrollIncrement) {
var me = this,
viewSize = me.viewSize,
view = me.view,
rows = view.all,
store = me.store,
storeCount = store.getCount(),
maxIndex = Math.max(0, storeCount - 1),
lockingPartnerRows = view.lockingPartner && view.lockingPartner.all,
preserveScroll = me.bodyTop && view.preserveScrollOnRefresh || scrollIncrement,
endIndex;
// Empty Store is simple, don't even ask the store
if (!storeCount) {
return me.doRefreshView([], 0, 0);
}
// Store doesn't fill the required view size. Simple start/end calcs.
else if (storeCount < viewSize) {
startIndex = 0;
endIndex = maxIndex;
me.nextRefreshStartIndex = preserveScroll ? null : 0;
}
// We're starting from nothing, but there's a locking partner with the range info,
// so match that
else if (startIndex == null && !rows.getCount() && lockingPartnerRows &&
lockingPartnerRows.getCount()) {
startIndex = lockingPartnerRows.startIndex;
endIndex = Math.min(lockingPartnerRows.endIndex, startIndex + viewSize - 1, maxIndex);
}
// Work out range to refresh
else {
if (startIndex == null) {
// Use a nextRefreshStartIndex as set by a load operation
// in which we are maintaining scroll position
if (me.nextRefreshStartIndex != null && !preserveScroll) {
startIndex = me.nextRefreshStartIndex;
}
else {
startIndex = rows.startIndex;
}
me.nextRefreshStartIndex = null;
}
// New start index should be current start index unless that's now too close
// to the end of the store to yield a full view, in which case work back
// from the end of the store. Ensure we don't go negative.
startIndex = Math.max(0, Math.min(startIndex, maxIndex - viewSize + 1));
// New end index works forward from the new start index ensuring
// we don't walk off the end
endIndex = Math.min(startIndex + viewSize - 1, maxIndex);
if (endIndex - startIndex + 1 > viewSize) {
startIndex = endIndex - viewSize + 1;
}
}
if (startIndex === 0 && endIndex === -1) {
me.doRefreshView([], 0, 0);
}
else {
store.getRange(startIndex, endIndex, {
callback: me.doRefreshView,
scope: me
});
}
},
doRefreshView: function(range, startIndex, endIndex) {
var me = this,
view = me.view,
scroller = me.scroller,
rows = view.all,
previousStartIndex = rows.startIndex,
previousEndIndex = rows.endIndex,
prevRowCount = rows.getCount(),
viewMoved = startIndex !== rows.startIndex && !me.isStoreLoading,
calculatedTop = -1,
previousFirstItem, previousLastItem, scrollIncrement, restoreFocus;
me.isStoreLoading = false;
// So that listeners to the itemremove events know that its because of a refresh.
// And so that this class's refresh listener knows to ignore it.
view.refreshing = me.refreshing = true;
if (view.refreshCounter) {
// Give CellEditors or other transient in-cell items a chance to get out of the way.
if (view.hasListeners.beforerefresh &&
view.fireEvent('beforerefresh', view) === false) {
return view.refreshNeeded = view.refreshing = me.refreshing = false;
}
// If focus was in any way in the view, whether actionable or navigable,
// this will return a function which will restore that state.
restoreFocus = view.saveFocusState();
view.clearViewEl(true);
view.refreshCounter++;
if (range.length) {
view.doAdd(range, startIndex);
if (viewMoved) {
// Try to find overlap between newly rendered block and old block
previousFirstItem = rows.item(previousStartIndex, true);
previousLastItem = rows.item(previousEndIndex, true);
// Work out where to move the view top if there is overlap
if (previousFirstItem) {
scrollIncrement = -previousFirstItem.offsetTop;
}
else if (previousLastItem) {
scrollIncrement = rows.last(true).offsetTop - previousLastItem.offsetTop;
}
// If there was an overlap, we know exactly where to move the view
if (scrollIncrement) {
calculatedTop = Math.max(me.bodyTop + scrollIncrement, 0);
me.scrollTop = calculatedTop ? me.scrollTop + scrollIncrement : 0;
}
// No overlap: calculate the a new body top and scrollTop.
else {
calculatedTop = startIndex * me.rowHeight;
// eslint-disable-next-line max-len
me.scrollTop = Math.max(calculatedTop + me.rowHeight * (calculatedTop < me.bodyTop ? me.leadingBufferZone : me.trailingBufferZone), 0);
}
}
}
// Clearing the view.
// Ensure we jump to top.
// Apply empty text.
else {
me.scrollTop = calculatedTop = me.position = 0;
view.addEmptyText();
}
// Keep scroll and rendered block positions synched if there is scrolling.
if (calculatedTop !== -1) {
me.setBodyTop(calculatedTop);
me.doVerticalScroll(scroller, me.scrollTop, true);
}
// Correct scroll range
me.refreshSize();
view.refreshSize(rows.getCount() !== prevRowCount);
view.fireItemMutationEvent('refresh', view, range);
// If focus was in any way in this view, this will restore it
restoreFocus();
if (view.preserveScrollOnRefresh && restoreFocus !== Ext.emptyFn) {
me.doVerticalScroll(scroller, me.scrollTop, true);
}
view.headerCt.setSortState();
}
else {
view.refresh();
}
//<debug>
// If there are columns to trigger rendering, and the rendered block is not
// either the view size or, if store count less than view size, the store count,
// then there's a bug.
if (view.getVisibleColumnManager().getColumns().length &&
rows.getCount() !== Math.min(me.store.getCount(), me.viewSize)) {
Ext.raise('rendered block refreshed at ' + rows.getCount() +
' rows while BufferedRenderer view size is ' + me.viewSize);
}
//</debug>
view.refreshNeeded = view.refreshing = me.refreshing = false;
},
renderRange: function(start, end, forceSynchronous) {
var me = this,
rows = me.view.all,
store = me.store;
// We're being told to render what we already have rendered.
if (rows.startIndex === start && rows.endIndex === end) {
return;
}
// Skip if we are being asked to render exactly the rows that we already have.
// This can happen if the viewSize has to be recalculated
// (due to either a data refresh or a view resize event) but the calculated size
// ends up the same.
if (!(start === rows.startIndex && end === rows.endIndex)) {
// If range is available synchronously, process it now.
if (store.rangeCached(start, end)) {
me.cancelLoad();
if (me.synchronousRender || forceSynchronous) {
me.onRangeFetched(null, start, end);
}
else {
if (!me.renderTask) {
me.renderTask = new Ext.util.DelayedTask(me.onRangeFetched, me);
}
// Render the new range very soon after this scroll event handler exits.
// If scrolling very quickly, a few more scroll events may fire before
// the render takes place. Each one will just *update* the arguments with which
// the pending invocation is called.
me.renderTask.delay(-1, null, null, [null, start, end]);
}
}
// Required range is not in the prefetch buffer. Ask the store to prefetch it.
else {
me.attemptLoad(start, end, me.scrollTop);
}
}
},
onRangeFetched: function(range, start, end) {
var me = this,
view = me.view,
scroller = me.scroller,
viewEl = view.el,
rows = view.all,
increment = 0,
calculatedTop,
partnerView = !me.doNotMirror && view.lockingPartner,
partnerColManger = partnerView && partnerView.getVisibleColumnManager(),
partnerViewConfigured = partnerColManger && partnerColManger.getColumns().length,
lockingPartner = partnerViewConfigured && partnerView.bufferedRenderer,
partnerRows = partnerViewConfigured && partnerView.all,
variableRowHeight = me.variableRowHeight,
doSyncRowHeight = partnerViewConfigured && partnerView.ownerCt.isVisible() && (
view.ownerGrid.syncRowHeight ||
view.ownerGrid.syncRowHeightOnNextLayout ||
(lockingPartner.variableRowHeight !== variableRowHeight)
),
activeEl, focusedView, i, newRows, newTop, noOverlap,
oldStart, partnerNewRows, pos, removeCount, topAdditionSize, topBufferZone, records;
// View may have been destroyed since the DelayedTask was kicked off.
if (view.destroyed) {
return;
}
// If called as a callback from the Store, the range will be passed,
// if called from renderRange, it won't
if (range) {
// Re-cache the scrollTop if there has been an asynchronous call to the server.
me.scrollTop = scroller.getPosition().y;
}
else {
range = me.store.getRange(start, end);
// Store may have been cleared since the DelayedTask was kicked off.
if (!range) {
return;
}
}
// If we contain focus now, but do not when we have rendered the new rows,
// we must focus the view el.
activeEl = Ext.fly(Ext.Element.getActiveElement());
if (viewEl.contains(activeEl)) {
focusedView = view;
}
else if (partnerView && partnerView.el.contains(activeEl)) {
focusedView = partnerView;
}
// In case the browser does fire synchronous focus events when a focused element
// is derendered...
if (focusedView) {
activeEl.suspendFocusEvents();
}
// Best guess rendered block position is start row index * row height.
// We can use this as bodyTop if the row heights are all standard.
// We MUST use this as bodyTop if the scroll is a teleporting scroll.
// If we are incrementally scrolling, we add the rows to the bottom, and
// remove a block of rows from the top.
// The bodyTop is then incremented by the height of the removed block to keep
// the visuals the same.
//
// We cannot always use the calculated top, and compensate by adjusting the scroll position
// because that would break momentum scrolling on DOM scrolling platforms, and would be
// immediately undone in the next frame update of a momentum scroll on touch scroll
// platforms.
calculatedTop = start * me.rowHeight;
// The new range encompasses the current range. Refresh and keep the scroll position stable
if (start < rows.startIndex && end > rows.endIndex) {
// How many rows will be added at top. So that we can reposition the table
// to maintain scroll position
topAdditionSize = rows.startIndex - start;
// MUST use View method so that itemremove events are fired so widgets can be recycled.
view.clearViewEl(true);
newRows = view.doAdd(range, start);
view.fireItemMutationEvent('itemadd', range, start, newRows, view);
// Keep other side's rendered block the same
if (lockingPartner) {
partnerView.clearViewEl(true);
partnerNewRows = partnerView.doAdd(range, start);
partnerView.fireItemMutationEvent('itemadd', range, start, partnerNewRows,
partnerView);
// We're going to be doing measurement of newRows
// Ensure heights are synced first
if (doSyncRowHeight) {
me.syncRowHeights(newRows, partnerNewRows);
doSyncRowHeight = false;
}
}
for (i = 0; i < topAdditionSize; i++) {
increment -= me.grid.getElementHeight(newRows[i]);
}
// We've just added a bunch of rows to the top of our range,
// so move upwards to keep the row appearance stable
newTop = me.bodyTop + increment;
}
else {
// No overlapping nodes; we'll need to render the whole range.
// teleported flag is set in getFirstVisibleRowIndex/getLastVisibleRowIndex if
// the table body has moved outside the viewport bounds
noOverlap = me.teleported || start > rows.endIndex || end < rows.startIndex;
if (noOverlap) {
view.clearViewEl(true);
me.teleported = false;
}
if (!rows.getCount()) {
newRows = view.doAdd(range, start);
view.fireItemMutationEvent('itemadd', range, start, newRows, view);
// Keep other side's rendered block the same
if (lockingPartner) {
partnerView.clearViewEl(true);
partnerNewRows = lockingPartner.view.doAdd(range, start);
partnerView.fireItemMutationEvent('itemadd', range, start, partnerNewRows,
partnerView);
}
newTop = calculatedTop;
// Adjust the bodyTop to place the data correctly around the scroll vieport
if (noOverlap && variableRowHeight) {
topBufferZone = me.scrollTop < me.position
? me.leadingBufferZone
: me.trailingBufferZone;
// Can't calculate a new top if there are fewer than topBufferZone rows above us
if (start > topBufferZone) {
// eslint-disable-next-line max-len
newTop = Math.max(me.scrollTop - rows.item(rows.startIndex + topBufferZone - 1, true).offsetTop, 0);
}
}
}
// Moved down the dataset (content moved up): remove rows from top, add to end
else if (end > rows.endIndex) {
removeCount = Math.max(start - rows.startIndex, 0);
// We only have to bump the table down by the height of removed rows
// if rows are not a standard size
if (variableRowHeight) {
increment = rows.item(rows.startIndex + removeCount, true).offsetTop;
}
records = Ext.Array.slice(range, rows.endIndex + 1 - start);
newRows = rows.scroll(records, 1, removeCount);
if (lockingPartner) {
partnerNewRows = partnerRows.scroll(records, 1, removeCount);
}
// We only have to bump the table down by the height of removed rows
// if rows are not a standard size
if (variableRowHeight) {
// Bump the table downwards by the height scraped off the top
newTop = me.bodyTop + increment;
}
// If the rows are standard size, then the calculated top will be correct
else {
newTop = calculatedTop;
}
}
// Moved up the dataset: remove rows from end, add to top
else {
removeCount = Math.max(rows.endIndex - end, 0);
oldStart = rows.startIndex;
records = Ext.Array.slice(range, 0, rows.startIndex - start);
newRows = rows.scroll(records, -1, removeCount);
if (lockingPartner) {
partnerNewRows = partnerRows.scroll(records, -1, removeCount);
}
// We only have to bump the table up by the height of top-added rows if
// rows are not a standard size. If they are standard, calculatedTop is correct.
// Sync the row heights *before* calculating the newTop and increment
if (doSyncRowHeight) {
me.syncRowHeights(newRows, partnerNewRows);
doSyncRowHeight = false;
}
if (variableRowHeight) {
// Bump the table upwards by the height added to the top
newTop = me.bodyTop - rows.item(oldStart, true).offsetTop;
// We've arrived at row zero...
if (!rows.startIndex) {
// But the calculated top position is out. It must be zero at this point
// We adjust the scroll position to keep visual position of table the same.
if (newTop) {
me.doVerticalScroll(scroller, me.scrollTop -= newTop);
newTop = 0;
}
}
// Not at zero yet, but the position has moved into negative range
else if (newTop < 0) {
increment = rows.startIndex * me.rowHeight;
me.doVerticalScroll(scroller, me.scrollTop += increment);
newTop = me.bodyTop + increment;
}
}
// If the rows are standard size, then the calculated top will be correct
else {
newTop = calculatedTop;
}
}
// The position property is the scrollTop value *at which the table was last correct*
// MUST be set at table render/adjustment time
me.position = me.scrollTop;
}
// A view contained focus at the start, check whether activeEl has been derendered.
// Focus the cell's column header if so.
if (focusedView) {
// Restore active element's focus processing.
activeEl.resumeFocusEvents();
if (!focusedView.el.contains(activeEl)) {
pos = focusedView.actionableMode
? focusedView.actionPosition
: focusedView.lastFocused;
if (pos && pos.column) {
// we set the rendering rows to true here so the actionables know
// that view is forcing the onFocusLeave method here
focusedView.renderingRows = true;
focusedView.onFocusLeave({});
focusedView.renderingRows = false;
me.lastFocusedElement = focusedView;
me.newFocusTarget = me.getNewFocusTarget(pos);
me.newFocusTarget.focus();
me.lastFocusedMultiRange = false;
me.colHeader = pos.column;
}
}
}
if (me.lastFocusedElement && me.newFocusTarget === me.colHeader &&
!me.lastFocusedMultiRange && !view.actionableMode) {
view = me.lastFocusedElement;
if (Ext.Array.contains(me.store.getRange(rows.startIndex, rows.endIndex),
view.lastFocused.record)) {
view.focus();
me.lastFocusedMultiRange = true;
}
}
// Calculate position of item container.
newTop = Math.max(Math.floor(newTop), 0);
if (view.positionBody) {
me.setBodyTop(newTop, true);
}
// Sync the other side to exactly the same range from the dataset.
// Then ensure that we are still at exactly the same scroll position.
if (lockingPartner) {
// Locking partner BufferedRenderer must not react to the scroll.
lockingPartner.scrollTop = me.scrollTop;
if (lockingPartner.bodyTop !== newTop) {
lockingPartner.setBodyTop(newTop, true);
}
if (doSyncRowHeight) {
me.syncRowHeights(newRows, partnerNewRows);
}
}
else if (variableRowHeight) {
delete me.rowHeight;
me.refreshSize();
}
//<debug>
// If there are columns to trigger rendering, and the rendered block
// is not either the view size or, if store count less than view size,
// the store count, then there's a bug.
if (view.getVisibleColumnManager().getColumns().length &&
rows.getCount() !== Math.min(me.store.getCount(), me.viewSize)) {
Ext.raise('rendered block refreshed at ' + rows.getCount() +
' rows while BufferedRenderer view size is ' + me.viewSize);
}
//</debug>
return newRows;
},
/**
* Gets the next focus target based on the position
* @param {Ext.grid.CellContext} pos
* @returns {Ext.Component}
* @since 6.2.2
*/
getNewFocusTarget: function(pos) {
var view = pos.view,
grid = view.grid,
column = pos.column,
hiddenHeaders = column.isHidden() || grid.hideHeaders,
tabbableItems;
// Focus MUST NOT silently die due to DOM removal. Focus will be moved
// in the following order as available:
// Try focusing the contextual column header
if (column.focusable && !hiddenHeaders) {
return column;
}
tabbableItems = column.el.findTabbableElements();
// Failing that, look inside it for a tabbable element
if (tabbableItems && tabbableItems.length) {
return tabbableItems[0];
}
// Failing that, find the available focus target of the grid or focus the view
return grid.findFocusTarget() || view.el;
},
syncRowHeights: function(itemEls, partnerItemEls) {
var me = this,
ln = 0,
otherLn = 1, // Different initial values so that all items are synched
mySynchronizer = [],
otherSynchronizer = [],
RowSynchronizer = Ext.grid.locking.RowSynchronizer,
i, rowSync;
if (itemEls && partnerItemEls) {
ln = itemEls.length;
otherLn = partnerItemEls.length;
}
// The other side might not quite by in scroll sync with us, in which case
// it may have gone a different path way and rolled some rows into
// the rendered block where we may have re-rendered the whole thing.
// If this has happened, fall back to syncing all rows.
if (ln !== otherLn) {
itemEls = me.view.all.slice();
partnerItemEls = me.view.lockingPartner.all.slice();
ln = otherLn = itemEls.length;
}
for (i = 0; i < ln; i++) {
mySynchronizer[i] = rowSync = new RowSynchronizer(me.view, itemEls[i]);
rowSync.measure();
}
for (i = 0; i < otherLn; i++) {
otherSynchronizer[i] = rowSync =
new RowSynchronizer(me.view.lockingPartner, partnerItemEls[i]);
rowSync.measure();
}
for (i = 0; i < ln; i++) {
mySynchronizer[i].finish(otherSynchronizer[i]);
otherSynchronizer[i].finish(mySynchronizer[i]);
}
// Ensure that both BufferedRenderers have the same idea about scroll range and row height
me.syncRowHeightsFinish();
},
syncRowHeightsFinish: function() {
var me = this,
view = me.view,
lockingPartner = view.lockingPartner.bufferedRenderer,
ownerGrid = view.ownerGrid,
scrollable = view.getScrollable();
ownerGrid.syncRowHeightOnNextLayout = false;
// Now that row heights have potentially changed, both BufferedRenderers
// have to re-evaluate what they think the average rowHeight is
// based on the synchronized-height rows.
//
// If the view has not been layed out, then the upcoming first resize event
// will trigger the needed refreshSize call; See onViewRefresh -
// If control arrives there and the componentLayoutCounter is zero and
// there is variableRowHeight, it schedules itself to be run on boxready
// so refreshSize will be called there for the first time.
if (view.componentLayoutCounter) {
delete me.rowHeight;
me.refreshSize();
delete lockingPartner.rowHeight;
lockingPartner.refreshSize();
}
// Component layout only restores the scroller's state for managed layouts
// here we need to make sure the scroller is restores after the rows sync
if (scrollable) {
scrollable.restoreState();
}
},
setBodyTop: function(bodyTop, skipStretchView) {
var me = this,
view = me.view,
rows = view.all,
store = me.store,
body = view.body;
if (!body.dom) {
// The view may be rendered, but the body element not attached.
return;
}
me.translateBody(body, bodyTop);
// If this is the last page, correct the scroll range to be just enough to fit.
if (me.variableRowHeight) {
me.bodyHeight = me.grid.getElementHeight(body.dom);
// We are displaying the last row, so ensure the scroll range
// finishes exactly at the bottom of the view body
if (rows.endIndex === store.getCount() - 1) {
me.scrollHeight = bodyTop + me.bodyHeight - 1;
}
// Not last row - recalculate scroll range
else {
me.scrollHeight = me.getScrollHeight();
}
if (!skipStretchView) {
me.stretchView(view, me.scrollHeight);
}
}
else {
// If we have fixed row heights, calculate rendered block height
// without forcing a layout
me.bodyHeight = rows.getCount() * me.rowHeight;
}
},
translateBody: function(body, bodyTop) {
body.translate(null, this.bodyTop = bodyTop);
},
getFirstVisibleRowIndex: function(startRow, endRow, viewportTop, viewportBottom) {
var me = this,
view = me.view,
rows = view.all,
elements = rows.elements,
clientHeight = me.viewClientHeight,
target,
targetTop,
bodyTop = me.bodyTop;
// If variableRowHeight, we have to search for the first row who's bottom edge
// is within the viewport
if (rows.getCount() && me.variableRowHeight) {
if (!arguments.length) {
startRow = rows.startIndex;
endRow = rows.endIndex;
viewportTop = me.scrollTop;
viewportBottom = viewportTop + clientHeight;
// Teleported so that body is outside viewport: Use rowHeight calculation
if (bodyTop > viewportBottom || bodyTop + me.bodyHeight < viewportTop) {
me.teleported = true;
return Math.floor(me.scrollTop / me.rowHeight);
}
// In first, non-recursive call, begin targeting the most likely first row
target = startRow + Math.min(me.numFromEdge + ((me.lastScrollDirection === -1)
? me.leadingBufferZone
: me.trailingBufferZone), Math.floor((endRow - startRow) / 2));
}
else {
if (startRow === endRow) {
return endRow;
}
target = startRow + Math.floor((endRow - startRow) / 2);
}
targetTop = bodyTop + elements[target].offsetTop;
// If target is entirely above the viewport, chop downwards
if (targetTop + me.grid.getElementHeight(elements[target]) <= viewportTop) {
return me.getFirstVisibleRowIndex(target + 1, endRow, viewportTop, viewportBottom);
}
// Target is first
if (targetTop <= viewportTop) {
return target;
}
// Not narrowed down to 1 yet; chop upwards
else if (target !== startRow) {
return me.getFirstVisibleRowIndex(startRow, target - 1, viewportTop,
viewportBottom);
}
}
return Math.floor(me.scrollTop / me.rowHeight);
},
/**
* Returns the index of the last row in your table view deemed to be visible.
* @return {Number}
* @private
*/
getLastVisibleRowIndex: function(startRow, endRow, viewportTop, viewportBottom) {
var me = this,
view = me.view,
rows = view.all,
elements = rows.elements,
clientHeight = me.viewClientHeight,
target,
targetTop, targetBottom,
bodyTop = me.bodyTop;
// If variableRowHeight, we have to search for the first row who's bottom edge
// is below the bottom of the viewport
if (rows.getCount() && me.variableRowHeight) {
if (!arguments.length) {
startRow = rows.startIndex;
endRow = rows.endIndex;
viewportTop = me.scrollTop;
viewportBottom = viewportTop + clientHeight;
// Teleported so that body is outside viewport: Use rowHeight calculation
if (bodyTop > viewportBottom || bodyTop + me.bodyHeight < viewportTop) {
me.teleported = true;
return Math.floor(me.scrollTop / me.rowHeight) +
Math.ceil(clientHeight / me.rowHeight);
}
// In first, non-recursive call, begin targeting the most likely last row
target = endRow - Math.min(me.numFromEdge + ((me.lastScrollDirection === 1)
? me.leadingBufferZone
: me.trailingBufferZone), Math.floor((endRow - startRow) / 2));
}
else {
if (startRow === endRow) {
return endRow;
}
target = startRow + Math.floor((endRow - startRow) / 2);
}
targetTop = bodyTop + elements[target].offsetTop;
// If target is entirely below the viewport, chop upwards
if (targetTop > viewportBottom) {
return me.getLastVisibleRowIndex(startRow, target - 1, viewportTop, viewportBottom);
}
targetBottom = targetTop + me.grid.getElementHeight(elements[target]);
// Target is last
if (targetBottom >= viewportBottom) {
return target;
}
// Not narrowed down to 1 yet; chop downwards
else if (target !== endRow) {
return me.getLastVisibleRowIndex(target + 1, endRow, viewportTop, viewportBottom);
}
}
return Math.min(me.getFirstVisibleRowIndex() + Math.ceil(clientHeight / me.rowHeight),
rows.endIndex);
},
getScrollHeight: function() {
var me = this,
view = me.view,
rows = view.all,
store = me.store,
recCount = store.getCount(),
rowCount = rows.getCount(),
row, rowHeight, borderWidth, scrollHeight;
if (!recCount) {
return 0;
}
if (!me.hasOwnProperty('rowHeight')) {
if (rowCount) {
if (me.variableRowHeight) {
me.rowHeight = Math.floor(me.bodyHeight / rowCount);
}
else {
row = rows.first();
// here, buffered rows takes time to render ,
// adding null check to 'row' to avoid uncaught errors.
rowHeight = row && row.getHeight();
// In IE8 we're adding bottom border on all the rows to work around
// the lack of :last-child selector, and we compensate that by setting
// a negative top margin that equals the border width, so that top and
// bottom borders overlap on adjacent rows. Negative margin does not
// affect the row's reported height though so we have to compensate
// for that effectively invisible additional border width here.
if (Ext.isIE8) {
borderWidth = row.getBorderWidth('b');
if (borderWidth > 0) {
rowHeight -= borderWidth;
}
}
me.rowHeight = rowHeight;
}
}
else {
delete me.rowHeight;
}
}
if (me.variableRowHeight) {
// If this is the last page, ensure the scroll range is exactly enough
// to scroll to the end of the rendered block.
if (rows.endIndex === recCount - 1) {
scrollHeight = me.bodyTop + me.bodyHeight - 1;
}
// Calculate the scroll range based upon measured row height and our scrollPosition.
else {
scrollHeight = Math.floor((recCount - rowCount) * me.rowHeight) + me.bodyHeight;
// If there's a discrepancy between the boy position we have scrolled to,
// and the calculated position, account for that in the scroll range
// so that we have enough range to scroll all the data into view.
scrollHeight += me.bodyTop - rows.startIndex * me.rowHeight;
}
}
else {
scrollHeight = Math.floor(recCount * me.rowHeight);
}
return (me.scrollHeight = scrollHeight);
},
getThemeRowHeight: function() {
var me = this,
testEl;
if (!me.themeRowHeight) {
testEl = Ext.getBody().createChild({
cls: Ext.baseCSSPrefix + 'theme-row-height-el'
});
me.self.prototype.themeRowHeight = testEl.dom.offsetHeight;
testEl.destroy();
}
return me.themeRowHeight;
},
attemptLoad: function(start, end, loadScrollPosition) {
var me = this;
if (me.scrollToLoadBuffer) {
if (!me.loadTask) {
me.loadTask = new Ext.util.DelayedTask();
}
me.loadTask.delay(
me.scrollToLoadBuffer, me.doAttemptLoad, me, [start, end, loadScrollPosition]
);
}
else {
me.doAttemptLoad(start, end, loadScrollPosition);
}
},
cancelLoad: function() {
if (this.loadTask) {
this.loadTask.cancel();
}
},
doAttemptLoad: function(start, end, loadScrollPosition) {
var me = this;
// If we were called on a delay, check for destruction
if (!me.destroyed) {
me.store.getRange(start, end, {
loadId: ++me.loadId,
callback: function(range, start, end, options) {
// If our loadId position has not changed since the getRange request started,
// we can continue to render.
// If the scroll position is different to the scroll position which triggered
// the load, ignore it - we don't need the data any more.
if (options.loadId === me.loadId && me.scrollTop === loadScrollPosition) {
me.onRangeFetched(range, start, end);
}
},
fireEvent: false
});
}
},
destroy: function() {
var me = this;
me.cancelLoad();
if (me.store) {
me.unbindStore();
}
// Remove listeners from old grid, view and store
Ext.destroy(me.viewListeners, me.stretcher, me.gridListeners, me.scrollListeners);
me.callParent();
}
});