import { ComponentTypeOrTemplateRef , getFlexiblePositions , ThyAbstractOverlayRef , ThyAbstractOverlayService } from 'ngx-tethys/core' ; import { FunctionProp , isFunction } from 'ngx-tethys/util' ; import { of , Subject } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { Directionality } from '@angular/cdk/bidi' ; import { coerceArray , coerceElement } from '@angular/cdk/coercion' ; import { ComponentType , FlexibleConnectedPositionStrategy , FlexibleConnectedPositionStrategyOrigin , Overlay , OverlayConfig , OverlayContainer , OverlayRef , PositionStrategy , ScrollStrategy , ViewportRuler } from '@angular/cdk/overlay' ; import { Platform } from '@angular/cdk/platform' ; import { ComponentPortal } from '@angular/cdk/portal' ; import { DOCUMENT } from '@angular/common' ; import { ElementRef , Inject , Injectable , Injector , NgZone , OnDestroy , Optional , StaticProvider , TemplateRef } from '@angular/core' ; import { ThyPopoverContainerComponent } from './popover-container.component' ; import { ThyInternalPopoverRef , ThyPopoverRef } from './popover-ref' ; import { THY_POPOVER_DEFAULT_CONFIG , THY_POPOVER_DEFAULT_CONFIG_VALUE , THY_POPOVER_SCROLL_STRATEGY , ThyPopoverConfig } from './popover.config' ; import { popoverAbstractOverlayOptions } from './popover.options' ; @ Injectable ( ) export class ThyPopover extends ThyAbstractOverlayService < ThyPopoverConfig , ThyPopoverContainerComponent > implements OnDestroy { private readonly ngUnsubscribe$ = new Subject ( ) ; private originInstancesMap = new Map < ElementRef | HTMLElement , { config : ThyPopoverConfig ; popoverRef : ThyPopoverRef < any , any > ; } > ( ) ; private buildPositionStrategy < TData > ( config ) { const origin = config . originPosition ? config . originPosition : config . origin ; const positionStrategy = new FlexibleConnectedPositionStrategy ( origin , this . _viewportRuler , this . _document , this . _platform , this . _overlayContainer ) ; const positions = getFlexiblePositions ( config . placement , config . offset , 'thy-popover' ) ; positionStrategy . withPositions ( positions ) ; positionStrategy . withGrowAfterOpen ( true ) ; positionStrategy . positionChanges . pipe ( takeUntil ( this . ngUnsubscribe$ ) ) . subscribe ( change => { if ( change . scrollableViewProperties . isOverlayClipped ) { this . ngZone . run ( ( ) => this . close ( ) ) ; } } ) ; return positionStrategy ; } private buildScrollStrategy ( config ) { if ( config . scrollStrategy ) { return config . scrollStrategy ; } else if ( this . scrollStrategy && isFunction ( this . scrollStrategy ) ) { return this . scrollStrategy ( ) ; } else { this . overlay . scrollStrategies . block ( ) ; } } protected buildOverlayConfig < TData > ( config ) { const positionStrategy = this . buildPositionStrategy ( config ) ; const overlayConfig = this . buildBaseOverlayConfig ( config ) ; overlayConfig . positionStrategy = positionStrategy ; overlayConfig . scrollStrategy = this . buildScrollStrategy ( config ) ; return overlayConfig ; } protected attachOverlayContainer ( overlay , config ) { const userInjector = config && config . viewContainerRef && config . viewContainerRef . injector ; const injector = Injector . create ( { parent : userInjector || this . injector , providers : [ { provide : ThyPopoverConfig , useValue : config } ] } ) ; const containerPortal = new ComponentPortal ( ThyPopoverContainerComponent , config . viewContainerRef , injector ) ; const containerRef = overlay . attach < ThyPopoverContainerComponent > ( containerPortal ) ; return containerRef . instance ; } protected createAbstractOverlayRef < T , TResult = unknown > ( overlayRef , containerInstance , config ) { return new ThyInternalPopoverRef ( overlayRef , containerInstance , config ) ; } protected createInjector < T > ( config , popoverRef , popoverContainer ) { const userInjector = config && config . viewContainerRef && config . viewContainerRef . injector ; const injectionTokens = [ { provide : ThyPopoverContainerComponent , useValue : popoverContainer } , { provide : ThyPopoverRef , useValue : popoverRef } ] ; if ( config . direction && ( ! userInjector || ! userInjector . get < Directionality | null > ( Directionality , null ) ) ) { injectionTokens . push ( { provide : Directionality , useValue : { value : config . direction , change : of ( ) } } ) ; } return Injector . create ( { parent : userInjector || this . injector , providers : injectionTokens } ) ; } private originElementAddActiveClass ( config ) { if ( config . originActiveClass ) { coerceElement < HTMLElement > ( config . origin ) . classList . add ( ... coerceArray ( config . originActiveClass ) ) ; } } private originElementRemoveActiveClass ( config ) { if ( config . originActiveClass ) { coerceElement < HTMLElement > ( config . origin ) . classList . remove ( ... coerceArray ( config . originActiveClass ) ) ; } } constructor ( overlay , injector , @ Optional ( ) @ Inject ( THY_POPOVER_DEFAULT_CONFIG ) defaultConfig , @ Inject ( THY_POPOVER_SCROLL_STRATEGY ) scrollStrategy , private ngZone , private _viewportRuler , @ Optional ( ) @ Inject ( DOCUMENT ) private _document , private _platform , private _overlayContainer ) { super ( popoverAbstractOverlayOptions , overlay , injector , { ... THY_POPOVER_DEFAULT_CONFIG_VALUE , ... defaultConfig } , scrollStrategy ) ; } private ensureCloseClosest ( origin ) { let closeAndEnd = false ; this . originInstancesMap . forEach ( ( value , key ) => { if ( value . config . manualClosure ) { if ( key === origin ) { value . popoverRef . close ( ) ; closeAndEnd = true ; } } else { if ( key === origin ) { closeAndEnd = true ; } value . popoverRef . close ( ) ; } } ) ; return closeAndEnd ; } open < T , TData = unknown , TResult = unknown > ( componentOrTemplateRef , config ? ) { const originElement = coerceElement ( config . origin ) ; if ( this . ensureCloseClosest ( originElement ) ) { return ; } const popoverRef = this . openOverlay < T , TResult > ( componentOrTemplateRef , config ) as ThyPopoverRef < T , TResult , TData > ; config = popoverRef . containerInstance . config ; popoverRef . afterClosed ( ) . subscribe ( ( ) => { this . originElementRemoveActiveClass ( config ) ; this . originInstancesMap . delete ( originElement ) ; } ) ; this . originElementAddActiveClass ( config ) ; this . originInstancesMap . set ( originElement , { config , popoverRef } ) ; return popoverRef ; } ngOnDestroy ( ) { this . dispose ( ) ; } }