import { Directive , ElementRef , Injectable , NgZone , OnDestroy , Input , OnInit , ViewContainerRef , HostBinding , Optional , Inject , ChangeDetectorRef } from '@angular/core' ; import { Platform } from '@angular/cdk/platform' ; import { OverlayRef , Overlay } from '@angular/cdk/overlay' ; import { FocusMonitor } from '@angular/cdk/a11y' ; import { ThyOverlayDirectiveBase , ThyPlacement } from 'ngx-tethys/core' ; import { ThyAutocompleteService } from './overlay/autocomplete.service' ; import { ThyAutocompleteRef } from './overlay/autocomplete-ref' ; import { ThyAutocompleteComponent } from './autocomplete.component' ; import { ThyOptionComponent , ThyOptionSelectionChangeEvent } from 'ngx-tethys/shared' ; import { DOCUMENT } from '@angular/common' ; import { Subject , Observable , merge , fromEvent , of , Subscription } from 'rxjs' ; import { ESCAPE , UP_ARROW , ENTER , DOWN_ARROW , TAB } from 'ngx-tethys/util' ; import { filter , map , take , tap , delay , switchMap } from 'rxjs/operators' ; import { ScrollToService } from 'ngx-tethys/core' ; import { warnDeprecation } from 'ngx-tethys/util' ; @ Directive ( { selector : 'input[thyAutocompleteTrigger], textarea[thyAutocompleteTrigger]' , exportAs : 'thyAutocompleteTrigger' , host : { '(input)' : 'handleInput($event)' , '(focusin)' : 'onFocus()' , '(keydown)' : 'onKeydown($event)' } } ) export class ThyAutocompleteTriggerDirective implements OnInit , OnDestroy { protected overlayRef : OverlayRef ; private autocompleteRef : ThyAutocompleteRef < ThyAutocompleteComponent > ; private readonly closeKeyEventStream = new Subject < void > ( ) ; private closingActionsSubscription : Subscription ; private _autocompleteComponent : ThyAutocompleteComponent ; @ HostBinding ( `class.thy-autocomplete-opened` ) panelOpened = false ; @ Input ( 'thyAutocompleteComponent' ) set autocompleteComponent ( data ) { this . _autocompleteComponent = data ; } @ Input ( 'thyAutocomplete' ) set autocomplete ( data ) { warnDeprecation ( `The property thyAutocomplete will be deprecated, please use thyAutocompleteComponent instead.` ) ; this . _autocompleteComponent = data ; } get autocompleteComponent ( ) { return this . _autocompleteComponent ; } @ Input ( ) thyOffset = 4 ; @ Input ( ) thyAutocompleteWidth : number ; @ Input ( ) thyPlacement : ThyPlacement = 'bottomLeft' ; get activeOption ( ) : ThyOptionComponent | null { if ( this . autocompleteComponent && this . autocompleteComponent . keyManager ) { return this . autocompleteComponent . keyManager . activeItem ; } return null ; } get panelClosingActions ( ) : Observable < ThyOptionSelectionChangeEvent | null > { return merge ( this . autocompleteComponent . thyOptionSelected , this . autocompleteComponent . keyManager . tabOut . pipe ( filter ( ( ) => this . panelOpened ) ) , this . closeKeyEventStream , this . getOutsideClickStream ( ) , this . overlayRef ? this . overlayRef . detachments ( ) . pipe ( filter ( ( ) => this . panelOpened ) ) : of ( ) ) . pipe ( map ( event => ( event instanceof ThyOptionSelectionChangeEvent ? event : null ) ) ) ; } constructor ( private elementRef , private ngZone , private overlay , private autocompleteService , private viewContainerRef , @ Optional ( ) @ Inject ( DOCUMENT ) private document , private cdr ) { } ngOnInit ( ) { } onFocus ( ) { if ( this . canOpen ( ) ) { this . openPanel ( ) ; } } onKeydown ( event ) { const keyCode = event . keyCode ; if ( keyCode === ESCAPE ) { event . preventDefault ( ) ; } if ( this . activeOption && keyCode === ENTER && this . panelOpened ) { this . activeOption . selectViaInteraction ( ) ; this . resetActiveItem ( ) ; event . preventDefault ( ) ; } else if ( this . autocompleteComponent ) { const prevActiveItem = this . autocompleteComponent . keyManager . activeItem ; const isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW ; if ( this . panelOpened || keyCode === TAB ) { this . autocompleteComponent . keyManager . onKeydown ( event ) ; } else if ( isArrowKey && this . canOpen ( ) ) { this . openPanel ( ) ; } if ( ( isArrowKey || this . autocompleteComponent . keyManager . activeItem !== prevActiveItem ) && this . autocompleteComponent . keyManager . activeItem ) { ScrollToService . scrollToElement ( this . autocompleteComponent . keyManager . activeItem . element . nativeElement , this . autocompleteComponent . optionsContainer . nativeElement ) ; } } } handleInput ( event ) { if ( this . canOpen ( ) && document . activeElement === event . target ) { this . openPanel ( ) ; } } openPanel ( ) { if ( this . overlayRef && this . overlayRef . hasAttached ( ) ) { return ; } const overlayRef = this . createOverlay ( ) ; this . overlayRef = overlayRef ; overlayRef . keydownEvents ( ) . subscribe ( event => { if ( event . keyCode === ESCAPE || ( event . keyCode === UP_ARROW && event . altKey ) ) { this . resetActiveItem ( ) ; this . closeKeyEventStream . next ( ) ; event . stopPropagation ( ) ; event . preventDefault ( ) ; } } ) ; this . panelOpened = true ; this . autocompleteComponent . open ( ) ; } closePanel ( ) { if ( this . autocompleteRef ) { this . autocompleteRef . close ( ) ; this . cdr . detectChanges ( ) ; this . closingActionsSubscription . unsubscribe ( ) ; } } createOverlay ( ) { const config = Object . assign ( { origin : this . elementRef . nativeElement , viewContainerRef : this . viewContainerRef , placement : this . thyPlacement , offset : this . thyOffset , scrollStrategy : this . overlay . scrollStrategies . reposition ( ) , width : this . thyAutocompleteWidth || this . elementRef . nativeElement . clientWidth } ) ; this . autocompleteRef = this . autocompleteService . open ( this . autocompleteComponent . contentTemplateRef , config ) ; this . autocompleteRef . afterClosed ( ) . subscribe ( ( ) => { this . panelOpened = false ; this . autocompleteComponent . close ( ) ; } ) ; this . autocompleteRef . afterOpened ( ) . pipe ( delay ( 200 ) ) . subscribe ( ( ) => { this . closingActionsSubscription = this . subscribeToClosingActions ( ) ; } ) ; return this . autocompleteRef . getOverlayRef ( ) ; } /**
     * This method listens to a stream of panel closing actions and resets the
     * stream every time the option list changes.
     */ private subscribeToClosingActions ( ) { const firstStable = this . ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) ; const optionChanges = this . autocompleteComponent . options . changes . pipe ( delay ( 0 ) ) ; return ( merge ( firstStable , optionChanges ) . pipe ( switchMap ( ( ) => { this . resetActiveItem ( ) ; if ( this . panelOpened ) { this . overlayRef . updatePosition ( ) ; } return this . panelClosingActions ; } ) , take ( 1 ) ) . subscribe ( event => this . setValueAndClose ( event ) ) ) ; } private setValueAndClose ( event ) { if ( event && event . option ) { this . setValue ( event . option . thyLabelText ) ; } this . closePanel ( ) ; } /** Stream of clicks outside of the autocomplete panel. */ private getOutsideClickStream ( ) { return merge ( fromEvent ( this . document , 'click' ) as Observable < MouseEvent > , fromEvent ( this . document , 'touchend' ) as Observable < TouchEvent > ) . pipe ( filter ( event => { const clickTarget = event . target as HTMLElement ; const formField = null ; return ( this . panelOpened && clickTarget !== this . elementRef . nativeElement && ( ! formField || ! formField . contains ( clickTarget ) ) && ! ! this . overlayRef && ! this . overlayRef . overlayElement . contains ( clickTarget ) ) ; } ) ) ; } private setValue ( value ) { this . elementRef . nativeElement . value = value ; this . elementRef . nativeElement . focus ( ) ; } private canOpen ( ) { const element = this . elementRef . nativeElement ; return ! element . readOnly && ! element . disabled ; } private resetActiveItem ( ) { this . autocompleteComponent . keyManager . setActiveItem ( this . autocompleteComponent . thyAutoActiveFirstOption ? 0 : - 1 ) ; } private destroyPanel ( ) { if ( this . overlayRef ) { this . closePanel ( ) ; this . overlayRef . dispose ( ) ; this . overlayRef = null ; } } ngOnDestroy ( ) { this . closeKeyEventStream . complete ( ) ; this . destroyPanel ( ) ; } }