123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708 |
- <!DOCTYPE html>
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <title>The source code</title>
- <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
- <script type="text/javascript" src="../resources/prettify/prettify.js"></script>
- <style type="text/css">
- .highlight { display: block; background-color: #ddd; }
- </style>
- <script type="text/javascript">
- function highlight() {
- document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
- }
- </script>
- </head>
- <body onload="prettyPrint(); highlight();">
- <pre class="prettyprint lang-js"><span id='Ext-grid-PagingScroller'>/**
- </span> * Implements infinite scrolling of a grid, allowing users can scroll
- * through thousands of records without the performance penalties of
- * renderering all the records on screen at once. The grid should be
- * bound to a *buffered* store with a pageSize specified.
- *
- * The number of rows rendered outside the visible area, and the
- * buffering of pages of data from the remote server for immediate
- * rendering upon scroll can be controlled by configuring the
- * {@link Ext.grid.PagingScroller #verticalScroller}.
- *
- * You can tell it to create a larger table to provide more scrolling
- * before a refresh is needed, and also to keep more pages of records
- * in memory for faster refreshing when scrolling.
- *
- * var myStore = Ext.create('Ext.data.Store', {
- * // ...
- * buffered: true,
- * pageSize: 100,
- * // ...
- * });
- *
- * var grid = Ext.create('Ext.grid.Panel', {
- * // ...
- * autoLoad: true,
- * verticalScroller: {
- * trailingBufferZone: 200, // Keep 200 records buffered in memory behind scroll
- * leadingBufferZone: 5000 // Keep 5000 records buffered in memory ahead of scroll
- * },
- * // ...
- * });
- *
- * ## Implementation notes
- *
- * This class monitors scrolling of the {@link Ext.view.Table
- * TableView} within a {@link Ext.grid.Panel GridPanel} which is using
- * a buffered store to only cache and render a small section of a very
- * large dataset.
- *
- * **NB!** The GridPanel will instantiate this to perform monitoring,
- * this class should never be instantiated by user code. Always use the
- * {@link Ext.panel.Table#verticalScroller verticalScroller} config.
- *
- */
- Ext.define('Ext.grid.PagingScroller', {
- <span id='Ext-grid-PagingScroller-cfg-percentageFromEdge'> /**
- </span> * @cfg
- * @deprecated This config is now ignored.
- */
- percentageFromEdge: 0.35,
- <span id='Ext-grid-PagingScroller-cfg-numFromEdge'> /**
- </span> * @cfg
- * The zone which causes a refresh of the rendered viewport. 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,
- <span id='Ext-grid-PagingScroller-cfg-trailingBufferZone'> /**
- </span> * @cfg
- * The number of extra rows to render on the trailing side of scrolling
- * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
- */
- trailingBufferZone: 5,
- <span id='Ext-grid-PagingScroller-cfg-leadingBufferZone'> /**
- </span> * @cfg
- * The number of extra rows to render on the leading side of scrolling
- * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
- */
- leadingBufferZone: 15,
- <span id='Ext-grid-PagingScroller-cfg-scrollToLoadBuffer'> /**
- </span> * @cfg
- * This is the time in milliseconds to buffer load requests when scrolling the PagingScrollbar.
- */
- scrollToLoadBuffer: 200,
- // private. Initial value of zero.
- viewSize: 0,
- // private. Start at default value
- rowHeight: 21,
- // private. Table extent at startup time
- tableStart: 0,
- tableEnd: 0,
- constructor: function(config) {
- var me = this;
- me.variableRowHeight = config.variableRowHeight;
- me.bindView(config.view);
- Ext.apply(me, config);
- me.callParent(arguments);
- },
- bindView: function(view) {
- var me = this,
- viewListeners = {
- scroll: {
- fn: me.onViewScroll,
- element: 'el',
- scope: me
- },
- render: me.onViewRender,
- resize: me.onViewResize,
- boxready: {
- fn: me.onViewResize,
- scope: me,
- single: true
- },
- // If there are variable row heights, then in beforeRefresh, we have to find a common
- // row so that we can synchronize the table's top position after the refresh.
- // Also flag whether the grid view has focus so that it can be refocused after refresh.
- beforerefresh: me.beforeViewRefresh,
- refresh: me.onViewRefresh,
- scope: me
- },
- storeListeners = {
- guaranteedrange: me.onGuaranteedRange,
- scope: me
- },
- gridListeners = {
- reconfigure: me.onGridReconfigure,
- scope: me
- }, partner;
- // If we need unbinding...
- if (me.view) {
- if (me.view.el) {
- me.view.el.un('scroll', me.onViewScroll, me); // un does not understand the element options
- }
-
- partner = view.lockingPartner;
- if (partner) {
- partner.un('refresh', me.onLockRefresh, me);
- }
-
- me.view.un(viewListeners);
- me.store.un(storeListeners);
- if (me.grid) {
- me.grid.un(gridListeners);
- }
- delete me.view.refreshSize; // Remove the injected refreshSize implementation
- }
- me.view = view;
- me.grid = me.view.up('tablepanel');
- me.store = view.store;
- if (view.rendered) {
- me.viewSize = me.store.viewSize = Math.ceil(view.getHeight() / me.rowHeight) + me.trailingBufferZone + (me.numFromEdge * 2) + me.leadingBufferZone;
- }
-
- partner = view.lockingPartner;
- if (partner) {
- partner.on('refresh', me.onLockRefresh, me);
- }
- me.view.mon(me.store.pageMap, {
- scope: me,
- clear: me.onCacheClear
- });
- // During scrolling we do not need to refresh the height - the Grid height must be set by config or layout in order to create a scrollable
- // table just larger than that, so removing the layout call improves efficiency and removes the flicker when the
- // HeaderContainer is reset to scrollLeft:0, and then resynced on the very next "scroll" event.
- me.view.refreshSize = Ext.Function.createInterceptor(me.view.refreshSize, me.beforeViewrefreshSize, me);
- <span id='Ext-grid-PagingScroller-property-position'> /**
- </span> * @property {Number} position
- * Current pixel scroll position of the associated {@link Ext.view.Table View}.
- */
- me.position = 0;
- // We are created in View constructor. There won't be an ownerCt at this time.
- if (me.grid) {
- me.grid.on(gridListeners);
- } else {
- me.view.on({
- added: function() {
- me.grid = me.view.up('tablepanel');
- me.grid.on(gridListeners);
- },
- single: true
- });
- }
- me.view.on(me.viewListeners = viewListeners);
- me.store.on(storeListeners);
- },
- onCacheClear: function() {
- var me = this;
- // Do not do anything if view is not rendered, or if the reason for cache clearing is store destruction
- if (me.view.rendered && !me.store.isDestroyed) {
- // Temporarily disable scroll monitoring until the scroll event caused by any following *change* of scrollTop has fired.
- // Otherwise it will attempt to process a scroll on a stale view
- me.ignoreNextScrollEvent = me.view.el.dom.scrollTop !== 0;
- me.view.el.dom.scrollTop = 0;
- delete me.lastScrollDirection;
- delete me.scrollOffset;
- delete me.scrollProportion;
- }
- },
- onGridReconfigure: function (grid) {
- this.bindView(grid.view);
- },
- // Ensure that the stretcher element is inserted into the View as the first element.
- onViewRender: function() {
- var me = this,
- view = me.view,
- el = me.view.el,
- stretcher;
- me.stretcher = me.createStretcher(view);
-
- view = view.lockingPartner;
- if (view) {
- stretcher = me.stretcher;
- me.stretcher = new Ext.CompositeElement(stretcher);
- me.stretcher.add(me.createStretcher(view));
- }
- },
-
- createStretcher: function(view) {
- var el = view.el;
- el.setStyle('position', 'relative');
-
- return el.createChild({
- style:{
- position: 'absolute',
- width: '1px',
- height: 0,
- top: 0,
- left: 0
- }
- }, el.dom.firstChild);
- },
-
- onViewResize: function(view, width, height) {
- var me = this,
- newViewSize;
- newViewSize = Math.ceil(height / me.rowHeight) + me.trailingBufferZone + (me.numFromEdge * 2) + me.leadingBufferZone;
- if (newViewSize > me.viewSize) {
- me.viewSize = me.store.viewSize = newViewSize;
- me.handleViewScroll(me.lastScrollDirection || 1);
- }
- },
- // Used for variable row heights. Try to find the offset from scrollTop of a common row
- beforeViewRefresh: function() {
- var me = this,
- view = me.view,
- rows,
- direction;
- // Refreshing can cause loss of focus.
- me.focusOnRefresh = Ext.Element.getActiveElement === view.el.dom;
- // Only need all this is variableRowHeight
- if (me.variableRowHeight) {
- direction = me.lastScrollDirection;
- me.commonRecordIndex = undefined;
- // If we are refreshing in response to a scroll,
- // And we know where the previous start was,
- // and we're not teleporting out of visible range
- // and the view is not empty
- if (direction && (me.previousStart !== undefined) && (me.scrollProportion === undefined) && (rows = view.getNodes()).length) {
- // We have scrolled downwards
- if (direction === 1) {
- // If the ranges overlap, we are going to be able to position the table exactly
- if (me.tableStart <= me.previousEnd) {
- me.commonRecordIndex = rows.length - 1;
- }
- }
- // We have scrolled upwards
- else if (direction === -1) {
- // If the ranges overlap, we are going to be able to position the table exactly
- if (me.tableEnd >= me.previousStart) {
- me.commonRecordIndex = 0;
- }
- }
- // Cache the old offset of the common row from the scrollTop
- me.scrollOffset = -view.el.getOffsetsTo(rows[me.commonRecordIndex])[1];
- // In the new table the common row is at a different index
- me.commonRecordIndex -= (me.tableStart - me.previousStart);
- } else {
- me.scrollOffset = undefined;
- }
- }
- },
- onLockRefresh: function(view) {
- view.table.dom.style.position = 'absolute';
- },
- // Used for variable row heights. Try to find the offset from scrollTop of a common row
- // Ensure, upon each refresh, that the stretcher element is the correct height
- onViewRefresh: function() {
- var me = this,
- store = me.store,
- newScrollHeight,
- view = me.view,
- viewEl = view.el,
- viewDom = viewEl.dom,
- rows,
- newScrollOffset,
- scrollDelta,
- table = view.table.dom,
- tableTop,
- scrollTop;
- // Refresh causes loss of focus
- if (me.focusOnRefresh) {
- viewEl.focus();
- me.focusOnRefresh = false;
- }
- // Scroll events caused by processing in here must be ignored, so disable for the duration
- me.disabled = true;
- // No scroll monitoring is needed if
- // All data is in view OR
- // Store is filtered locally.
- // - scrolling a locally filtered page is obv a local operation within the context of a huge set of pages
- // so local scrolling is appropriate.
- if (store.getCount() === store.getTotalCount() || (store.isFiltered() && !store.remoteFilter)) {
- me.stretcher.setHeight(0);
- me.position = viewDom.scrollTop = 0;
- // Chrome's scrolling went crazy upon zeroing of the stretcher, and left the view's scrollTop stuck at -15
- // This is the only thing that fixes that
- me.setTablePosition('absolute');
- // We remain disabled now because no scrolling is needed - we have the full dataset in the Store
- return;
- }
- me.stretcher.setHeight(newScrollHeight = me.getScrollHeight());
- scrollTop = viewDom.scrollTop;
- // Flag to the refreshSize interceptor that regular refreshSize postprocessing should be vetoed.
- me.isScrollRefresh = (scrollTop > 0);
- // If we have had to calculate the store position from the pure scroll bar position,
- // then we must calculate the table's vertical position from the scrollProportion
- if (me.scrollProportion !== undefined) {
- me.setTablePosition('absolute');
- me.setTableTop((me.scrollProportion ? (newScrollHeight * me.scrollProportion) - (table.offsetHeight * me.scrollProportion) : 0) + 'px');
- } else {
- me.setTablePosition('absolute');
- me.setTableTop((tableTop = (me.tableStart||0) * me.rowHeight) + 'px');
- // ScrollOffset to a common row was calculated in beforeViewRefresh, so we can synch table position with how it was before
- if (me.scrollOffset) {
- rows = view.getNodes();
- newScrollOffset = -viewEl.getOffsetsTo(rows[me.commonRecordIndex])[1];
- scrollDelta = newScrollOffset - me.scrollOffset;
- me.position = (viewDom.scrollTop += scrollDelta);
- }
- // If the table is not fully in view view, scroll to where it is in view.
- // This will happen when the page goes out of view unexpectedly, outside the
- // control of the PagingScroller. For example, a refresh caused by a remote sort or filter reverting
- // back to page 1.
- // Note that with buffered Stores, only remote sorting is allowed, otherwise the locally
- // sorted page will be out of order with the whole dataset.
- else if ((tableTop > scrollTop) || ((tableTop + table.offsetHeight) < scrollTop + viewDom.clientHeight)) {
- me.lastScrollDirection = -1;
- me.position = viewDom.scrollTop = tableTop;
- }
- }
- // Re-enable upon function exit
- me.disabled = false;
- },
-
- setTablePosition: function(position) {
- this.setViewTableStyle(this.view, 'position', position);
- },
-
- setTableTop: function(top){
- this.setViewTableStyle(this.view, 'top', top);
- },
-
- setViewTableStyle: function(view, prop, value) {
- view.el.child('table', true).style[prop] = value;
- view = view.lockingPartner;
-
- if (view) {
- view.el.child('table', true).style[prop] = value;
- }
- },
- beforeViewrefreshSize: function() {
- // Veto the refreshSize if the refresh is due to a scroll.
- if (this.isScrollRefresh) {
- // If we're vetoing refreshSize, attach the table DOM to the View's Flyweight.
- this.view.table.attach(this.view.el.child('table', true));
- return (this.isScrollRefresh = false);
- }
- },
- onGuaranteedRange: function(range, start, end) {
- var me = this,
- ds = me.store;
- // this should never happen
- if (range.length && me.visibleStart < range[0].index) {
- return;
- }
- // Cache last table position in dataset so that if we are using variableRowHeight,
- // we can attempt to locate a common row to align the table on.
- me.previousStart = me.tableStart;
- me.previousEnd = me.tableEnd;
- me.tableStart = start;
- me.tableEnd = end;
- ds.loadRecords(range, {
- start: start
- });
- },
- onViewScroll: function(e, t) {
- var me = this,
- view = me.view,
- lastPosition = me.position;
- me.position = view.el.dom.scrollTop;
- // Flag set when the scrollTop is programatically set to zero upon cache clear.
- // We must not attempt to process that as a scroll event.
- if (me.ignoreNextScrollEvent) {
- me.ignoreNextScrollEvent = false;
- return;
- }
- // Only check for nearing the edge if we are enabled.
- // If there is no paging to be done (Store's dataset is all in memory) we will be disabled.
- if (!me.disabled) {
- me.lastScrollDirection = me.position > lastPosition ? 1 : -1;
- // Check the position so we ignore horizontal scrolling
- if (lastPosition !== me.position) {
- me.handleViewScroll(me.lastScrollDirection);
- }
- }
- },
- handleViewScroll: function(direction) {
- var me = this,
- store = me.store,
- view = me.view,
- viewSize = me.viewSize,
- totalCount = store.getTotalCount(),
- highestStartPoint = totalCount - viewSize,
- visibleStart = me.getFirstVisibleRowIndex(),
- visibleEnd = me.getLastVisibleRowIndex(),
- el = view.el.dom,
- requestStart,
- requestEnd;
- // Only process if the total rows is larger than the visible page size
- if (totalCount >= viewSize) {
- // This is only set if we are using variable row height, and the thumb is dragged so that
- // There are no remaining visible rows to vertically anchor the new table to.
- // In this case we use the scrollProprtion to anchor the table to the correct relative
- // position on the vertical axis.
- me.scrollProportion = undefined;
- // We're scrolling up
- if (direction == -1) {
- // If table starts at record zero, we have nothing to do
- if (me.tableStart) {
- if (visibleStart !== undefined) {
- if (visibleStart < (me.tableStart + me.numFromEdge)) {
- requestStart = Math.max(0, visibleEnd + me.trailingBufferZone - viewSize);
- }
- }
- // The only way we can end up without a visible start is if, in variableRowHeight mode, the user drags
- // the thumb up out of the visible range. In this case, we have to estimate the start row index
- else {
- // If we have no visible rows to orientate with, then use the scroll proportion
- me.scrollProportion = el.scrollTop / (el.scrollHeight - el.clientHeight);
- requestStart = Math.max(0, totalCount * me.scrollProportion - (viewSize / 2) - me.numFromEdge - ((me.leadingBufferZone + me.trailingBufferZone) / 2));
- }
- }
- }
- // We're scrolling down
- else {
- if (visibleStart !== undefined) {
- if (visibleEnd > (me.tableEnd - me.numFromEdge)) {
- requestStart = Math.max(0, visibleStart - me.trailingBufferZone);
- }
- }
- // The only way we can end up without a visible end is if, in variableRowHeight mode, the user drags
- // the thumb down out of the visible range. In this case, we have to estimate the start row index
- else {
- // If we have no visible rows to orientate with, then use the scroll proportion
- me.scrollProportion = el.scrollTop / (el.scrollHeight - el.clientHeight);
- requestStart = totalCount * me.scrollProportion - (viewSize / 2) - me.numFromEdge - ((me.leadingBufferZone + me.trailingBufferZone) / 2);
- }
- }
- // We scrolled close to the edge and the Store needs reloading
- if (requestStart !== undefined) {
- // The calculation walked off the end; Request the highest possible chunk which starts on an even row count (Because of row striping)
- if (requestStart > highestStartPoint) {
- requestStart = highestStartPoint & ~1;
- requestEnd = totalCount - 1;
- }
- // Make sure first row is even to ensure correct even/odd row striping
- else {
- requestStart = requestStart & ~1;
- requestEnd = requestStart + viewSize - 1;
- }
- // If range is satsfied within the prefetch buffer, then just draw it from the prefetch buffer
- if (store.rangeCached(requestStart, requestEnd)) {
- me.cancelLoad();
- store.guaranteeRange(requestStart, requestEnd);
- }
- // Required range is not in the prefetch buffer. Ask the store to prefetch it.
- // We will recieve a guaranteedrange event when that is done.
- else {
- me.attemptLoad(requestStart, requestEnd);
- }
- }
- }
- },
- getFirstVisibleRowIndex: function() {
- var me = this,
- view = me.view,
- scrollTop = view.el.dom.scrollTop,
- rows,
- count,
- i,
- rowBottom;
- if (me.variableRowHeight) {
- rows = view.getNodes();
- count = rows.length;
- if (!count) {
- return;
- }
- rowBottom = Ext.fly(rows[0]).getOffsetsTo(view.el)[1];
- for (i = 0; i < count; i++) {
- rowBottom += rows[i].offsetHeight;
- // Searching for the first visible row, and off the bottom of the clientArea, then there's no visible first row!
- if (rowBottom > view.el.dom.clientHeight) {
- return;
- }
- // Return the index *within the total dataset* of the first visible row
- // We cannot use the loop index to offset from the table's start index because of possible intervening group headers.
- if (rowBottom > 0) {
- return view.getRecord(rows[i]).index;
- }
- }
- } else {
- return Math.floor(scrollTop / me.rowHeight);
- }
- },
- getLastVisibleRowIndex: function() {
- var me = this,
- store = me.store,
- view = me.view,
- clientHeight = view.el.dom.clientHeight,
- rows,
- count,
- i,
- rowTop;
- if (me.variableRowHeight) {
- rows = view.getNodes();
- if (!rows.length) {
- return;
- }
- count = store.getCount() - 1;
- rowTop = Ext.fly(rows[count]).getOffsetsTo(view.el)[1] + rows[count].offsetHeight;
- for (i = count; i >= 0; i--) {
- rowTop -= rows[i].offsetHeight;
- // Searching for the last visible row, and off the top of the clientArea, then there's no visible last row!
- if (rowTop < 0) {
- return;
- }
- // Return the index *within the total dataset* of the last visible row.
- // We cannot use the loop index to offset from the table's start index because of possible intervening group headers.
- if (rowTop < clientHeight) {
- return view.getRecord(rows[i]).index;
- }
- }
- } else {
- return me.getFirstVisibleRowIndex() + Math.ceil(clientHeight / me.rowHeight) + 1;
- }
- },
- getScrollHeight: function() {
- var me = this,
- view = me.view,
- table,
- firstRow,
- store = me.store,
- deltaHeight = 0,
- doCalcHeight = !me.hasOwnProperty('rowHeight');
- if (me.variableRowHeight) {
- table = me.view.table.dom;
- if (doCalcHeight) {
- me.initialTableHeight = table.offsetHeight;
- me.rowHeight = me.initialTableHeight / me.store.getCount();
- } else {
- deltaHeight = table.offsetHeight - me.initialTableHeight;
- // Store size has been bumped because of odd end row.
- if (store.getCount() > me.viewSize) {
- deltaHeight -= me.rowHeight;
- }
- }
- } else if (doCalcHeight) {
- firstRow = view.el.down(view.getItemSelector());
- if (firstRow) {
- me.rowHeight = firstRow.getHeight(false, true);
- }
- }
- return Math.floor(store.getTotalCount() * me.rowHeight) + deltaHeight;
- },
- attemptLoad: function(start, end) {
- var me = this;
- if (me.scrollToLoadBuffer) {
- if (!me.loadTask) {
- me.loadTask = new Ext.util.DelayedTask(me.doAttemptLoad, me, []);
- }
- me.loadTask.delay(me.scrollToLoadBuffer, me.doAttemptLoad, me, [start, end]);
- } else {
- me.store.guaranteeRange(start, end);
- }
- },
- cancelLoad: function() {
- if (this.loadTask) {
- this.loadTask.cancel();
- }
- },
- doAttemptLoad: function(start, end) {
- this.store.guaranteeRange(start, end);
- },
- destroy: function() {
- var me = this,
- scrollListener = me.viewListeners.scroll;
- me.store.un({
- guaranteedrange: me.onGuaranteedRange,
- scope: me
- });
- me.view.un(me.viewListeners);
- if (me.view.rendered) {
- me.stretcher.remove();
- me.view.el.un('scroll', scrollListener.fn, scrollListener.scope);
- }
- }
- });
- </pre>
- </body>
- </html>
|