license import { Directionality } from '@angular/cdk/bidi' ; import { ListRange } from '@angular/cdk/collections' ; import { ChangeDetectionStrategy , ChangeDetectorRef , Component , ElementRef , Inject , Input , NgZone , OnDestroy , OnInit , Optional , Output , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { animationFrameScheduler , asapScheduler , Observable , Subject , Observer , Subscription , } from 'rxjs' ; import { auditTime , startWith , takeUntil } from 'rxjs/operators' ; import { ScrollDispatcher } from './scroll-dispatcher' ; import { CdkScrollable , ExtendedScrollToOptions } from './scrollable' ; import { VIRTUAL_SCROLL_STRATEGY , VirtualScrollStrategy } from './virtual-scroll-strategy' ; import { ViewportRuler } from './viewport-ruler' ; import { CdkVirtualScrollRepeater } from './virtual-scroll-repeater' ; import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ; /** Checks if the given ranges are equal. */ function rangesEqual ( r1 , r2 ) { return r1 . start == r2 . start && r1 . end == r2 . end ; } /**
 * Scheduler to be used for scroll events. Needs to fall back to
 * something that doesn't rely on requestAnimationFrame on environments
 * that don't support it (e.g. server-side rendering).
 */ const SCROLL_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler ; /** A viewport that virtualizes its scrolling with the help of `CdkVirtualForOf`. */ @ Component ( { selector : 'cdk-virtual-scroll-viewport' , templateUrl : 'virtual-scroll-viewport.html' , styleUrls : [ 'virtual-scroll-viewport.css' ] , host : { 'class' : 'cdk-virtual-scroll-viewport' , '[class.cdk-virtual-scroll-orientation-horizontal]' : 'orientation === "horizontal"' , '[class.cdk-virtual-scroll-orientation-vertical]' : 'orientation !== "horizontal"' , } , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , providers : [ { provide : CdkScrollable , useExisting : CdkVirtualScrollViewport , } , ] , } ) export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit , OnDestroy { /** Emits when the viewport is detached from a CdkVirtualForOf. */ private readonly _detachedSubject = new Subject < void > ( ) ; /** Emits when the rendered range changes. */ private readonly _renderedRangeSubject = new Subject < ListRange > ( ) ; /** The direction the viewport scrolls. */ @ Input ( ) get orientation ( ) { return this . _orientation ; } set orientation ( orientation ) { if ( this . _orientation !== orientation ) { this . _orientation = orientation ; this . _calculateSpacerSize ( ) ; } } private _orientation : 'horizontal' | 'vertical' = 'vertical' ; /**
     * Whether rendered items should persist in the DOM after scrolling out of view. By default, items
     * will be removed.
     */ @ Input ( ) get appendOnly ( ) : boolean { return this . _appendOnly ; } set appendOnly ( value ) { this . _appendOnly = coerceBooleanProperty ( value ) ; } private _appendOnly = false ; /** Emits when the index of the first element visible in the viewport changes. */ @ Output ( ) readonly scrolledIndexChange : Observable < number > = new Observable ( ( observer ) => this . _scrollStrategy . scrolledIndexChange . subscribe ( index => Promise . resolve ( ) . then ( ( ) => this . ngZone . run ( ( ) => observer . next ( index ) ) ) ) ) ; /** The element that wraps the rendered content. */ @ ViewChild ( 'contentWrapper' , { static : true } ) _contentWrapper : ElementRef < HTMLElement > ; /** A stream that emits whenever the rendered range changes. */ readonly renderedRangeStream : Observable < ListRange > = this . _renderedRangeSubject ; /**
     * The total size of all content (in pixels), including content that is not currently rendered.
     */ private _totalContentSize = 0 ; /** A string representing the `style.width` property value to be used for the spacer element. */ _totalContentWidth = '' ; /** A string representing the `style.height` property value to be used for the spacer element. */ _totalContentHeight = '' ; /**
     * The CSS transform applied to the rendered subset of items so that they appear within the bounds
     * of the visible viewport.
     */ private _renderedContentTransform : string ; /** The currently rendered range of indices. */ private _renderedRange : ListRange = { start : 0 , end : 0 } ; /** The length of the data bound to this viewport (in number of items). */ private _dataLength = 0 ; /** The size of the viewport (in pixels). */ private _viewportSize = 0 ; /** the currently attached CdkVirtualScrollRepeater. */ private _forOf : CdkVirtualScrollRepeater < any > | null ; /** The last rendered content offset that was set. */ private _renderedContentOffset = 0 ; /**
     * Whether the last rendered content offset was to the end of the content (and therefore needs to
     * be rewritten as an offset to the start of the content).
     */ private _renderedContentOffsetNeedsRewrite = false ; /** Whether there is a pending change detection cycle. */ private _isChangeDetectionPending = false ; /** A list of functions to run after the next change detection cycle. */ private _runAfterChangeDetection : Function [ ] = [ ] ; /** Subscription to changes in the viewport size. */ private _viewportChanges = Subscription . EMPTY ; constructor ( public override elementRef , private _changeDetectorRef , ngZone , @ Optional ( ) @ Inject ( VIRTUAL_SCROLL_STRATEGY ) private _scrollStrategy , @ Optional ( ) dir , scrollDispatcher , viewportRuler ) { super ( elementRef , scrollDispatcher , ngZone , dir ) ; if ( ! _scrollStrategy && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw Error ( 'Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set.' ) ; } this . _viewportChanges = viewportRuler . change ( ) . subscribe ( ( ) => { this . checkViewportSize ( ) ; } ) ; } override ngOnInit ( ) { super . ngOnInit ( ) ; this . ngZone . runOutsideAngular ( ( ) => Promise . resolve ( ) . then ( ( ) => { this . _measureViewportSize ( ) ; this . _scrollStrategy . attach ( this ) ; this . elementScrolled ( ) . pipe ( startWith ( null ) , auditTime ( 0 , SCROLL_SCHEDULER ) ) . subscribe ( ( ) => this . _scrollStrategy . onContentScrolled ( ) ) ; this . _markChangeDetectionNeeded ( ) ; } ) ) ; } override ngOnDestroy ( ) { this . detach ( ) ; this . _scrollStrategy . detach ( ) ; this . _renderedRangeSubject . complete ( ) ; this . _detachedSubject . complete ( ) ; this . _viewportChanges . unsubscribe ( ) ; super . ngOnDestroy ( ) ; } /** Attaches a `CdkVirtualScrollRepeater` to this viewport. */ attach ( forOf ) { if ( this . _forOf && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw Error ( 'CdkVirtualScrollViewport is already attached.' ) ; } this . ngZone . runOutsideAngular ( ( ) => { this . _forOf = forOf ; this . _forOf . dataStream . pipe ( takeUntil ( this . _detachedSubject ) ) . subscribe ( data => { const newLength = data . length ; if ( newLength !== this . _dataLength ) { this . _dataLength = newLength ; this . _scrollStrategy . onDataLengthChanged ( ) ; } this . _doChangeDetection ( ) ; } ) ; } ) ; } /** Detaches the current `CdkVirtualForOf`. */ detach ( ) { this . _forOf = null ; this . _detachedSubject . next ( ) ; } /** Gets the length of the data bound to this viewport (in number of items). */ getDataLength ( ) { return this . _dataLength ; } /** Gets the size of the viewport (in pixels). */ getViewportSize ( ) { return this . _viewportSize ; } /** Get the current rendered range of items. */ getRenderedRange ( ) { return this . _renderedRange ; } /**
     * Sets the total size of all content (in pixels), including content that is not currently
     * rendered.
     */ setTotalContentSize ( size ) { if ( this . _totalContentSize !== size ) { this . _totalContentSize = size ; this . _calculateSpacerSize ( ) ; this . _markChangeDetectionNeeded ( ) ; } } /** Sets the currently rendered range of indices. */ setRenderedRange ( range ) { if ( ! rangesEqual ( this . _renderedRange , range ) ) { if ( this . appendOnly ) { range = { start : 0 , end : Math . max ( this . _renderedRange . end , range . end ) } ; } this . _renderedRangeSubject . next ( ( this . _renderedRange = range ) ) ; this . _markChangeDetectionNeeded ( ( ) => this . _scrollStrategy . onContentRendered ( ) ) ; } } /**
     * Gets the offset from the start of the viewport to the start of the rendered data (in pixels).
     */ getOffsetToRenderedContentStart ( ) { return this . _renderedContentOffsetNeedsRewrite ? null : this . _renderedContentOffset ; } /**
     * Sets the offset from the start of the viewport to either the start or end of the rendered data
     * (in pixels).
     */ setRenderedContentOffset ( offset , to = 'to-start' ) { const isRtl = this . dir && this . dir . value == 'rtl' ; const isHorizontal = this . orientation == 'horizontal' ; const axis = isHorizontal ? 'X' : 'Y' ; const axisDirection = isHorizontal && isRtl ? - 1 : 1 ; let transform = `translate${ axis }(${ Number ( axisDirection * offset ) }px)` ; this . _renderedContentOffset = offset ; if ( to === 'to-end' ) { transform += ` translate${ axis }(-100%)` ; this . _renderedContentOffsetNeedsRewrite = true ; } if ( this . _renderedContentTransform != transform ) { this . _renderedContentTransform = transform ; this . _markChangeDetectionNeeded ( ( ) => { if ( this . _renderedContentOffsetNeedsRewrite ) { this . _renderedContentOffset -= this . measureRenderedContentSize ( ) ; this . _renderedContentOffsetNeedsRewrite = false ; this . setRenderedContentOffset ( this . _renderedContentOffset ) ; } else { this . _scrollStrategy . onRenderedOffsetChanged ( ) ; } } ) ; } } param offset param behavior scrollToOffset ( offset , behavior = 'auto' ) { const options = { behavior } ; if ( this . orientation === 'horizontal' ) { options . start = offset ; } else { options . top = offset ; } this . scrollTo ( options ) ; } param index param behavior scrollToIndex ( index , behavior = 'auto' ) { this . _scrollStrategy . scrollToIndex ( index , behavior ) ; } param from override measureScrollOffset ( from ? ) { return from ? super . measureScrollOffset ( from ) : super . measureScrollOffset ( this . orientation === 'horizontal' ? 'start' : 'top' ) ; } /** Measure the combined size of all of the rendered items. */ measureRenderedContentSize ( ) { const contentEl = this . _contentWrapper . nativeElement ; return this . orientation === 'horizontal' ? contentEl . offsetWidth : contentEl . offsetHeight ; } /**
     * Measure the total combined size of the given range. Throws if the range includes items that are
     * not rendered.
     */ measureRangeSize ( range ) { if ( ! this . _forOf ) { return 0 ; } return this . _forOf . measureRangeSize ( range , this . orientation ) ; } /** Update the viewport dimensions and re-render. */ checkViewportSize ( ) { this . _measureViewportSize ( ) ; this . _scrollStrategy . onDataLengthChanged ( ) ; } /** Measure the viewport size. */ private _measureViewportSize ( ) { const viewportEl = this . elementRef . nativeElement ; this . _viewportSize = this . orientation === 'horizontal' ? viewportEl . clientWidth : viewportEl . clientHeight ; } /** Queue up change detection to run. */ private _markChangeDetectionNeeded ( runAfter ? ) { if ( runAfter ) { this . _runAfterChangeDetection . push ( runAfter ) ; } if ( ! this . _isChangeDetectionPending ) { this . _isChangeDetectionPending = true ; this . ngZone . runOutsideAngular ( ( ) => Promise . resolve ( ) . then ( ( ) => { this . _doChangeDetection ( ) ; } ) ) ; } } /** Run change detection. */ private _doChangeDetection ( ) { this . _isChangeDetectionPending = false ; this . _contentWrapper . nativeElement . style . transform = this . _renderedContentTransform ; this . ngZone . run ( ( ) => this . _changeDetectorRef . markForCheck ( ) ) ; const runAfterChangeDetection = this . _runAfterChangeDetection ; this . _runAfterChangeDetection = [ ] ; for ( const fn of runAfterChangeDetection ) { fn ( ) ; } } /** Calculates the `style.width` and `style.height` for the spacer element. */ private _calculateSpacerSize ( ) { this . _totalContentHeight = this . orientation === 'horizontal' ? '' : `${ this . _totalContentSize }px` ; this . _totalContentWidth = this . orientation === 'horizontal' ? `${ this . _totalContentSize }px` : '' ; } }