license import { coerceNumberProperty } from '@angular/cdk/coercion' ; import { ListRange } from '@angular/cdk/collections' ; import { CdkVirtualScrollViewport , VIRTUAL_SCROLL_STRATEGY , VirtualScrollStrategy } from '@angular/cdk/scrolling' ; import { Directive , forwardRef , Input , OnChanges } from '@angular/core' ; import { Observable } from 'rxjs' ; /**
 * A class that tracks the size of items that have been seen and uses it to estimate the average
 * item size.
 */ export class ItemSizeAverager { /** The total amount of weight behind the current average. */ private _totalWeight = 0 ; /** The current average item size. */ private _averageItemSize : number ; /** The default size to use for items when no data is available. */ private _defaultItemSize : number ; param defaultItemSize constructor ( defaultItemSize = 50 ) { this . _defaultItemSize = defaultItemSize ; this . _averageItemSize = defaultItemSize ; } /** Returns the average item size. */ getAverageItemSize ( ) { return this . _averageItemSize ; } param range param size addSample ( range , size ) { const newTotalWeight = this . _totalWeight + range . end - range . start ; if ( newTotalWeight ) { const newAverageItemSize = ( size + this . _averageItemSize * this . _totalWeight ) / newTotalWeight ; if ( newAverageItemSize ) { this . _averageItemSize = newAverageItemSize ; this . _totalWeight = newTotalWeight ; } } } /** Resets the averager. */ reset ( ) { this . _averageItemSize = this . _defaultItemSize ; this . _totalWeight = 0 ; } } /** Virtual scrolling strategy for lists with items of unknown or dynamic size. */ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy { docs-private scrolledIndexChange = Observable . create ( ( ) => { throw Error ( 'cdk-virtual-scroll: scrolledIndexChange is currently not supported for the' + ' autosize scroll strategy' ) ; } ) ; /** The attached viewport. */ private _viewport : CdkVirtualScrollViewport | null = null ; /** The minimum amount of buffer rendered beyond the viewport (in pixels). */ private _minBufferPx : number ; /** The number of buffer items to render beyond the edge of the viewport (in pixels). */ private _maxBufferPx : number ; /** The estimator used to estimate the size of unseen items. */ private _averager : ItemSizeAverager ; /** The last measured scroll offset of the viewport. */ private _lastScrollOffset : number ; /** The last measured size of the rendered content in the viewport. */ private _lastRenderedContentSize : number ; /** The last measured size of the rendered content in the viewport. */ private _lastRenderedContentOffset : number ; /**
     * The number of consecutive cycles where removing extra items has failed. Failure here means that
     * we estimated how many items we could safely remove, but our estimate turned out to be too much
     * and it wasn't safe to remove that many elements.
     */ private _removalFailures = 0 ; param minBufferPx param maxBufferPx param averager constructor ( minBufferPx , maxBufferPx , averager = new ItemSizeAverager ( ) ) { this . _minBufferPx = minBufferPx ; this . _maxBufferPx = maxBufferPx ; this . _averager = averager ; } param viewport attach ( viewport ) { this . _averager . reset ( ) ; this . _viewport = viewport ; this . _renderContentForCurrentOffset ( ) ; } /** Detaches this scroll strategy from the currently attached viewport. */ detach ( ) { this . _viewport = null ; } docs-private onContentScrolled ( ) { if ( this . _viewport ) { this . _updateRenderedContentAfterScroll ( ) ; } } docs-private onDataLengthChanged ( ) { if ( this . _viewport ) { this . _renderContentForCurrentOffset ( ) ; this . _checkRenderedContentSize ( ) ; } } docs-private onContentRendered ( ) { if ( this . _viewport ) { this . _checkRenderedContentSize ( ) ; } } docs-private onRenderedOffsetChanged ( ) { if ( this . _viewport ) { this . _checkRenderedContentOffset ( ) ; } } /** Scroll to the offset for the given index. */ scrollToIndex ( ) { throw Error ( 'cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize' + ' scroll strategy' ) ; } param minBufferPx param maxBufferPx updateBufferSize ( minBufferPx , maxBufferPx ) { if ( maxBufferPx < minBufferPx ) { throw ( 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx' ) ; } this . _minBufferPx = minBufferPx ; this . _maxBufferPx = maxBufferPx ; } /** Update the rendered content after the user scrolls. */ private _updateRenderedContentAfterScroll ( ) { const viewport = this . _viewport ! ; const scrollOffset = viewport . measureScrollOffset ( ) ; let scrollDelta = scrollOffset - this . _lastScrollOffset ; let scrollMagnitude = Math . abs ( scrollDelta ) ; const renderedRange = viewport . getRenderedRange ( ) ; let offsetCorrection = 0 ; if ( scrollDelta < 0 ) { const predictedOffset = renderedRange . start * this . _averager . getAverageItemSize ( ) ; const offsetDifference = predictedOffset - this . _lastRenderedContentOffset ; offsetCorrection = Math . round ( offsetDifference * Math . max ( 0 , Math . min ( 1 , scrollMagnitude / ( scrollOffset + scrollMagnitude ) ) ) ) ; scrollDelta = scrollDelta - offsetCorrection ; scrollMagnitude = Math . abs ( scrollDelta ) ; } const startBuffer = this . _lastScrollOffset - this . _lastRenderedContentOffset ; const endBuffer = ( this . _lastRenderedContentOffset + this . _lastRenderedContentSize ) - ( this . _lastScrollOffset + viewport . getViewportSize ( ) ) ; const underscan = scrollMagnitude + this . _minBufferPx - ( scrollDelta < 0 ? startBuffer : endBuffer ) ; if ( underscan > 0 ) { if ( scrollMagnitude >= viewport . getViewportSize ( ) ) { this . _renderContentForCurrentOffset ( ) ; } else { const addItems = Math . max ( 0 , Math . ceil ( ( underscan - this . _minBufferPx + this . _maxBufferPx ) / this . _averager . getAverageItemSize ( ) ) ) ; const overscan = ( scrollDelta < 0 ? endBuffer : startBuffer ) - this . _minBufferPx + scrollMagnitude ; const unboundedRemoveItems = Math . floor ( overscan / this . _averager . getAverageItemSize ( ) / ( this . _removalFailures + 1 ) ) ; const removeItems = Math . min ( renderedRange . end - renderedRange . start , Math . max ( 0 , unboundedRemoveItems ) ) ; const range = this . _expandRange ( renderedRange , scrollDelta < 0 ? addItems : 0 , scrollDelta > 0 ? addItems : 0 ) ; if ( scrollDelta < 0 ) { range . end = Math . max ( range . start + 1 , range . end - removeItems ) ; } else { range . start = Math . min ( range . end - 1 , range . start + removeItems ) ; } let contentOffset ; let contentOffsetTo ; if ( scrollDelta < 0 ) { let removedSize = viewport . measureRangeSize ( { start : range . end , end : renderedRange . end , } ) ; if ( removedSize <= overscan ) { contentOffset = this . _lastRenderedContentOffset + this . _lastRenderedContentSize - removedSize ; this . _removalFailures = 0 ; } else { range . end = renderedRange . end ; contentOffset = this . _lastRenderedContentOffset + this . _lastRenderedContentSize ; this . _removalFailures ++ ; } contentOffsetTo = 'to-end' ; } else { const removedSize = viewport . measureRangeSize ( { start : renderedRange . start , end : range . start , } ) ; if ( removedSize <= overscan ) { contentOffset = this . _lastRenderedContentOffset + removedSize ; this . _removalFailures = 0 ; } else { range . start = renderedRange . start ; contentOffset = this . _lastRenderedContentOffset ; this . _removalFailures ++ ; } contentOffsetTo = 'to-start' ; } viewport . setRenderedRange ( range ) ; viewport . setRenderedContentOffset ( contentOffset + offsetCorrection , contentOffsetTo ) ; } } else if ( offsetCorrection ) { viewport . setRenderedContentOffset ( this . _lastRenderedContentOffset + offsetCorrection ) ; } this . _lastScrollOffset = scrollOffset ; } /**
     * Checks the size of the currently rendered content and uses it to update the estimated item size
     * and estimated total content size.
     */ private _checkRenderedContentSize ( ) { const viewport = this . _viewport ! ; this . _lastRenderedContentSize = viewport . measureRenderedContentSize ( ) ; this . _averager . addSample ( viewport . getRenderedRange ( ) , this . _lastRenderedContentSize ) ; this . _updateTotalContentSize ( this . _lastRenderedContentSize ) ; } /** Checks the currently rendered content offset and saves the value for later use. */ private _checkRenderedContentOffset ( ) { const viewport = this . _viewport ! ; this . _lastRenderedContentOffset = viewport . getOffsetToRenderedContentStart ( ) ! ; } /**
     * Recalculates the rendered content based on our estimate of what should be shown at the current
     * scroll offset.
     */ private _renderContentForCurrentOffset ( ) { const viewport = this . _viewport ! ; const scrollOffset = viewport . measureScrollOffset ( ) ; this . _lastScrollOffset = scrollOffset ; this . _removalFailures = 0 ; const itemSize = this . _averager . getAverageItemSize ( ) ; const firstVisibleIndex = Math . min ( viewport . getDataLength ( ) - 1 , Math . floor ( scrollOffset / itemSize ) ) ; const bufferSize = Math . ceil ( this . _maxBufferPx / itemSize ) ; const range = this . _expandRange ( this . _getVisibleRangeForIndex ( firstVisibleIndex ) , bufferSize , bufferSize ) ; viewport . setRenderedRange ( range ) ; viewport . setRenderedContentOffset ( itemSize * range . start ) ; } param startIndex return private _getVisibleRangeForIndex ( startIndex ) { const viewport = this . _viewport ! ; const range = { start : startIndex , end : startIndex + Math . ceil ( viewport . getViewportSize ( ) / this . _averager . getAverageItemSize ( ) ) } ; const extra = range . end - viewport . getDataLength ( ) ; if ( extra > 0 ) { range . start = Math . max ( 0 , range . start - extra ) ; } return range ; } param range param expandStart param expandEnd return private _expandRange ( range , expandStart , expandEnd ) { const viewport = this . _viewport ! ; const start = Math . max ( 0 , range . start - expandStart ) ; const end = Math . min ( viewport . getDataLength ( ) , range . end + expandEnd ) ; return { start , end } ; } /** Update the viewport's total content size. */ private _updateTotalContentSize ( renderedContentSize ) { const viewport = this . _viewport ! ; const renderedRange = viewport . getRenderedRange ( ) ; const totalSize = renderedContentSize + ( viewport . getDataLength ( ) - ( renderedRange . end - renderedRange . start ) ) * this . _averager . getAverageItemSize ( ) ; viewport . setTotalContentSize ( totalSize ) ; } } param autoSizeDir export function _autoSizeVirtualScrollStrategyFactory ( autoSizeDir ) { return autoSizeDir . _scrollStrategy ; } /** A virtual scroll strategy that supports unknown or dynamic size items. */ @ Directive ( { selector : 'cdk-virtual-scroll-viewport[autosize]' , providers : [ { provide : VIRTUAL_SCROLL_STRATEGY , useFactory : _autoSizeVirtualScrollStrategyFactory , deps : [ forwardRef ( ( ) => CdkAutoSizeVirtualScroll ) ] , } ] , } ) export class CdkAutoSizeVirtualScroll implements OnChanges { /**
     * The minimum amount of buffer rendered beyond the viewport (in pixels).
     * If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px.
     */ @ Input ( ) get minBufferPx ( ) : number { return this . _minBufferPx ; } set minBufferPx ( value ) { this . _minBufferPx = coerceNumberProperty ( value ) ; } _minBufferPx = 100 ; /**
     * The number of pixels worth of buffer to shoot for when rendering new items.
     * If the actual amount turns out to be less it will not necessarily trigger an additional
     * rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`).
     * Defaults to 200px.
     */ @ Input ( ) get maxBufferPx ( ) : number { return this . _maxBufferPx ; } set maxBufferPx ( value ) { this . _maxBufferPx = coerceNumberProperty ( value ) ; } _maxBufferPx = 200 ; /** The scroll strategy used by this directive. */ _scrollStrategy = new AutoSizeVirtualScrollStrategy ( this . minBufferPx , this . maxBufferPx ) ; ngOnChanges ( ) { this . _scrollStrategy . updateBufferSize ( this . minBufferPx , this . maxBufferPx ) ; } }