import { ChangeDetectionStrategy , Component , EventEmitter , Renderer2 , ViewChild , Output , Input , OnInit , AfterContentInit , OnChanges , SimpleChanges , ContentChildren , QueryList , Inject } from '@angular/core' ; import { Platform } from '@ionic/angular' ; import { HAMMER_GESTURE_CONFIG , HammerGestureConfig } from '@angular/platform-browser' ; import 'hammerjs' ; export interface FooterMetadata { height : number ; posY : number ; lastPosY : number ; toolbarDefaultHeight ? : number ; toolbarDefaultExpandedPosition ? : number ; toolbarUpperBoundary ? : number ; toolbarLowerBoundary ? : number ; ionContentRef ? : any ; } export interface ViewMetadata { tabsRef ? : Element ; tabsHeight ? : number ; hasBottomTabs ? : boolean ; toolbarRef ? : Element ; toolbarHeight ? : number ; bottomSpace ? : number ; } export interface FooterTab { x ? : number ; y ? : number ; upperLeftRadius ? : number ; upperRightRadius ? : number ; backgroundColor ? : string ; color ? : string ; content ? : string ; } export enum IonPullUpFooterState { Collapsed = 0 , Expanded = 1 , Minimized = 2 } export enum IonPullUpFooterBehavior { Hide , Expand } export interface DraggedOutputEvent { delta : number ; toolbarAbsolutePosition : DOMRect ; } @ Component ( { selector : 'lib-ionic-pullup' , templateUrl : './ionic-pullup.component.html' , styleUrls : [ './ionic-pullup.component.scss' ] , changeDetection : ChangeDetectionStrategy . OnPush , } ) export class IonicPullupComponent implements OnInit , AfterContentInit , OnChanges { @ Input ( ) state : IonPullUpFooterState ; @ Output ( ) stateChange : EventEmitter < IonPullUpFooterState > = new EventEmitter < IonPullUpFooterState > ( ) ; @ Input ( ) initialState : IonPullUpFooterState ; @ Input ( ) defaultBehavior : IonPullUpFooterBehavior ; /**
     *  Maximum expanded position - useful if there are top headers
     *  If not provided by default computes available screen minus tabs and headers
     */ @ Input ( ) toolbarTopMargin = 0 ; /**
     *  Minimum position - useful to keep a part of the footer always visible at the bottom
     */ @ Input ( ) minBottomVisible = 0 ; /**
     * If true, footer can be docked at the bottom
     */ @ Input ( ) dockable : boolean ; @ Output ( ) expanded = new EventEmitter < any > ( ) ; @ Output ( ) collapsed = new EventEmitter < any > ( ) ; @ Output ( ) minimized = new EventEmitter < any > ( ) ; /**
     * Outputs the amount of pixels the user has dragged positive or negative
     */ @ Output ( ) dragged = new EventEmitter < DraggedOutputEvent > ( ) ; @ ViewChild ( 'footer' , { static : true } ) childFooter ; @ ContentChildren ( 'ionDragFooter' ) dragElements ! : QueryList < any > ; protected footerMeta : FooterMetadata ; protected currentViewMeta : ViewMetadata ; constructor ( private platform , private renderer , @ Inject ( HAMMER_GESTURE_CONFIG ) private hammerConfig ) { this . footerMeta = { height : 0 , posY : 0 , lastPosY : 0 } ; this . currentViewMeta = { bottomSpace : screen . height - window . innerHeight } ; this . initialState = this . initialState ?? IonPullUpFooterState . Collapsed ; this . defaultBehavior = this . defaultBehavior ?? IonPullUpFooterBehavior . Expand ; } ngOnInit ( ) { window . addEventListener ( 'orientationchange' , ( ) => { this . updateUI ( ) ; this . collapse ( ) ; } ) ; this . platform . resume . subscribe ( ( ) => { this . updateUI ( ) ; this . collapse ( ) ; } ) ; this . footerMeta . toolbarLowerBoundary = this . dockable ? this . minBottomVisible : 0 ; } ngAfterContentInit ( ) { this . computeDefaults ( ) ; this . state = IonPullUpFooterState . Collapsed ; this . updateUI ( ) ; } public get expandedHeight ( ) : number { return window . innerHeight - this . currentViewMeta . toolbarHeight - this . currentViewMeta . tabsHeight ; } computeDefaults ( ) { setTimeout ( ( ) => { this . footerMeta . toolbarDefaultHeight = this . childFooter . nativeElement . offsetHeight ; this . findIonicComponentsInPage ( ) ; this . dragElements . forEach ( elem => { const hammer = this . hammerConfig . buildHammer ( elem . el ) ; hammer . on ( 'pan panstart panend' , ( ev ) => { this . onPan ( ev ) ; } ) ; } ) ; } , 300 ) ; } computeHeights ( ) { this . footerMeta . height = this . expandedHeight ; this . footerMeta . toolbarDefaultExpandedPosition = - this . footerMeta . height + this . footerMeta . toolbarDefaultHeight + this . minBottomVisible ; this . footerMeta . toolbarUpperBoundary = this . footerMeta . height - this . footerMeta . toolbarDefaultHeight - this . minBottomVisible ; this . renderer . setStyle ( this . childFooter . nativeElement , 'height' , this . footerMeta . height + 'px' ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'top' , `${ window . innerHeight - this . footerMeta . toolbarDefaultHeight - this . currentViewMeta . tabsHeight - this . minBottomVisible }px` ) ; this . updateIonContentHeight ( ) ; } updateUI ( isInit = false ) { if ( ! this . childFooter ) { return ; } setTimeout ( ( ) => { this . computeHeights ( ) ; } , 300 ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'transition' , 'none' ) ; } expand ( ) { this . footerMeta . lastPosY = this . footerMeta . toolbarDefaultExpandedPosition ; this . updateIonContentHeight ( this . minBottomVisible - this . footerMeta . lastPosY ) ; this . renderer . setStyle ( this . childFooter . nativeElement , '-webkit-transform' , `translate3d(0, ${ this . footerMeta . lastPosY }px, 0)` ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'transform' , `translate3d(0, ${ this . footerMeta . lastPosY }px, 0)` ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'transition' , '300ms ease-in-out' ) ; this . expanded . emit ( null ) ; } collapse ( isInit = false ) { if ( ! this . childFooter ) { return ; } this . footerMeta . lastPosY = 0 ; this . renderer . setStyle ( this . childFooter . nativeElement , '-webkit-transform' , `translate3d(0, ${ this . footerMeta . lastPosY }px, 0)` ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'transform' , `translate3d(0, ${ this . footerMeta . lastPosY }px, 0)` ) ; setTimeout ( ( ) => this . updateIonContentHeight ( this . minBottomVisible - this . footerMeta . lastPosY ) , 300 ) ; if ( ! isInit ) { this . collapsed . emit ( null ) ; } } /**
     * TODO
     */ minimize ( ) { this . footerMeta . lastPosY = this . footerMeta . height ; this . renderer . setStyle ( this . childFooter . nativeElement , '-webkit-transform' , 'translate3d(0, ' + this . footerMeta . lastPosY + 'px, 0)' ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'transform' , 'translate3d(0, ' + this . footerMeta . lastPosY + 'px, 0)' ) ; this . minimized . emit ( null ) ; } onTap ( e ) { e . preventDefault ( ) ; if ( this . state === IonPullUpFooterState . Collapsed ) { if ( this . defaultBehavior === IonPullUpFooterBehavior . Hide ) { this . state = IonPullUpFooterState . Minimized ; } else { this . state = IonPullUpFooterState . Expanded ; } } else { if ( this . state === IonPullUpFooterState . Minimized ) { if ( this . defaultBehavior === IonPullUpFooterBehavior . Hide ) { this . state = IonPullUpFooterState . Collapsed ; } else { this . state = IonPullUpFooterState . Expanded ; } } else { this . state = this . initialState === IonPullUpFooterState . Minimized ? IonPullUpFooterState . Minimized : IonPullUpFooterState . Collapsed ; } } } onPan ( e ) { this . renderer . setStyle ( this . childFooter . nativeElement , 'transition' , 'none' ) ; e . preventDefault ( ) ; switch ( e . type ) { case 'pan' : this . footerMeta . posY = e . deltaY + this . footerMeta . lastPosY ; this . footerMeta . posY = this . footerMeta . posY > this . footerMeta . toolbarLowerBoundary ? this . footerMeta . toolbarLowerBoundary : ( Math . abs ( this . footerMeta . posY ) > this . footerMeta . toolbarUpperBoundary ? this . footerMeta . toolbarDefaultExpandedPosition : this . footerMeta . posY ) ; this . updateIonContentHeight ( this . minBottomVisible - this . footerMeta . posY ) ; this . renderer . setStyle ( this . childFooter . nativeElement , '-webkit-transform' , 'translate3d(0, ' + this . footerMeta . posY + 'px, 0)' ) ; this . renderer . setStyle ( this . childFooter . nativeElement , 'transform' , 'translate3d(0, ' + this . footerMeta . posY + 'px, 0)' ) ; break ; case 'panend' : this . renderer . setStyle ( this . childFooter . nativeElement , 'transition' , '300ms ease-in-out' ) ; this . footerMeta . lastPosY = this . footerMeta . posY ; this . updateIonContentHeight ( this . minBottomVisible - this . footerMeta . lastPosY ) ; const handle = this . dragElements . first ; this . dragged . emit ( { delta : this . footerMeta . lastPosY , toolbarAbsolutePosition : handle ? handle . el . getBoundingClientRect ( ) : null } ) ; break ; } } ngOnChanges ( changes ) { if ( changes . state ?. firstChange ?? changes . state ?. currentValue === changes . state ?. previousValue ) { return ; } switch ( this . state ) { case IonPullUpFooterState . Collapsed : this . collapse ( ) ; break ; case IonPullUpFooterState . Expanded : this . expand ( ) ; break ; case IonPullUpFooterState . Minimized : this . minimize ( ) ; break ; } window . setTimeout ( ( ) => { this . stateChange . emit ( this . state ) ; } ) ; } /**
     * Detect ionic components in page
     */ private findIonicComponentsInPage ( ) { this . footerMeta . ionContentRef = this . childFooter . nativeElement . querySelector ( 'ion-content' ) ; this . currentViewMeta . tabsRef = document . querySelector ( 'ion-tab-bar' ) ; this . currentViewMeta . tabsHeight = this . currentViewMeta . tabsRef ? ( this . currentViewMeta . tabsRef as HTMLElement ) . offsetHeight : 0 ; if ( ! this . toolbarTopMargin ) { const outletRef = document . querySelector ( 'ion-router-outlet' ) ; if ( outletRef ) { const headerRef = outletRef . querySelector ( 'ion-header' ) ; if ( headerRef ) { this . currentViewMeta . toolbarRef = headerRef . querySelector ( 'ion-toolbar' ) ; this . currentViewMeta . toolbarHeight = this . currentViewMeta . toolbarRef . clientHeight ; } else { this . currentViewMeta . toolbarHeight = 0 ; } } } else { this . currentViewMeta . toolbarHeight = this . toolbarTopMargin ; } } param maxHeight private updateIonContentHeight ( maxHeight ? ) { if ( ! this . footerMeta . ionContentRef ) { return ; } const maxHeightUnits = maxHeight ? `${ maxHeight }px` : ( this . minBottomVisible > 0 ? `${ this . minBottomVisible }px` : '100%' ) ; this . renderer . setStyle ( this . footerMeta . ionContentRef , 'max-height' , maxHeightUnits ) ; } }