import { Component , ElementRef , EventEmitter , Input , OnInit , Output , ViewChild , ViewEncapsulation } from '@angular/core' ; import { NguiAutoComplete } from './auto-complete.service' ; @ Component ( { selector : 'ngui-auto-complete' , template : `
    <div #autoCompleteContainer class="ngui-auto-complete">
      <!-- keyword input -->
      <input *ngIf="showInputTag"
             #autoCompleteInput class="keyword"
             [attr.autocomplete]="autocomplete ? 'null' : 'off'"
             placeholder="{{placeholder}}"
             (focus)="showDropdownList($event)"
             (blur)="blurHandler($event)"
             (keydown)="inputElKeyHandler($event)"
             (input)="reloadListInDelay($event)"
             [(ngModel)]="keyword"/>

      <!-- dropdown that user can select -->
      <ul *ngIf="dropdownVisible" [class.empty]="emptyList">
        <li *ngIf="isLoading && loadingTemplate" class="loading"
            [innerHTML]="loadingTemplate"></li>
        <li *ngIf="isLoading && !loadingTemplate" class="loading">{{loadingText}}</li>
        <li *ngIf="minCharsEntered && !isLoading && !filteredList.length"
            (mousedown)="selectOne('')"
            class="no-match-found">{{noMatchFoundText || 'No Result Found'}}
        </li>
        <li *ngIf="headerItemTemplate && filteredList.length" class="header-item"
            [innerHTML]="headerItemTemplate"></li>
        <li *ngIf="blankOptionText && filteredList.length"
            (mousedown)="selectOne('')"
            class="blank-item">{{blankOptionText}}
        </li>
        <li class="item"
            *ngFor="let item of filteredList; let i=index; trackBy: trackByIndex"
            (mousedown)="selectOne(item)"
            [ngClass]="{selected: i === itemIndex}"
            [innerHtml]="autoComplete.getFormattedListItem(item)">
        </li>
      </ul>

    </div>
  ` , styles : [ `
    @keyframes slideDown {
      0% {
        transform: translateY(-10px);
      }
      100% {
        transform: translateY(0px);
      }
    }

    .ngui-auto-complete {
      background-color: transparent;
    }

    .ngui-auto-complete > input {
      outline: none;
      border: 0;
      padding: 2px;
      box-sizing: border-box;
      background-clip: content-box;
    }

    .ngui-auto-complete > ul {
      background-color: #fff;
      margin: 0;
      width: 100%;
      overflow-y: auto;
      list-style-type: none;
      padding: 0;
      border: 1px solid #ccc;
      box-sizing: border-box;
      animation: slideDown 0.1s;
    }

    .ngui-auto-complete > ul.empty {
      display: none;
    }

    .ngui-auto-complete > ul li {
      padding: 2px 5px;
      border-bottom: 1px solid #eee;
    }

    .ngui-auto-complete > ul li.selected {
      background-color: #ccc;
    }

    .ngui-auto-complete > ul li:last-child {
      border-bottom: none;
    }

    .ngui-auto-complete > ul li:not(.header-item):hover {
      background-color: #ccc;
    }` ] , encapsulation : ViewEncapsulation . None } ) export class NguiAutoCompleteComponent implements OnInit { /**
     * public input properties
     */ @ Input ( 'autocomplete' ) public autocomplete = false ; @ Input ( 'list-formatter' ) public listFormatter : ( arg : any ) => string ; @ Input ( 'source' ) public source : any ; @ Input ( 'path-to-data' ) public pathToData : string ; @ Input ( 'min-chars' ) public minChars = 0 ; @ Input ( 'placeholder' ) public placeholder : string ; @ Input ( 'blank-option-text' ) public blankOptionText : string ; @ Input ( 'no-match-found-text' ) public noMatchFoundText : string ; @ Input ( 'accept-user-input' ) public acceptUserInput = true ; @ Input ( 'loading-text' ) public loadingText = 'Loading' ; @ Input ( 'loading-template' ) public loadingTemplate = null ; @ Input ( 'max-num-list' ) public maxNumList : number ; @ Input ( 'show-input-tag' ) public showInputTag = true ; @ Input ( 'show-dropdown-on-init' ) public showDropdownOnInit = false ; @ Input ( 'tab-to-select' ) public tabToSelect = true ; @ Input ( 'match-formatted' ) public matchFormatted = false ; @ Input ( 'auto-select-first-item' ) public autoSelectFirstItem = false ; @ Input ( 'select-on-blur' ) public selectOnBlur = false ; @ Input ( 're-focus-after-select' ) public reFocusAfterSelect = true ; @ Input ( 'header-item-template' ) public headerItemTemplate = null ; @ Input ( 'ignore-accents' ) public ignoreAccents = true ; @ Output ( ) public valueSelected = new EventEmitter ( ) ; @ Output ( ) public customSelected = new EventEmitter ( ) ; @ Output ( ) public textEntered = new EventEmitter ( ) ; @ ViewChild ( 'autoCompleteInput' ) public autoCompleteInput : ElementRef ; @ ViewChild ( 'autoCompleteContainer' ) public autoCompleteContainer : ElementRef ; public dropdownVisible = false ; public isLoading = false ; public filteredList : any [ ] = [ ] ; public minCharsEntered = false ; public itemIndex : number = null ; public keyword : string ; private el : HTMLElement ; private timer = 0 ; private delay = ( ( ) => { let timer = null ; return ( callback , ms ) => { clearTimeout ( timer ) ; timer = setTimeout ( callback , ms ) ; } ; } ) ( ) ; private selectOnEnter = false ; /**
     * constructor
     */ constructor ( elementRef , public autoComplete ) { this . el = elementRef . nativeElement ; } /**
     * user enters into input el, shows list to select, then select one
     */ ngOnInit ( ) { this . autoComplete . source = this . source ; this . autoComplete . pathToData = this . pathToData ; this . autoComplete . listFormatter = this . listFormatter ; if ( this . autoSelectFirstItem ) { this . itemIndex = 0 ; } setTimeout ( ( ) => { if ( this . autoCompleteInput && this . reFocusAfterSelect ) { this . autoCompleteInput . nativeElement . focus ( ) ; } if ( this . showDropdownOnInit ) { this . showDropdownList ( { target : { value : '' } } ) ; } } ) ; } public isSrcArr ( ) { return Array . isArray ( this . source ) ; } public reloadListInDelay = ( evt ) : void => { const delayMs = this . isSrcArr ( ) ? 10 : 500 ; const keyword = evt . target . value ; this . delay ( ( ) => this . reloadList ( keyword ) , delayMs ) ; } ; public showDropdownList ( event ) { this . dropdownVisible = true ; this . reloadList ( event . target . value ) ; } public hideDropdownList ( ) { this . selectOnEnter = false ; this . dropdownVisible = false ; } public findItemFromSelectValue ( selectText ) { const matchingItems = this . filteredList . filter ( ( item ) => ( '' + item ) === selectText ) ; return matchingItems . length ? matchingItems [ 0 ] : null ; } public reloadList ( keyword ) { this . filteredList = [ ] ; if ( keyword . length < ( this . minChars || 0 ) ) { this . minCharsEntered = false ; return ; } else { this . minCharsEntered = true ; } if ( this . isSrcArr ( ) ) { this . isLoading = false ; this . filteredList = this . autoComplete . filter ( this . source , keyword , this . matchFormatted , this . ignoreAccents ) ; if ( this . maxNumList ) { this . filteredList = this . filteredList . slice ( 0 , this . maxNumList ) ; } } else { this . isLoading = true ; if ( typeof this . source === 'function' ) { this . source ( keyword ) . subscribe ( ( resp ) => { if ( this . pathToData ) { const paths = this . pathToData . split ( '.' ) ; paths . forEach ( ( prop ) => resp = resp [ prop ] ) ; } this . filteredList = resp ; if ( this . maxNumList ) { this . filteredList = this . filteredList . slice ( 0 , this . maxNumList ) ; } } , ( error ) => null , ( ) => this . isLoading = false ) ; } else { this . autoComplete . getRemoteData ( keyword ) . subscribe ( ( resp ) => { this . filteredList = resp ? resp : [ ] ; if ( this . maxNumList ) { this . filteredList = this . filteredList . slice ( 0 , this . maxNumList ) ; } } , ( error ) => null , ( ) => this . isLoading = false ) ; } } } public selectOne ( data ) { if ( ! ! data || data === '' ) { this . valueSelected . emit ( data ) ; } else { this . customSelected . emit ( this . keyword ) ; } } public enterText ( data ) { this . textEntered . emit ( data ) ; } public blurHandler ( evt ) { if ( this . selectOnBlur ) { this . selectOne ( this . filteredList [ this . itemIndex ] ) ; } this . hideDropdownList ( ) ; } public inputElKeyHandler = ( evt ) => { const totalNumItem = this . filteredList . length ; if ( ! this . selectOnEnter && this . autoSelectFirstItem && ( 0 !== totalNumItem ) ) { this . selectOnEnter = true ; } switch ( evt . keyCode ) { case 27 : this . selectOnEnter = false ; this . selectOne ( undefined ) ; break ; case 38 : if ( 0 === totalNumItem ) { return ; } this . selectOnEnter = true ; this . itemIndex = ( totalNumItem + this . itemIndex - 1 ) % totalNumItem ; this . scrollToView ( this . itemIndex ) ; break ; case 40 : if ( 0 === totalNumItem ) { return ; } this . selectOnEnter = true ; this . dropdownVisible = true ; let sum = this . itemIndex ; sum = ( this . itemIndex === null ) ? 0 : sum + 1 ; this . itemIndex = ( totalNumItem + sum ) % totalNumItem ; this . scrollToView ( this . itemIndex ) ; break ; case 13 : if ( this . selectOnEnter ) { this . selectOne ( this . filteredList [ this . itemIndex ] ) ; } evt . preventDefault ( ) ; break ; case 9 : if ( this . tabToSelect ) { this . selectOne ( this . filteredList [ this . itemIndex ] ) ; } break ; } } ; public scrollToView ( index ) { const container = this . autoCompleteContainer . nativeElement ; const ul = container . querySelector ( 'ul' ) ; const li = ul . querySelector ( 'li' ) ; const liHeight = li . offsetHeight ; const scrollTop = ul . scrollTop ; const viewport = scrollTop + ul . offsetHeight ; const scrollOffset = liHeight * index ; if ( scrollOffset < scrollTop || ( scrollOffset + liHeight ) > viewport ) { ul . scrollTop = scrollOffset ; } } public trackByIndex ( index , item ) { return index ; } get emptyList ( ) : boolean { return ! ( this . isLoading || ( this . minCharsEntered && ! this . isLoading && ! this . filteredList . length ) || ( this . filteredList . length ) ) ; } }