| 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>
 |