import { Component , Input , Output , EventEmitter , ViewChild , OnInit , forwardRef } from '@angular/core' ; import { NG_VALUE_ACCESSOR , ControlValueAccessor } from '@angular/forms' ; const noop = ( ) => { } ; export const TYPEAHEAD_CONTROL_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => Typeahead ) , multi : true } ; @ Component ( { selector : 'typeahead' , template : `
    <div class="typeahead">

      <input #inputElement
        [placeholder]="placeholder"
        [(ngModel)]="input"
        type="text"
        [ngClass]="{'typeahead-input': true, 'typeahead-input-has-selection': hasSelection()}"
        typeahead="off"
        spellcheck="false"
        (keyup)="inputKeyUp($event)"
        (keydown)="inputKeyDown($event)"
        (focus)="inputFocus($event)"
        (blur)="inputBlur($event)"
        [disabled]="isDisabled">

      <input type="text"
        class="typeahead-typeahead"
        [(ngModel)]="typeahead"
        typeahead="off"
        spellcheck="false"
        disabled="true">

      <div #suggestionsContainer
        class="typeahead-suggestions"
        [hidden]="!areSuggestionsVisible">

        <ul (mouseout)="suggestionsMouseOut($event)">

          <li *ngFor="let suggestion of suggestions"
            (mouseover)="suggestionMouseOver(suggestion)"
            (mousedown)="suggestionMouseDown(suggestion)"
            [ngClass]="{'typeahead-suggestion-active': activeSuggestion===suggestion}">{{ suggestion[displayProperty] }}</li>

        </ul>

      </div>

    </div>
    ` , styles : [ `
    .typeahead {
      position: relative;
      width: 100%;
      text-align: left;
      vertical-align: top;
      padding-bottom: 2.5em;
    }

    .typeahead-input {
      border-color: transparent;
      position: absolute;
      z-index: 10;
      background-color: transparent;
      background-repeat: no-repeat;
      background-position: right 10px;
      background-size: 28px 18px;
    }

    .typeahead-input-has-selection {
      background-color: #f5f5f5;
      border: 1px solid #4c4845;
      color: #008fca;
    }

    .typeahead-typeahead {
      color: rgb(128, 128, 128);
      position: absolute;
      z-index: 5;
      text-align: start;
      background-color: rgb(255, 255, 255);
    }

    .typeahead-suggestions {
      position: absolute;
      top: 42px;
      overflow-y: auto;
      color: #666666;
      border-radius: 3px;
      padding: 0;
      background-color: #f5f5f5;
      width: 100%;
      max-height: 18em !important;
      border: 1px solid #e0e0e0;
      z-index: 100;
    }

    .typeahead-suggestions ul {
      list-style-type: none;
      padding-left: 0;
      margin-top: 3px;
    }

    .typeahead-suggestions ul li {
      padding: 6px !important;
      font-size: 0.9em;
      border-bottom: 1px solid #e0e0e0;
    }

    .typeahead-suggestion-active {
      background-color: #008fca;
      color: #ffffff;
    }
    ` ] , providers : [ TYPEAHEAD_CONTROL_VALUE_ACCESSOR ] } ) export class Typeahead implements OnInit , ControlValueAccessor { abstract ; /**
     * The complete list of items.
     */ @ Input ( ) list : any [ ] = [ ] ; /**
     * Input element placeholder text.
     */ @ Input ( ) placeholder : string = '' ; /**
     * The property of a list item that should be used for matching.
     */ @ Input ( ) searchProperty : string = 'name' ; /**
     * The property of a list item that should be displayed.
     */ @ Input ( ) displayProperty : string = 'name' ; /**
     * The maximum number of suggestions to display.
     */ @ Input ( ) maxSuggestions : number = - 1 ; /**
     * Event that occurs when a suggestion is selected.
     */ @ Output ( ) suggestionSelected = new EventEmitter < any > ( ) ; /**
     * Handle to the input element.
     */ @ ViewChild ( 'inputElement' ) private inputElement : any ; /**
     * The input element's value.
     */ private input : string ; /**
     * The typeahead element's value. This element is displayed behind the input element.
     */ private typeahead : string ; /**
     * The previously entered input string.
     */ private previousInput : string ; /**
     * The filtered list of suggestions.
     */ private suggestions : any [ ] = [ ] ; /**
     * Indicates whether the suggestions are visible.
     */ private areSuggestionsVisible : boolean = false ; /**
     * The currently selected suggestion.
     */ private selectedSuggestion : any ; /**
     * The active (highlighted) suggestion.
     */ private activeSuggestion : any ; /**
     * Indicates whether the control is disabled.
     */ private isDisabled : boolean = false ; /**
     * Creates and initializes a new typeahead component.
     */ constructor ( ) { } /**
     * Implement this interface to execute custom initialization logic after your
     * directive's data-bound properties have been initialized.
     *
     * ngOnInit is called right after the directive's data-bound properties have
     * been checked for the first time, and before any of its
     * children have been checked. It is invoked only once when the directive is
     * instantiated.
     */ public ngOnInit ( ) { } /**
     * Placeholder for a callback which is later provided by the Control Value Accessor.
     */ private onTouchedCallback : ( ) => void = noop ; /**
     * Placeholder for a callback which is later provided by the Control Value Accessor.
     */ private onChangeCallback : ( _ : any ) => void = noop ; /**
     * Get accessor.
     */ get value ( ) : any { return this . selectedSuggestion ; } /**
     * Set accessor including call the onchange callback.
     */ set value ( v ) { if ( v !== this . selectedSuggestion ) { this . selectSuggestion ( v ) ; this . onChangeCallback ( v ) ; } } /**
     * From ControlValueAccessor interface.
     */ writeValue ( value ) { if ( value !== this . selectedSuggestion ) { this . selectSuggestion ( value ) ; } } /**
     * From ControlValueAccessor interface.
     */ registerOnChange ( fn ) { this . onChangeCallback = fn ; } /**
     * From ControlValueAccessor interface.
     */ registerOnTouched ( fn ) { this . onTouchedCallback = fn ; } param isDisabled setDisabledState ( isDisabled ) { this . isDisabled = isDisabled ; } /**
     * Called when a keydown event is fired on the input element.
     */ public inputKeyDown ( event ) { if ( event . which === 9 || event . keyCode === 9 ) { if ( ! this . areSuggestionsVisible ) { return ; } this . selectSuggestion ( this . activeSuggestion ) ; this . suggestions . splice ( 1 ) ; this . areSuggestionsVisible = false ; event . preventDefault ( ) ; } else if ( event . which === 38 || event . keyCode === 38 ) { let activeSuggestionIndex = this . getActiveSuggestionIndex ( ) ; if ( activeSuggestionIndex === - 1 ) { this . setActiveSuggestion ( this . suggestions [ 0 ] ) ; return ; } if ( activeSuggestionIndex === 0 ) { this . setActiveSuggestion ( this . suggestions [ this . suggestions . length - 1 ] ) ; } else { this . setActiveSuggestion ( this . suggestions [ activeSuggestionIndex - 1 ] ) ; } } else if ( event . which === 40 || event . keyCode === 40 ) { let activeSuggestionIndex = this . getActiveSuggestionIndex ( ) ; if ( activeSuggestionIndex === - 1 ) { this . setActiveSuggestion ( this . suggestions [ 0 ] ) ; return ; } if ( activeSuggestionIndex === ( this . suggestions . length - 1 ) ) { this . setActiveSuggestion ( this . suggestions [ 0 ] ) ; } else { this . setActiveSuggestion ( this . suggestions [ activeSuggestionIndex + 1 ] ) ; } } else if ( ( event . which === 10 || event . which === 13 || event . keyCode === 10 || event . keyCode === 13 ) && this . areSuggestionsVisible ) { this . selectSuggestion ( this . activeSuggestion ) ; this . areSuggestionsVisible = false ; event . preventDefault ( ) ; } } /**
     * Sets the active (highlighted) suggestion.
     */ public setActiveSuggestion ( suggestion ) { this . activeSuggestion = suggestion ; this . populateTypeahead ( ) ; } /**
     * Gets the index of the active suggestion within the suggestions list.
     */ public getActiveSuggestionIndex ( ) { let activeSuggestionIndex = - 1 ; if ( this . activeSuggestion != null ) { activeSuggestionIndex = this . indexOfObject ( this . suggestions , this . searchProperty , this . activeSuggestion [ this . searchProperty ] ) ; } return activeSuggestionIndex ; } /**
     * Gets the index of an object in a list by matching a property value.
     */ public indexOfObject ( array , property , value ) { if ( array == null || array . length === 0 ) return - 1 ; let index = - 1 ; for ( let i = 0 ; i < array . length ; i ++ ) { if ( array [ i ] [ property ] != null && array [ i ] [ property ] === value ) { index = i ; } } return index ; } /**
     * Called when a keyup event is fired on the input element.
     */ public inputKeyUp ( event ) { if ( event . which === 9 || event . keyCode === 9 || event . which === 38 || event . keyCode === 38 || event . which === 40 || event . keyCode === 40 ) { return ; } if ( this . input == null || this . input . length === 0 ) { console . debug ( `When the input is cleared` ) ; this . typeahead = '' ; this . populateSuggestions ( ) ; return ; } if ( this . selectedSuggestion != null ) { console . debug ( `If the suggestion matches the input, then return` ) ; if ( this . selectedSuggestion [ this . displayProperty ] === this . input ) { return ; } } this . previousInput = this . input ; this . populateSuggestions ( ) ; this . populateTypeahead ( ) ; } /**
     * Called when a focus event is fired on the input element.
     */ public inputFocus ( event ) { if ( this . selectedSuggestion != null ) { this . selectSuggestion ( null ) ; this . input = null ; this . populateTypeahead ( ) ; } this . populateSuggestions ( ) ; if ( this . suggestions . length > 0 ) { this . populateTypeahead ( ) ; this . areSuggestionsVisible = this . suggestions . length > 0 ; } } /**
     * Called when a blur event is fired on the input element.
     */ public inputBlur ( event ) { this . typeahead = '' ; this . areSuggestionsVisible = false ; this . onTouchedCallback ( ) ; } /**
     * Called when a mouseover event is fired on a suggestion element.
     */ public suggestionMouseOver ( suggestion ) { this . setActiveSuggestion ( suggestion ) ; } /**
     * Called when a mousedown event is fired on a suggestion element.
     */ public suggestionMouseDown ( suggestion ) { this . selectSuggestion ( suggestion ) ; } /**
     * Called when a mouseout event is fired on the suggestions element.
     */ public suggestionsMouseOut ( event ) { this . setActiveSuggestion ( null ) ; } /**
     * Fills the suggestions list with items matching the input pattern.
     */ public populateSuggestions ( ) { let searchProperty = this . searchProperty ; let input = this . input ; if ( searchProperty == null || searchProperty . length === 0 ) { console . error ( 'The input attribute `searchProperty` must be provided' ) ; return ; } if ( input == null || input . length === 0 ) { this . suggestions = [ ] ; this . areSuggestionsVisible = false ; return ; } if ( this . list == null || this . list . length === 0 ) return ; this . suggestions = this . list . filter ( function ( item ) { return item [ searchProperty ] . toLowerCase ( ) . indexOf ( input . toLowerCase ( ) ) > - 1 ; } ) ; if ( this . maxSuggestions > - 1 ) { this . suggestions = this . suggestions . slice ( 0 , this . maxSuggestions ) ; } if ( this . suggestions . length === 0 ) { this . typeahead = '' ; } else { this . populateTypeahead ( ) ; this . activeSuggestion = this . suggestions [ 0 ] ; } this . areSuggestionsVisible = this . suggestions . length > 0 ; } /**
     * Sets the typeahead input element's value based on the active suggestion.
     */ public populateTypeahead ( ) { if ( this . activeSuggestion == null || ! this . areSuggestionsVisible ) { this . typeahead = '' ; return ; } this . typeahead = this . input + ( this . activeSuggestion [ this . displayProperty ] || '' ) . slice ( this . input . length ) ; } /**
     * Selects a suggestion.
     */ public selectSuggestion ( suggestion ) { this . selectedSuggestion = suggestion ; this . areSuggestionsVisible = false ; this . suggestionSelected . emit ( suggestion ) ; if ( this . selectedSuggestion != null ) { this . input = suggestion [ this . displayProperty ] ; this . typeahead = suggestion [ this . displayProperty ] ; this . blurInputElement ( ) ; } } /**
     * Blurs the input element in order to "lock" the value and prevent partial editing.
     */ public blurInputElement ( ) { if ( this . inputElement && this . inputElement . nativeElement ) { this . inputElement . nativeElement . blur ( ) ; } } /**
     * Indicates whether a suggestion has been selected.
     */ public hasSelection ( ) { return this . selectedSuggestion != null ; } }