import { BooleanInput , coerceBooleanProperty , coerceNumberProperty , NumberInput } from '@angular/cdk/coercion' ; import { ENTER } from '@angular/cdk/keycodes' ; import { Platform } from '@angular/cdk/platform' ; import { DOCUMENT } from '@angular/common' ; import { ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChild , ElementRef , EventEmitter , forwardRef , HostBinding , Inject , Input , OnChanges , OnDestroy , OnInit , Output , SimpleChanges , StaticProvider , TemplateRef , ViewChild } from '@angular/core' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { BehaviorSubject , Observable , ReplaySubject , Subject } from 'rxjs' ; import { debounceTime , delay , distinctUntilChanged , filter , map , skip , take , takeUntil } from 'rxjs/operators' ; import { InfiniteScrollLoadFunction } from '../../directives/infinite-scroll/index' ; import { TagInputComponent , TagTemplateContext } from '../tag-input/index' ; import { TypeaheadComponent , TypeaheadKeyService , TypeaheadOptionEvent } from '../typeahead/index' ; import { TypeaheadOptionContext } from '../typeahead/typeahead-option-context' ; let uniqueId = 0 ; export const SELECT_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => SelectComponent ) , multi : true } ; @ Component ( { selector : 'ux-select, ux-combobox, ux-dropdown' , templateUrl : 'select.component.html' , providers : [ SELECT_VALUE_ACCESSOR ] , host : { '[class.ux-select-custom-icon]' : '!!icon' , '[class.ux-select-disabled]' : 'disabled' } , changeDetection : ChangeDetectionStrategy . OnPush , } ) export class SelectComponent < T > implements OnInit , OnChanges , OnDestroy , ControlValueAccessor { /** A unique id for the component. */ @ Input ( ) @ HostBinding ( 'attr.id' ) id : string = `ux-select-${ ++ uniqueId }` ; /** The selected option (for single select) or array of options (for multiple select). */ @ Input ( ) set value ( value ) { this . _value$ . next ( value ) ; } get value ( ) : T | ReadonlyArray < T > { return this . _value ; } /** The text in the input area. This is used to filter the options dropdown. */ @ Input ( ) set input ( value ) { this . _input$ . next ( { ... this . _input$ . value , value } ) ; } get input ( ) { return this . _input$ . value . value ; } /** The status of the typeahead dropdown. */ @ Input ( ) set dropdownOpen ( value ) { this . _dropdownOpen = value ; this . dropdownOpenChange . emit ( value ) ; } get dropdownOpen ( ) { return this . _dropdownOpen ; } param pageNum param pageSize param filter returns @ Input ( ) options : T [ ] | InfiniteScrollLoadFunction ; /**
     * Determines the display value of the `options`, if they are custom objects. This may be a function or a string.
     * If a function is provided, it receives the option object as an argument, and should return the appropriate
     * display value. If the name of a property is provided as a string, that property is used as the display value.
     */ @ Input ( ) display : ( ( option : T ) => string ) | string ; /**
     * Determines the unique key value of the `options`, if they are custom objects. This may be a function or a string.
     * If a function is provided, it receives the option object as an argument, and should return the appropriate
     * key value. If the name of a property is provided as a string, that property is used as the key value.
     */ @ Input ( ) key : ( ( option : T ) => string ) | string ; /**
     * Controls whether the value of the single select control can be cleared by deleting the selected value in the
     * input field. This does not affect the initial state of the control, so specify a value for `value` if null should
     * never be allowed.
     */ @ Input ( ) allowNull : boolean = false ; /** The aria-label to apply to the child `input` element. */ @ Input ( ) ariaLabel : string ; /** Controls the disabled state of the tag input. */ @ Input ( ) disabled : boolean = false ; /** The positioning of the typeahead dropdown in relation to its parent. */ @ Input ( ) dropDirection : 'auto' | 'up' | 'down' = 'down' ; /** The maximum height of the typeahead dropdown, as a CSS value. */ @ Input ( ) maxHeight : string = '250px' ; /**
     * Controls whether the user can select more than one option in the select control. If set to true, selected
     * options will appear as tags in the input area. If set to false, the selected value will appear as editable text
     * in the input area.
     */ @ Input ( ) multiple : boolean = false ; /**
     * The number of options to request in a page. This should ideally be more than twice the number of items which
     * fit into the height of the dropdown, but this is not required.
     */ @ Input ( ) pageSize : number = 20 ; /** The placeholder text which appears in the text input area when it is empty. */ @ Input ( ) placeholder : string = '' ; /**
     * A template which will be rendered as the content of each selected option. The following context
     * properties are available in the template via the TagTemplateContext:
     * - `tag: T` - the string or custom object representing the selected option.
     * - `index: number` - the zero-based index of the selected option as it appears in the dropdown.
     * - `api: TagApi` - provides the functions getTagDisplay, removeTagAt and canRemoveTagAt.
     */ @ Input ( ) tagTemplate : TemplateRef < TagTemplateContext > ; @ Input ( ) optionsHeadingTemplate : TemplateRef < void > ; @ Input ( ) recentOptionsHeadingTemplate : TemplateRef < void > ; /**
     * Defines the `autocomplete` property on the `input` element which can be used to prevent the browser from
     * displaying autocomplete suggestions.
     */ @ Input ( ) autocomplete : string = 'off' ; /** A template which will be rendered in the dropdown while options are being loaded. */ @ Input ( ) loadingTemplate : TemplateRef < void > ; /** A template which will be rendered in the dropdown if no options match the current filter value. */ @ Input ( ) noOptionsTemplate : TemplateRef < void > ; /** If `true` the input field will be readonly and selection can only occur by using the dropdown. */ @ Input ( ) readonlyInput : boolean = false ; /** Determine if we should show the clear all button */ @ Input ( ) clearButton : boolean = false ; /** Determine an aria label for the clear button */ @ Input ( ) clearButtonAriaLabel : string = 'Reset selection' ; /** Determine if the dropdown panel should close on external click.*/ @ Input ( ) set autoCloseDropdown ( value ) { this . _autoCloseDropdown = coerceBooleanProperty ( value ) ; } get autoCloseDropdown ( ) : boolean { return this . _autoCloseDropdown ; } /**
     * A template which will be rendered in the dropdown for each option.
     * The following context properties are available in the template:
     * - option: any - the string or custom object representing the option.
     * - api: TypeaheadOptionApi - provides the functions `getKey`, `getDisplay` and `getDisplayHtml`.
     */ @ Input ( ) optionTemplate : TemplateRef < TypeaheadOptionContext < T > > ; /**
     * An initial list of recently selected options, to be presented above the full list of options.
     * Bind an empty array to `recentOptions` to enable this feature without providing an initial set.
     */ @ Input ( ) recentOptions : ReadonlyArray < T > ; /** Maximum number of displayed recently selected options. */ @ Input ( ) recentOptionsMaxCount : number ; /** Specified if this is a required input. */ @ Input ( ) required : boolean ; /** Specify the debounceTime value for the select filter */ @ Input ( ) get filterDebounceTime ( ) : number { return this . _filterDebounceTime ; } set filterDebounceTime ( filterDebounceTime ) { this . _filterDebounceTime = coerceNumberProperty ( filterDebounceTime ) ; } /** Emits when `value` changes. */ @ Output ( ) valueChange = new EventEmitter < T | ReadonlyArray < T > > ( ) ; /** Emits when `input` changes. */ @ Output ( ) inputChange = new EventEmitter < string > ( ) ; /** Emits when `dropdownOpen` changes. */ @ Output ( ) dropdownOpenChange = new EventEmitter < boolean > ( ) ; /** Emits when recently selected options change. */ @ Output ( ) recentOptionsChange = new EventEmitter < ReadonlyArray < T > > ( ) ; /** Allow a custom icon to be used instead of the chevron */ @ ContentChild ( 'icon' , { static : false } ) icon : TemplateRef < void > ; @ ViewChild ( 'singleInput' , { static : false } ) singleInput : ElementRef ; @ ViewChild ( 'tagInput' , { static : false } ) tagInput : TagInputComponent ; @ ViewChild ( 'multipleTypeahead' , { static : false } ) multipleTypeahead : TypeaheadComponent ; @ ViewChild ( 'singleTypeahead' , { static : false } ) singleTypeahead : TypeaheadComponent ; highlightedElement : HTMLElement ; filter$ : Observable < string > ; _value$ = new ReplaySubject < T | ReadonlyArray < T > > ( 1 ) ; _hasValue = false ; /** We need to store the most recent value*/ private _value : T | ReadonlyArray < T > ; private _input$ = new BehaviorSubject < InputValue > ( { userInteraction : false , value : '' } ) ; private _dropdownOpen : boolean = false ; private _userInput : boolean = false ; private _filterDebounceTime : number = 200 ; private _autoCloseDropdown : boolean = true ; private _onChange = ( _ ) => { } ; private _onTouched = ( ) => { } ; private _onDestroy = new Subject < void > ( ) ; constructor ( private _element , private _platform , @ Inject ( DOCUMENT ) private _document , private _typeaheadKeyService , private readonly _changeDetector ) { } ngOnInit ( ) { this . _value$ . pipe ( takeUntil ( this . _onDestroy ) , distinctUntilChanged ( ) ) . subscribe ( value => { this . _value = value ; this . _hasValue = ! ! value ; } ) ; this . _input$ . pipe ( skip ( 1 ) , filter ( ( ) => this . allowNull ) , filter ( value => ! this . multiple && value . value !== this . getDisplay ( this . value ) ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( input ) => { if ( input . userInteraction && input . value === '' ) { this . value = null ; this . _onChange ( null ) ; this . valueChange . next ( null ) ; } } ) ; this . filter$ . pipe ( filter ( ( ) => this . _userInput ) , take ( 1 ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => { this . dropdownOpen = true ; this . _userInput = false ; } ) ; this . _value$ . pipe ( distinctUntilChanged ( ) , delay ( 0 ) , filter ( value => value !== null && ! this . multiple ) , takeUntil ( this . _onDestroy ) ) . subscribe ( value => { const inputValue = this . getDisplay ( value ) ; if ( inputValue !== this . input ) { this . input = inputValue ; this . inputChange . emit ( this . input ) ; } } ) ; } ngOnChanges ( changes ) { if ( changes . multiple && ! changes . multiple . firstChange && changes . multiple . currentValue !== changes . multiple . previousValue ) { this . input = '' ; } this . filter$ = this . _input$ . pipe ( map ( input => ! this . multiple && input . value === this . getDisplay ( this . value ) ? '' : input . value ) , debounceTime ( this . filterDebounceTime ) ) ; } ngOnDestroy ( ) { this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } writeValue ( obj ) { if ( obj !== undefined && obj !== this . value ) { this . value = obj ; this . _changeDetector . markForCheck ( ) ; } } registerOnChange ( fn ) { this . _onChange = fn ; } registerOnTouched ( fn ) { this . _onTouched = fn ; } setDisabledState ( isDisabled ) { this . disabled = isDisabled ; this . _changeDetector . markForCheck ( ) ; } inputClickHandler ( ) { this . selectInputText ( ) ; this . dropdownOpen = true ; } inputBlurHandler ( ) { if ( this . singleTypeahead && this . singleTypeahead . clicking ) { this . singleInput . nativeElement . focus ( ) ; return ; } setTimeout ( ( ) => { if ( ! this . _element . nativeElement . contains ( this . _document . activeElement ) && this . _autoCloseDropdown ) { this . dropdownOpen = false ; if ( ! this . multiple ) { this . input = this . getDisplay ( this . value ) ; } } } , 200 ) ; } /**
     * Key handler for single select only. Multiple select key handling is in TagInputComponent.
     */ inputKeyHandler ( event ) { this . _typeaheadKeyService . handleKey ( event , this . singleTypeahead ) ; if ( event . keyCode === ENTER ) { if ( this . _dropdownOpen ) { this . singleTypeahead . selectHighlighted ( ) ; } else { this . dropdownOpen = true ; } this . input = this . getDisplay ( this . value ) ; event . preventDefault ( ) ; } if ( event . key . length === 1 ) { this . _userInput = true ; this . _dropdownOpen = true ; } } /** This gets called whenever the user types in the input */ onInputChange ( input ) { this . _input$ . next ( { value : input , userInteraction : true } ) ; this . inputChange . emit ( this . input ) ; } /** Whenever a single select item is selected emit the values */ _singleOptionSelected ( event ) { if ( event . option && event . option !== this . value ) { this . value = event . option ; this . dropdownOpen = false ; this . valueChange . emit ( this . value ) ; this . _onChange ( this . value ) ; } } /** Whenever a multi-select item is selected emit the values */ _multipleOptionSelected ( selection ) { this . _value$ . next ( selection ) ; this . valueChange . emit ( this . value ) ; this . _onChange ( this . value ) ; } /**
     * Returns the display value of the given option.
     */ getDisplay ( option ) { if ( option === null || option === undefined ) { return '' ; } if ( typeof this . display === 'function' ) { return this . display ( option as T ) ; } if ( typeof this . display === 'string' && typeof option === 'object' && option . hasOwnProperty ( this . display ) ) { return option [ this . display ] ; } return option as string ; } /** Toggle the dropdown open state */ toggle ( ) { if ( this . disabled ) { return ; } if ( this . dropdownOpen ) { this . dropdownOpen = false ; } else { this . inputClickHandler ( ) ; } } /** Handle input focus events */ onFocus ( ) { this . _onTouched ( ) ; if ( this . readonlyInput ) { const element = this . singleInput . nativeElement as HTMLInputElement ; this . _platform . FIREFOX ? requestAnimationFrame ( ( ) => element . setSelectionRange ( 0 , 0 ) ) : element . setSelectionRange ( 0 , 0 ) ; } } clear ( ) { if ( this . disabled ) { return ; } this . value = null ; this . input = null ; this . selectInputText ( ) ; this . valueChange . emit ( this . value ) ; this . _onChange ( this . value ) ; this . inputChange . emit ( this . input ) ; } private selectInputText ( ) { if ( ! this . readonlyInput ) { this . singleInput . nativeElement . select ( ) ; } } static ngAcceptInputType_filterDebounceTime : NumberInput ; static ngAcceptInputType_autoCloseDropdown : BooleanInput ; } interface InputValue { value : string ; userInteraction : boolean ; }