import { Component , ContentChildren , EventEmitter , HostListener , Injector , Input , QueryList , TemplateRef , ViewChild , AfterViewInit } from '@angular/core' ; import { Observable } from 'rxjs' ; import { filter , first , debounceTime , distinctUntilChanged } from 'rxjs/operators' ; import { Ng2Dropdown , Ng2MenuItem } from 'ng2-material-dropdown' ; import { defaults } from '../../defaults' ; import { TagInputComponent } from '../tag-input/tag-input' ; import { TagModel } from '../../core/tag-model' ; @ Component ( { selector : 'tag-input-dropdown' , templateUrl : './tag-input-dropdown.template.html' } ) export class TagInputDropdown implements AfterViewInit { name @ ViewChild ( Ng2Dropdown ) public dropdown : Ng2Dropdown ; name desc @ ContentChildren ( TemplateRef ) public templates : QueryList < TemplateRef < any > > ; name @ Input ( ) public offset : string = defaults . dropdown . offset ; name @ Input ( ) public focusFirstElement = defaults . dropdown . focusFirstElement ; name @ Input ( ) public showDropdownIfEmpty = defaults . dropdown . showDropdownIfEmpty ; description name @ Input ( ) public autocompleteObservable : ( text : string ) => Observable < any > ; name @ Input ( ) public minimumTextLength = defaults . dropdown . minimumTextLength ; name @ Input ( ) public limitItemsTo : number = defaults . dropdown . limitItemsTo ; name @ Input ( ) public displayBy = defaults . dropdown . displayBy ; name @ Input ( ) public identifyBy = defaults . dropdown . identifyBy ; description name @ Input ( ) public matchingFn : ( value : string , target : TagModel ) => boolean = defaults . dropdown . matchingFn ; name @ Input ( ) public appendToBody = defaults . dropdown . appendToBody ; name description @ Input ( ) public keepOpen = defaults . dropdown . keepOpen ; name @ Input ( ) public dynamicUpdate = defaults . dropdown . dynamicUpdate ; name @ Input ( ) public zIndex = defaults . dropdown . zIndex ; name public items : TagModel [ ] = [ ] ; name public tagInput : TagInputComponent = this . injector . get ( TagInputComponent ) ; name private _autocompleteItems : TagModel [ ] = [ ] ; name param items public set autocompleteItems ( items ) { this . _autocompleteItems = items ; } name desc @ Input ( ) public get autocompleteItems ( ) : TagModel [ ] { const items = this . _autocompleteItems ; if ( ! items ) { return [ ] ; } return items . map ( ( item ) => { return typeof item === 'string' ? { [ this . displayBy ] : item , [ this . identifyBy ] : item } : item ; } ) ; } constructor ( private readonly injector ) { } name ngAfterViewInit ( ) { this . onItemClicked ( ) . subscribe ( ( item ) => { this . requestAdding ( item ) ; } ) ; this . onHide ( ) . subscribe ( this . resetItems ) ; const DEBOUNCE_TIME = 200 ; const KEEP_OPEN = this . keepOpen ; this . tagInput . onTextChange . asObservable ( ) . pipe ( distinctUntilChanged ( ) , debounceTime ( DEBOUNCE_TIME ) , filter ( ( value ) => { if ( KEEP_OPEN === false ) { return value . length > 0 ; } return true ; } ) ) . subscribe ( this . show ) ; } name public updatePosition ( ) { const position = this . tagInput . inputForm . getElementPosition ( ) ; this . dropdown . menu . updatePosition ( position , this . dynamicUpdate ) ; } name public get isVisible ( ) : boolean { return this . dropdown . menu . dropdownState . menuState . isVisible ; } name public onHide ( ) { return this . dropdown . onHide ; } name public onItemClicked ( ) { return this . dropdown . onItemClicked ; } name public get selectedItem ( ) : Ng2MenuItem { return this . dropdown . menu . dropdownState . dropdownState . selectedItem ; } name public get state ( ) : any { return this . dropdown . menu . dropdownState ; } name public show = ( ) : void => { const maxItemsReached = this . tagInput . items . length === this . tagInput . maxItems ; const value = this . getFormValue ( ) ; const hasMinimumText = value . trim ( ) . length >= this . minimumTextLength ; const position = this . calculatePosition ( ) ; const items = this . getMatchingItems ( value ) ; const hasItems = items . length > 0 ; const isHidden = this . isVisible === false ; const showDropdownIfEmpty = this . showDropdownIfEmpty && hasItems && ! value ; const isDisabled = this . tagInput . disable ; const shouldShow = isHidden && ( ( hasItems && hasMinimumText ) || showDropdownIfEmpty ) ; const shouldHide = this . isVisible && ! hasItems ; if ( this . autocompleteObservable && hasMinimumText ) { return this . getItemsFromObservable ( value ) ; } if ( ( ! this . showDropdownIfEmpty && ! value ) || maxItemsReached || isDisabled ) { return this . dropdown . hide ( ) ; } this . setItems ( items ) ; if ( shouldShow ) { this . dropdown . show ( position ) ; } else if ( shouldHide ) { this . hide ( ) ; } } ; name public hide ( ) { this . resetItems ( ) ; this . dropdown . hide ( ) ; } name @ HostListener ( 'window:scroll' ) public scrollListener ( ) { if ( ! this . isVisible || ! this . dynamicUpdate ) { return ; } this . updatePosition ( ) ; } name @ HostListener ( 'window:blur' ) public onWindowBlur ( ) { this . dropdown . hide ( ) ; } name private getFormValue ( ) { const formValue = this . tagInput . formValue ; return formValue ? formValue . toString ( ) . trim ( ) : '' ; } name private calculatePosition ( ) { return this . tagInput . inputForm . getElementPosition ( ) ; } name param item { Ng2MenuItem } private requestAdding = async ( item ) => { const tag = this . createTagModel ( item ) ; await this . tagInput . onAddingRequested ( true , tag ) . catch ( ( ) => { } ) ; } ; name param item private createTagModel ( item ) { const display = typeof item . value === 'string' ? item . value : item . value [ this . displayBy ] ; const value = typeof item . value === 'string' ? item . value : item . value [ this . identifyBy ] ; return { ... item . value , [ this . tagInput . displayBy ] : display , [ this . tagInput . identifyBy ] : value } ; } param value { string } private getMatchingItems ( value ) { if ( ! value && ! this . showDropdownIfEmpty ) { return [ ] ; } const dupesAllowed = this . tagInput . allowDupes ; return this . autocompleteItems . filter ( ( item ) => { const hasValue = dupesAllowed ? false : this . tagInput . tags . some ( tag => { const identifyBy = this . tagInput . identifyBy ; const model = typeof tag . model === 'string' ? tag . model : tag . model [ identifyBy ] ; return model === item [ this . identifyBy ] ; } ) ; return this . matchingFn ( value , item ) && hasValue === false ; } ) ; } name private setItems ( items ) { this . items = items . slice ( 0 , this . limitItemsTo || items . length ) ; } name private resetItems = ( ) : void => { this . items = [ ] ; } ; name param data private populateItems ( data ) { this . autocompleteItems = data . map ( item => { return typeof item === 'string' ? { [ this . displayBy ] : item , [ this . identifyBy ] : item } : item ; } ) ; return this ; } name param text private getItemsFromObservable = ( text ) : void => { this . setLoadingState ( true ) ; const subscribeFn = ( data ) => { this . setLoadingState ( false ) . populateItems ( data ) ; this . setItems ( this . getMatchingItems ( text ) ) ; if ( this . items . length ) { this . dropdown . show ( this . calculatePosition ( ) ) ; } else { this . dropdown . hide ( ) ; } } ; this . autocompleteObservable ( text ) . pipe ( first ( ) ) . subscribe ( subscribeFn , ( ) => this . setLoadingState ( false ) ) ; } ; name param state private setLoadingState ( state ) { this . tagInput . isLoading = state ; return this ; } }