import { AfterContentInit , ChangeDetectorRef , ContentChild , Directive , DoCheck , ElementRef , Input , IterableDiffer , IterableDiffers , NgZone , OnChanges , OnDestroy , Renderer , SimpleChanges , TrackByFunction } from '@angular/core' ; import { adjustRendered , calcDimensions , estimateHeight , initReadNodes , populateNodeData , processRecords , updateDimensions , updateNodeContext , writeToNodes } from './virtual-util' ; import { Config } from '../../config/config' ; import { Content , ScrollEvent } from '../content/content' ; import { DomController } from '../../platform/dom-controller' ; import { assert , isFunction , isPresent } from '../../util/util' ; import { Platform } from '../../platform/platform' ; import { ViewController } from '../../navigation/view-controller' ; import { VirtualCell , VirtualData , VirtualNode } from './virtual-util' ; import { VirtualItem } from './virtual-item' ; import { VirtualFooter } from './virtual-footer' ; import { VirtualHeader } from './virtual-header' ; name description @ Directive ( { selector : '[virtualScroll]' } ) export class VirtualScroll implements DoCheck , OnChanges , AfterContentInit , OnDestroy { _differ : IterableDiffer < any > ; _scrollSub : any ; _scrollEndSub : any ; _resizeSub : any ; _init : boolean = false ; _lastEle : boolean = false ; _hdrFn : Function ; _ftrFn : Function ; _records : any [ ] = [ ] ; _cells : VirtualCell [ ] = [ ] ; _nodes : VirtualNode [ ] = [ ] ; _vHeight : number = 0 ; _lastCheck : number = 0 ; _recordSize : number = 0 ; _data : VirtualData = { scrollTop : 0 , } ; _queue : number = ScrollQueue . NoChanges ; @ ContentChild ( VirtualItem ) _itmTmp : VirtualItem ; @ ContentChild ( VirtualHeader ) _hdrTmp : VirtualHeader ; @ ContentChild ( VirtualFooter ) _ftrTmp : VirtualFooter ; input @ Input ( ) set virtualScroll ( val ) { this . _records = val ; } get virtualScroll ( ) { return this . _records ; } input @ Input ( ) bufferRatio : number = 3 ; input @ Input ( ) approxItemWidth : string = '100%' ; input @ Input ( ) approxItemHeight : string ; input @ Input ( ) approxHeaderWidth : string = '100%' ; input @ Input ( ) approxHeaderHeight : string = '40px' ; input @ Input ( ) approxFooterWidth : string = '100%' ; input @ Input ( ) approxFooterHeight : string = '40px' ; input @ Input ( ) set headerFn ( val ) { if ( isFunction ( val ) ) { this . _hdrFn = val . bind ( ( this . _ctrl . _cmp ) || this ) ; } } input @ Input ( ) set footerFn ( val ) { if ( isFunction ( val ) ) { this . _ftrFn = val . bind ( ( this . _ctrl . _cmp ) || this ) ; } } input @ Input ( ) virtualTrackBy : TrackByFunction < any > ; constructor ( private _iterableDiffers , private _elementRef , private _renderer , private _zone , private _cd , private _content , private _plt , private _ctrl , private _config , private _dom ) { this . setElementClass ( 'virtual-loading' , true ) ; const readSub = _ctrl . readReady . subscribe ( ( ) => { readSub . unsubscribe ( ) ; this . readUpdate ( true ) ; } ) ; const writeSub = _ctrl . writeReady . subscribe ( ( ) => { writeSub . unsubscribe ( ) ; this . _init = true ; this . writeUpdate ( true ) ; this . _listeners ( ) ; } ) ; } hidden firstRecord ( ) { const cells = this . _cells ; return ( cells . length > 0 ) ? cells [ 0 ] . record : 0 ; } hidden lastRecord ( ) { const cells = this . _cells ; return ( cells . length > 0 ) ? cells [ cells . length - 1 ] . record : 0 ; } hidden ngOnChanges ( changes ) { if ( 'virtualScroll' in changes ) { const value = changes [ 'virtualScroll' ] . currentValue ; if ( ! isPresent ( this . _differ ) && isPresent ( value ) ) { try { this . _differ = this . _iterableDiffers . find ( value ) . create ( this . virtualTrackBy ) ; } catch ( e ) { throw new Error ( `Cannot find a differ supporting object '${ value }'. VirtualScroll only supports binding to Iterables such as Arrays.` ) ; } } } } hidden ngDoCheck ( ) { if ( ! this . _init ) { return ; } const changes = isPresent ( this . _differ ) ? this . _differ . diff ( this . virtualScroll ) : null ; if ( ! isPresent ( changes ) ) { return ; } let needClean = false ; var lastRecord = this . _recordSize ; changes . forEachOperation ( ( _ , pindex , cindex ) => { if ( pindex === null && ( cindex < lastRecord ) ) { console . debug ( 'virtual-scroll' , 'adding record before current position, slow path' ) ; needClean = true ; return ; } if ( pindex < lastRecord && cindex === null ) { console . debug ( 'virtual-scroll' , 'removing record before current position, slow path' ) ; needClean = true ; return ; } } ) ; this . _recordSize = this . _records ? this . _records . length : 0 ; this . readUpdate ( needClean ) ; this . writeUpdate ( needClean ) ; } hidden readUpdate ( needClean ) { if ( needClean ) { console . debug ( 'virtual-scroll' , 'readUpdate: slow path' ) ; this . _cells . length = 0 ; this . calcDimensions ( ) ; } else { console . debug ( 'virtual-scroll' , 'readUpdate: fast path' ) ; } } hidden writeUpdate ( needClean ) { console . debug ( 'virtual-scroll' , 'writeUpdate need clean:' , needClean ) ; const data = this . _data ; const stopAtHeight = ( data . scrollTop + data . renderHeight ) ; data . scrollDiff = SCROLL_DIFFERENCE_MINIMUM + 1 ; processRecords ( stopAtHeight , this . _records , this . _cells , this . _hdrFn , this . _ftrFn , this . _data ) ; this . renderVirtual ( needClean ) ; } hidden private calcDimensions ( ) { calcDimensions ( this . _data , this . _elementRef . nativeElement , this . approxItemWidth , this . approxItemHeight , this . approxHeaderWidth , this . approxHeaderHeight , this . approxFooterWidth , this . approxFooterHeight , this . bufferRatio ) ; } hidden renderVirtual ( needClean ) { this . _plt . raf ( ( ) => { const nodes = this . _nodes ; const cells = this . _cells ; const data = this . _data ; const records = this . _records ; if ( needClean ) { updateDimensions ( this . _plt , nodes , cells , data , true ) ; data . topCell = 0 ; data . bottomCell = ( cells . length - 1 ) ; } adjustRendered ( cells , data ) ; this . _zone . run ( ( ) => { populateNodeData ( data . topCell , data . bottomCell , true , cells , records , nodes , this . _itmTmp . viewContainer , this . _itmTmp . templateRef , this . _hdrTmp && this . _hdrTmp . templateRef , this . _ftrTmp && this . _ftrTmp . templateRef ) ; } ) ; if ( needClean ) { this . _cd . detectChanges ( ) ; } this . _dom . read ( ( ) => initReadNodes ( this . _plt , nodes , cells , data ) ) ; this . _dom . write ( ( ) => { updateNodeContext ( nodes , cells , data ) ; this . _stepChangeDetection ( ) ; this . _stepDOMWrite ( ) ; this . _content . imgsUpdate ( ) ; if ( ! this . _lastEle ) { var ele = this . _elementRef . nativeElement ; var lastEle = this . _renderer . createElement ( ele , 'div' ) ; lastEle . className = 'virtual-last' ; this . _lastEle = true ; this . setElementClass ( 'virtual-scroll' , true ) ; this . setElementClass ( 'virtual-loading' , false ) ; } assert ( this . _queue === ScrollQueue . NoChanges , 'queue value should be NO_CHANGES' ) ; } ) ; } ) ; } hidden resize ( ) { if ( ! this . _init ) { return ; } if ( this . _elementRef . nativeElement . offsetParent === null ) { return ; } console . debug ( 'virtual-scroll' , 'resized window' ) ; this . calcDimensions ( ) ; this . writeUpdate ( false ) ; } hidden private _stepDOMWrite ( ) { const cells = this . _cells ; const nodes = this . _nodes ; writeToNodes ( this . _plt , nodes , cells , this . _recordSize ) ; this . _setHeight ( estimateHeight ( this . _recordSize , cells [ cells . length - 1 ] , this . _vHeight , 0.25 ) ) ; this . _queue = ScrollQueue . NoChanges ; } hidden private _stepChangeDetection ( ) { const nodes = this . _nodes ; for ( var i = 0 ; i < nodes . length ; i ++ ) { if ( nodes [ i ] . hasChanges ) { ( < any > nodes [ i ] . view ) . detectChanges ( ) ; } } this . _queue = ScrollQueue . DomWrite ; } hidden private _stepNoChanges ( ) { const data = this . _data ; const diff = data . scrollDiff = ( data . scrollTop - this . _lastCheck ) ; if ( Math . abs ( diff ) < SCROLL_DIFFERENCE_MINIMUM ) { return ; } const cells = this . _cells ; const nodes = this . _nodes ; const records = this . _records ; this . _lastCheck = data . scrollTop ; if ( diff > 0 ) { var stopAtHeight = ( data . scrollTop + data . renderHeight ) ; processRecords ( stopAtHeight , records , cells , this . _hdrFn , this . _ftrFn , data ) ; } updateDimensions ( this . _plt , nodes , cells , data , false ) ; adjustRendered ( cells , data ) ; var hasChanges = populateNodeData ( data . topCell , data . bottomCell , diff > 0 , cells , records , nodes , this . _itmTmp . viewContainer , this . _itmTmp . templateRef , this . _hdrTmp && this . _hdrTmp . templateRef , this . _ftrTmp && this . _ftrTmp . templateRef ) ; if ( hasChanges ) { this . _queue = ScrollQueue . ChangeDetection ; updateNodeContext ( nodes , cells , data ) ; } } hidden scrollUpdate ( ev ) { if ( ! ev ) { return ; } this . _data . scrollTop = ev . scrollTop ; const queue = this . _queue ; if ( queue === ScrollQueue . NoChanges ) { this . _stepNoChanges ( ) ; } else if ( queue === ScrollQueue . ChangeDetection ) { this . _dom . write ( ( ) => this . _stepChangeDetection ( ) ) ; } else { assert ( queue === ScrollQueue . DomWrite , 'queue value unexpected' ) ; this . _dom . write ( ( ) => this . _stepDOMWrite ( ) ) ; } } hidden scrollEnd ( ) { updateDimensions ( this . _plt , this . _nodes , this . _cells , this . _data , false ) ; adjustRendered ( this . _cells , this . _data ) ; populateNodeData ( this . _data . topCell , this . _data . bottomCell , true , this . _cells , this . _records , this . _nodes , this . _itmTmp . viewContainer , this . _itmTmp . templateRef , this . _hdrTmp && this . _hdrTmp . templateRef , this . _ftrTmp && this . _ftrTmp . templateRef ) ; this . _dom . write ( ( ) => { updateNodeContext ( this . _nodes , this . _cells , this . _data ) ; this . _stepChangeDetection ( ) ; this . _stepDOMWrite ( ) ; } ) ; } hidden private _listeners ( ) { assert ( ! this . _scrollSub , '_listeners was already called' ) ; if ( ! this . _scrollSub ) { if ( this . _config . getBoolean ( 'virtualScrollEventAssist' ) ) { this . _content . enableJsScroll ( ) ; } this . _resizeSub = this . _plt . resize . subscribe ( this . resize . bind ( this ) ) ; this . _scrollSub = this . _content . ionScroll . subscribe ( this . scrollUpdate . bind ( this ) ) ; this . _scrollEndSub = this . _content . ionScrollEnd . subscribe ( this . scrollEnd . bind ( this ) ) ; } } hidden private _setHeight ( newVirtualHeight ) { if ( newVirtualHeight !== this . _vHeight ) { this . _renderer . setElementStyle ( this . _elementRef . nativeElement , 'height' , newVirtualHeight > 0 ? newVirtualHeight + 'px' : '' ) ; this . _vHeight = newVirtualHeight ; console . debug ( 'virtual-scroll' , 'height' , newVirtualHeight ) ; } } hidden ngAfterContentInit ( ) { assert ( this . _itmTmp , 'virtualItem required within virtualScroll' ) ; if ( ! this . approxItemHeight ) { this . approxItemHeight = '40px' ; console . warn ( 'Virtual Scroll: Please provide an "approxItemHeight" input to ensure proper virtual scroll rendering' ) ; } } hidden setElementClass ( className , add ) { this . _renderer . setElementClass ( this . _elementRef . nativeElement , className , add ) ; } hidden ngOnDestroy ( ) { this . _resizeSub && this . _resizeSub . unsubscribe ( ) ; this . _scrollSub && this . _scrollSub . unsubscribe ( ) ; this . _scrollEndSub && this . _scrollEndSub . unsubscribe ( ) ; this . _resizeSub = this . _scrollEndSub = this . _scrollSub = null ; this . _hdrFn = this . _ftrFn = this . _records = this . _cells = this . _nodes = this . _data = null ; } } const SCROLL_DIFFERENCE_MINIMUM = 40 ; const enum ScrollQueue { NoChanges = 1 , ChangeDetection = 2 , DomWrite = 3 }