import { AfterViewInit , ChangeDetectionStrategy , Component , ElementRef , EventEmitter , Input , NgZone , OnDestroy , Optional , Output , Renderer , ViewChild , ViewEncapsulation } from '@angular/core' ; import { App } from '../app/app' ; import { Config } from '../../config/config' ; import { DomController } from '../../platform/dom-controller' ; import { Img } from '../img/img-interface' ; import { Ion } from '../ion' ; import { isTabs } from '../../navigation/nav-util' ; import { assert , isTrueProperty , removeArrayItem } from '../../util/util' ; import { Keyboard } from '../../platform/keyboard' ; import { NavController } from '../../navigation/nav-controller' ; import { Content as IContent , Tabs } from '../../navigation/nav-interfaces' ; import { Platform } from '../../platform/platform' ; import { ScrollEvent , ScrollView } from '../../util/scroll-view' ; import { ViewController } from '../../navigation/view-controller' ; export { ScrollEvent } from '../../util/scroll-view' ; export class EventEmitterProxy < T > extends EventEmitter < T > { onSubscribe : Function ; subscribe ( generatorOrNext ? , error ? , complete ? ) { this . onSubscribe ( ) ; return super . subscribe ( generatorOrNext , error , complete ) ; } } name description usage Component ViewChild advanced Component Component ViewChild Component ViewChild @ Component ( { selector : 'ion-content' , template : '<div class="fixed-content" #fixedContent>' + '<ng-content select="[ion-fixed],ion-fab"></ng-content>' + '</div>' + '<div class="scroll-content" #scrollContent>' + '<ng-content></ng-content>' + '</div>' + '<ng-content select="ion-refresher"></ng-content>' , host : { '[class.statusbar-padding]' : 'statusbarPadding' , '[class.has-refresher]' : '_hasRefresher' } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None } ) export class Content extends Ion implements OnDestroy , AfterViewInit , IContent { internal _cTop : number ; internal _cBottom : number ; internal _pTop : number ; internal _pRight : number ; internal _pBottom : number ; internal _pLeft : number ; internal _scrollPadding : number = 0 ; internal _hdrHeight : number ; internal _ftrHeight : number ; internal _tabs : Tabs ; internal _tabbarHeight : number ; internal _tabsPlacement : string ; internal _tTop : number ; internal _fTop : number ; internal _fBottom : number ; internal _inputPolling : boolean = false ; internal _scroll : ScrollView ; internal _scLsn : Function ; internal _fullscreen : boolean ; internal _hasRefresher : boolean = false ; internal _footerEle : HTMLElement ; internal _dirty : boolean ; internal _imgs : Img [ ] = [ ] ; internal _viewCtrlReadSub : any ; internal _viewCtrlWriteSub : any ; internal _scrollDownOnLoad : boolean = false ; _viewCtrl : any ; private _imgReqBfr : number ; private _imgRndBfr : number ; private _imgVelMax : number ; hidden statusbarPadding : boolean ; internal @ ViewChild ( 'fixedContent' , { read : ElementRef } ) _fixedContent : ElementRef ; internal @ ViewChild ( 'scrollContent' , { read : ElementRef } ) _scrollContent : ElementRef ; return { number } get contentHeight ( ) : number { return this . _scroll . ev . contentHeight ; } return { number } get contentWidth ( ) : number { return this . _scroll . ev . contentWidth ; } return { number } contentTop : number ; return { number } contentBottom : number ; return { number } get scrollHeight ( ) : number { return this . _scroll . ev . scrollHeight ; } return { number } get scrollWidth ( ) : number { return this . _scroll . ev . scrollWidth ; } return { number } get scrollTop ( ) : number { return this . _scroll . ev . scrollTop ; } param { number } top set scrollTop ( top ) { this . _scroll . setTop ( top ) ; } return { number } get scrollLeft ( ) : number { return this . _scroll . ev . scrollLeft ; } param { number } top set scrollLeft ( top ) { this . _scroll . setLeft ( top ) ; } return { boolean } get isScrolling ( ) : boolean { return this . _scroll . isScrolling ; } return { string } get directionY ( ) : string { return this . _scroll . ev . directionY ; } return { string } get directionX ( ) : string { return this . _scroll . ev . directionX ; } output @ Output ( ) ionScrollStart : EventEmitterProxy < ScrollEvent > = new EventEmitterProxy < ScrollEvent > ( ) ; output @ Output ( ) ionScroll : EventEmitterProxy < ScrollEvent > = new EventEmitterProxy < ScrollEvent > ( ) ; output @ Output ( ) ionScrollEnd : EventEmitterProxy < ScrollEvent > = new EventEmitterProxy < ScrollEvent > ( ) ; constructor ( config , private _plt , private _dom , elementRef , renderer , public _app , public _keyboard , public _zone , @ Optional ( ) viewCtrl , @ Optional ( ) navCtrl ) { super ( config , elementRef , renderer , 'content' ) ; const enableScrollListener = ( ) => this . _scroll . enableEvents ( ) ; this . ionScroll . onSubscribe = enableScrollListener ; this . ionScrollStart . onSubscribe = enableScrollListener ; this . ionScrollEnd . onSubscribe = enableScrollListener ; this . statusbarPadding = config . getBoolean ( 'statusbarPadding' , false ) ; this . _imgReqBfr = config . getNumber ( 'imgRequestBuffer' , 1400 ) ; this . _imgRndBfr = config . getNumber ( 'imgRenderBuffer' , 400 ) ; this . _imgVelMax = config . getNumber ( 'imgVelocityMax' , 3 ) ; this . _scroll = new ScrollView ( _app , _plt , _dom ) ; while ( navCtrl ) { if ( isTabs ( < any > navCtrl ) ) { this . _tabs = < any > navCtrl ; break ; } navCtrl = navCtrl . parent ; } if ( viewCtrl ) { this . _viewCtrl = viewCtrl ; viewCtrl . _setIONContent ( this ) ; viewCtrl . _setIONContentRef ( elementRef ) ; this . _viewCtrlReadSub = viewCtrl . readReady . subscribe ( ( ) => { this . _viewCtrlReadSub . unsubscribe ( ) ; this . _readDimensions ( ) ; } ) ; this . _viewCtrlWriteSub = viewCtrl . writeReady . subscribe ( ( ) => { this . _viewCtrlWriteSub . unsubscribe ( ) ; this . _writeDimensions ( ) ; } ) ; } else { _dom . read ( this . _readDimensions . bind ( this ) ) ; _dom . write ( this . _writeDimensions . bind ( this ) ) ; } } hidden ngAfterViewInit ( ) { assert ( this . getFixedElement ( ) , 'fixed element was not found' ) ; assert ( this . getScrollElement ( ) , 'scroll element was not found' ) ; const scroll = this . _scroll ; scroll . ev . fixedElement = this . getFixedElement ( ) ; scroll . ev . scrollElement = this . getScrollElement ( ) ; scroll . onScrollStart = ( ev ) => { this . ionScrollStart . emit ( ev ) ; } ; scroll . onScroll = ( ev ) => { this . ionScroll . emit ( ev ) ; this . imgsUpdate ( ) ; } ; scroll . onScrollEnd = ( ev ) => { this . ionScrollEnd . emit ( ev ) ; this . imgsUpdate ( ) ; } ; } hidden enableJsScroll ( ) { this . _scroll . enableJsScroll ( this . _cTop , this . _cBottom ) ; } hidden ngOnDestroy ( ) { this . _scLsn && this . _scLsn ( ) ; this . _viewCtrlReadSub && this . _viewCtrlReadSub . unsubscribe ( ) ; this . _viewCtrlWriteSub && this . _viewCtrlWriteSub . unsubscribe ( ) ; this . _viewCtrlReadSub = this . _viewCtrlWriteSub = null ; this . _scroll && this . _scroll . destroy ( ) ; this . _footerEle = this . _scLsn = this . _scroll = null ; } hidden getScrollElement ( ) { return this . _scrollContent . nativeElement ; } private getFixedElement ( ) { return this . _fixedContent . nativeElement ; } hidden onScrollElementTransitionEnd ( callback ) { this . _plt . transitionEnd ( this . getScrollElement ( ) , callback ) ; } param { number } x param { number } y param { number } duration returns { Promise } scrollTo ( x , y , duration = 300 , done ? ) { console . debug ( `content, scrollTo started, y: ${ y }, duration: ${ duration }` ) ; return this . _scroll . scrollTo ( x , y , duration , done ) ; } param { number } duration returns { Promise } scrollToTop ( duration = 300 ) { console . debug ( `content, scrollToTop, duration: ${ duration }` ) ; return this . _scroll . scrollToTop ( duration ) ; } param { number } duration returns { Promise } scrollToBottom ( duration = 300 ) { console . debug ( `content, scrollToBottom, duration: ${ duration }` ) ; return this . _scroll . scrollToBottom ( duration ) ; } input @ Input ( ) get fullscreen ( ) : boolean { return this . _fullscreen ; } set fullscreen ( val ) { this . _fullscreen = isTrueProperty ( val ) ; } input @ Input ( ) get scrollDownOnLoad ( ) : boolean { return this . _scrollDownOnLoad ; } set scrollDownOnLoad ( val ) { this . _scrollDownOnLoad = isTrueProperty ( val ) ; } private addImg ( img ) { this . _imgs . push ( img ) ; } hidden removeImg ( img ) { removeArrayItem ( this . _imgs , img ) ; } hidden setScrollElementStyle ( prop , val ) { const scrollEle = this . getScrollElement ( ) ; if ( scrollEle ) { this . _dom . write ( ( ) => { ( < any > scrollEle . style ) [ prop ] = val ; } ) ; } } returns { object } getContentDimensions ( ) { const scrollEle = this . getScrollElement ( ) ; const parentElement = scrollEle . parentElement ; return { contentHeight : parentElement . offsetHeight - this . _cTop - this . _cBottom , contentTop : this . _cTop , contentBottom : this . _cBottom , contentWidth : parentElement . offsetWidth , contentLeft : parentElement . offsetLeft , scrollHeight : scrollEle . scrollHeight , scrollTop : scrollEle . scrollTop , scrollWidth : scrollEle . scrollWidth , scrollLeft : scrollEle . scrollLeft , } ; } hidden addScrollPadding ( newPadding ) { assert ( typeof this . _scrollPadding === 'number' , '_scrollPadding must be a number' ) ; if ( newPadding === 0 ) { this . _inputPolling = false ; this . _scrollPadding = - 1 ; } if ( newPadding > this . _scrollPadding ) { console . debug ( `content, addScrollPadding, newPadding: ${ newPadding }, this._scrollPadding: ${ this . _scrollPadding }` ) ; this . _scrollPadding = newPadding ; var scrollEle = this . getScrollElement ( ) ; if ( scrollEle ) { this . _dom . write ( ( ) => { scrollEle . style . paddingBottom = ( newPadding > 0 ) ? newPadding + 'px' : '' ; } ) ; } } } hidden clearScrollPaddingFocusOut ( ) { if ( ! this . _inputPolling ) { console . debug ( `content, clearScrollPaddingFocusOut begin` ) ; this . _inputPolling = true ; this . _keyboard . onClose ( ( ) => { console . debug ( `content, clearScrollPaddingFocusOut _keyboard.onClose` ) ; this . addScrollPadding ( 0 ) ; } , 200 , 3000 ) ; } } /**
     * Tell the content to recalculate its dimensions. This should be called
     * after dynamically adding/removing headers, footers, or tabs.
     */ resize ( ) { this . _dom . read ( this . _readDimensions . bind ( this ) ) ; this . _dom . write ( this . _writeDimensions . bind ( this ) ) ; } hidden private _readDimensions ( ) { const cachePaddingTop = this . _pTop ; const cachePaddingRight = this . _pRight ; const cachePaddingBottom = this . _pBottom ; const cachePaddingLeft = this . _pLeft ; const cacheHeaderHeight = this . _hdrHeight ; const cacheFooterHeight = this . _ftrHeight ; const cacheTabsPlacement = this . _tabsPlacement ; let tabsTop = 0 ; let scrollEvent ; this . _pTop = 0 ; this . _pRight = 0 ; this . _pBottom = 0 ; this . _pLeft = 0 ; this . _hdrHeight = 0 ; this . _ftrHeight = 0 ; this . _tabsPlacement = null ; this . _tTop = 0 ; this . _fTop = 0 ; this . _fBottom = 0 ; if ( ! this . _scroll ) { return ; } scrollEvent = this . _scroll . ev ; let ele = this . getNativeElement ( ) ; if ( ! ele ) { assert ( false , 'ele should be valid' ) ; return ; } let computedStyle ; let tagName ; let parentEle = ele . parentElement ; let children = parentEle . children ; for ( var i = children . length - 1 ; i >= 0 ; i -- ) { ele = < HTMLElement > children [ i ] ; tagName = ele . tagName ; if ( tagName === 'ION-CONTENT' ) { scrollEvent . contentElement = ele ; if ( this . _fullscreen ) { computedStyle = getComputedStyle ( ele ) ; this . _pTop = parsePxUnit ( computedStyle . paddingTop ) ; this . _pBottom = parsePxUnit ( computedStyle . paddingBottom ) ; this . _pRight = parsePxUnit ( computedStyle . paddingRight ) ; this . _pLeft = parsePxUnit ( computedStyle . paddingLeft ) ; } } else if ( tagName === 'ION-HEADER' ) { scrollEvent . headerElement = ele ; this . _hdrHeight = ele . clientHeight ; } else if ( tagName === 'ION-FOOTER' ) { scrollEvent . footerElement = ele ; this . _ftrHeight = ele . clientHeight ; this . _footerEle = ele ; } } ele = parentEle ; let tabbarEle ; while ( ele && ele . tagName !== 'ION-MODAL' && ! ele . classList . contains ( 'tab-subpage' ) ) { if ( ele . tagName === 'ION-TABS' ) { tabbarEle = < HTMLElement > ele . firstElementChild ; this . _tabbarHeight = tabbarEle . clientHeight ; if ( this . _tabsPlacement === null ) { this . _tabsPlacement = ele . getAttribute ( 'tabsplacement' ) ; } } ele = ele . parentElement ; } if ( this . _tabs && this . _tabsPlacement === 'top' ) { this . _tTop = this . _hdrHeight ; tabsTop = this . _tabs . _top ; } this . _cTop = this . _hdrHeight ; this . _cBottom = this . _ftrHeight ; if ( this . _tabsPlacement === 'top' ) { this . _cTop += this . _tabbarHeight ; } else if ( this . _tabsPlacement === 'bottom' ) { this . _cBottom += this . _tabbarHeight ; } if ( this . _hasRefresher ) { this . _cTop -= 1 ; } this . _fTop = this . _cTop ; this . _fBottom = this . _cBottom ; if ( this . _fullscreen ) { this . _cTop += this . _pTop ; this . _cBottom += this . _pBottom ; } const contentDimensions = this . getContentDimensions ( ) ; scrollEvent . scrollHeight = contentDimensions . scrollHeight ; scrollEvent . scrollWidth = contentDimensions . scrollWidth ; scrollEvent . contentHeight = contentDimensions . contentHeight ; scrollEvent . contentWidth = contentDimensions . contentWidth ; scrollEvent . contentTop = contentDimensions . contentTop ; scrollEvent . contentBottom = contentDimensions . contentBottom ; this . _dirty = ( cachePaddingTop !== this . _pTop || cachePaddingBottom !== this . _pBottom || cachePaddingLeft !== this . _pLeft || cachePaddingRight !== this . _pRight || cacheHeaderHeight !== this . _hdrHeight || cacheFooterHeight !== this . _ftrHeight || cacheTabsPlacement !== this . _tabsPlacement || tabsTop !== this . _tTop || this . _cTop !== this . contentTop || this . _cBottom !== this . contentBottom ) ; this . _scroll . init ( this . getScrollElement ( ) , this . _cTop , this . _cBottom ) ; this . imgsUpdate ( ) ; } hidden private _writeDimensions ( ) { if ( ! this . _dirty ) { console . debug ( 'Skipping writeDimensions' ) ; return ; } const scrollEle = this . getScrollElement ( ) ; if ( ! scrollEle ) { assert ( false , 'this.getScrollElement() should be valid' ) ; return ; } const fixedEle = this . getFixedElement ( ) ; if ( ! fixedEle ) { assert ( false , 'this._fixedEle should be valid' ) ; return ; } if ( this . _tabsPlacement === 'bottom' && this . _cBottom > 0 && this . _footerEle ) { var footerPos = this . _cBottom - this . _ftrHeight ; assert ( footerPos >= 0 , 'footerPos has to be positive' ) ; this . _footerEle . style . bottom = cssFormat ( footerPos ) ; } let topProperty = 'marginTop' ; let bottomProperty = 'marginBottom' ; let fixedTop = this . _fTop ; let fixedBottom = this . _fBottom ; if ( this . _fullscreen ) { assert ( this . _pTop >= 0 , '_paddingTop has to be positive' ) ; assert ( this . _pBottom >= 0 , '_paddingBottom has to be positive' ) ; topProperty = 'paddingTop' ; bottomProperty = 'paddingBottom' ; } if ( this . _cTop !== this . contentTop ) { assert ( this . _cTop >= 0 , 'contentTop has to be positive' ) ; assert ( fixedTop >= 0 , 'fixedTop has to be positive' ) ; ( < any > scrollEle . style ) [ topProperty ] = cssFormat ( this . _cTop ) ; fixedEle . style . marginTop = cssFormat ( fixedTop ) ; this . contentTop = this . _cTop ; } if ( this . _cBottom !== this . contentBottom ) { assert ( this . _cBottom >= 0 , 'contentBottom has to be positive' ) ; assert ( fixedBottom >= 0 , 'fixedBottom has to be positive' ) ; ( < any > scrollEle . style ) [ bottomProperty ] = cssFormat ( this . _cBottom ) ; fixedEle . style . marginBottom = cssFormat ( fixedBottom ) ; this . contentBottom = this . _cBottom ; } if ( this . _tabsPlacement !== null && this . _tabs ) { if ( this . _tabsPlacement === 'top' ) { this . _tabs . setTabbarPosition ( this . _tTop , - 1 ) ; } else { assert ( this . _tabsPlacement === 'bottom' , 'tabsPlacement should be bottom' ) ; this . _tabs . setTabbarPosition ( - 1 , 0 ) ; } } if ( this . _scrollDownOnLoad ) { this . scrollToBottom ( 0 ) ; this . _scrollDownOnLoad = false ; } } hidden imgsUpdate ( ) { if ( this . _scroll . initialized && this . _imgs . length && this . isImgsUpdatable ( ) ) { updateImgs ( this . _imgs , this . scrollTop , this . contentHeight , this . directionY , this . _imgReqBfr , this . _imgRndBfr ) ; } } hidden isImgsUpdatable ( ) { return Math . abs ( this . _scroll . ev . velocityY ) < this . _imgVelMax ; } } export function updateImgs ( imgs , viewableTop , contentHeight , scrollDirectionY , requestableBuffer , renderableBuffer ) { const viewableBottom = ( viewableTop + contentHeight ) ; const priority1 = [ ] ; const priority2 = [ ] ; let img ; for ( var i = 0 , ilen = imgs . length ; i < ilen ; i ++ ) { img = imgs [ i ] ; if ( scrollDirectionY === 'up' ) { if ( img . top < viewableBottom && img . bottom > viewableTop - renderableBuffer ) { img . canRequest = img . canRender = true ; priority1 . push ( img ) ; continue ; } if ( img . bottom <= viewableTop && img . bottom > viewableTop - requestableBuffer ) { img . canRequest = true ; img . canRender = false ; priority2 . push ( img ) ; continue ; } if ( img . top >= viewableBottom && img . top < viewableBottom + renderableBuffer ) { img . canRequest = img . canRender = false ; continue ; } } else { if ( img . bottom > viewableTop && img . top < viewableBottom + renderableBuffer ) { img . canRequest = img . canRender = true ; priority1 . push ( img ) ; continue ; } if ( img . top >= viewableBottom && img . top < viewableBottom + requestableBuffer ) { img . canRequest = true ; img . canRender = false ; priority2 . push ( img ) ; continue ; } if ( img . bottom <= viewableTop && img . bottom > viewableTop - renderableBuffer ) { img . canRequest = img . canRender = false ; continue ; } } img . canRequest = img . canRender = false ; img . reset ( ) ; } priority1 . sort ( sortTopToBottom ) . forEach ( i => i . update ( ) ) ; if ( scrollDirectionY === 'up' ) { priority2 . sort ( sortTopToBottom ) . reverse ( ) . forEach ( i => i . update ( ) ) ; } else { priority2 . sort ( sortTopToBottom ) . forEach ( i => i . update ( ) ) ; } } function sortTopToBottom ( a , b ) { if ( a . top < b . top ) { return - 1 ; } if ( a . top > b . top ) { return 1 ; } return 0 ; } function parsePxUnit ( val ) { return ( val . indexOf ( 'px' ) > 0 ) ? parseInt ( val , 10 ) : 0 ; } function cssFormat ( val ) { return ( val > 0 ? val + 'px' : '' ) ; } export interface ContentDimensions { contentHeight : number ; contentTop : number ; contentBottom : number ; contentWidth : number ; contentLeft : number ; scrollHeight : number ; scrollTop : number ; scrollWidth : number ; scrollLeft : number ; }