import { coerceNumberProperty } from '@angular/cdk/coercion' ; import { AfterViewInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ElementRef , EventEmitter , Input , Output , TemplateRef , ViewChild } from '@angular/core' ; import { InViewportDirective } from 'ng-in-viewport' ; import { debounceable } from '../../decorators/debounceable/debounceable.decorator' ; import { CoerceBooleanProperty } from '../../utils/coerce/coerce-boolean' ; import { KeyboardKeys } from '../../enums/keyboard-keys.enum' ; import { containsFilter } from './contains-filter.util' ; import { SelectDropdownOption } from './select-dropdown-option.interface' ; @ Component ( { exportAs : 'ngxSelectDropdown' , selector : 'ngx-select-dropdown' , templateUrl : './select-dropdown.component.html' , host : { class : 'ngx-select-dropdown' , '[class.groupings]' : 'groupBy' } , changeDetection : ChangeDetectionStrategy . OnPush } ) export class SelectDropdownComponent implements AfterViewInit { @ Input ( ) selected : any [ ] ; @ Input ( ) identifier : any ; @ Input ( ) filterPlaceholder : string ; @ Input ( ) filterEmptyPlaceholder : string ; @ Input ( ) emptyPlaceholder : string ; @ Input ( ) allowAdditionsText : string | TemplateRef < any > = 'Add Value' ; @ Input ( ) @ CoerceBooleanProperty ( ) tagging : boolean ; @ Input ( ) @ CoerceBooleanProperty ( ) allowAdditions : boolean ; @ Input ( ) @ CoerceBooleanProperty ( ) filterable : boolean ; @ Input ( ) @ CoerceBooleanProperty ( ) filterCaseSensitive = false ; @ Input ( ) get focusIndex ( ) { return this . _focusIndex ; } set focusIndex ( val ) { this . _focusIndex = coerceNumberProperty ( val ) ; this . focusElement ( this . _focusIndex ) ; } @ Input ( ) get filterQuery ( ) { return this . _filterQuery ; } set filterQuery ( val ) { this . _filterQuery = val ; this . groups = this . calculateGroups ( this . groupBy , this . options , val ) ; } @ Input ( ) get groupBy ( ) { return this . _groupBy ; } set groupBy ( val ) { this . _groupBy = val ; this . groups = this . calculateGroups ( val , this . options ) ; } @ Input ( ) groupByTemplate : TemplateRef < unknown > ; @ Input ( ) get options ( ) { return this . _options ; } set options ( val ) { this . groups = this . calculateGroups ( this . groupBy , val ) ; this . _options = val ; } @ Output ( ) keyup = new EventEmitter < { event : KeyboardEvent ; value ? : string ; } > ( ) ; @ Output ( ) selection = new EventEmitter < SelectDropdownOption > ( ) ; @ Output ( ) deselection = new EventEmitter < SelectDropdownOption > ( ) ; @ Output ( ) keyboardSelection = new EventEmitter < SelectDropdownOption > ( ) ; @ Output ( ) keyboardDeselection = new EventEmitter < SelectDropdownOption > ( ) ; @ Output ( ) close = new EventEmitter < boolean | undefined > ( ) ; @ ViewChild ( 'filterInput' ) readonly filterInput ? : ElementRef < HTMLInputElement > ; @ ViewChild ( InViewportDirective ) readonly inViewport : InViewportDirective ; get element ( ) { return this . elementRef . nativeElement ; } get isNotTemplate ( ) { return ! ( typeof this . allowAdditionsText === 'object' && this . allowAdditionsText instanceof TemplateRef ) ; } groups : any [ ] ; filterQueryIsInOptions = false ; private _options : SelectDropdownOption [ ] ; private _groupBy : string ; private _filterQuery : string ; private _focusIndex : number ; constructor ( private readonly elementRef , private readonly cdr ) { } ngAfterViewInit ( ) { if ( this . filterable && ! this . tagging ) { setTimeout ( ( ) => { this . filterInput . nativeElement . focus ( ) ; } , 5 ) ; } } isSelected ( option ) { if ( ! this . selected || ! this . selected . length ) return false ; const idx = this . selected . findIndex ( o => { if ( this . identifier ) return o [ this . identifier ] === option . value [ this . identifier ] ; return o === option . value ; } ) ; return idx > - 1 ; } @ debounceable ( 500 ) updateFilterQueryIsInOptions ( ) { this . filterQueryIsInOptions = this . options . some ( o => o . name . toLowerCase ( ) === this . filterQuery . toLowerCase ( ) ) ; this . cdr . markForCheck ( ) ; } clearFilter ( filterInput ) { filterInput . value = '' ; this . filterQuery = '' ; this . updateFilterQueryIsInOptions ( ) ; this . cdr . markForCheck ( ) ; } onInputKeyUp ( event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; const key = event . key ; const value = ( event . target as any ) . value ; if ( key === ( KeyboardKeys . ESCAPE as any ) ) { this . close . emit ( true ) ; } else if ( event . key === ( KeyboardKeys . ARROW_DOWN as any ) ) { ++ this . focusIndex ; } if ( this . filterQuery !== value ) { this . filterQuery = value ; } this . updateFilterQueryIsInOptions ( ) ; this . keyup . emit ( { event , value } ) ; } onOptionClick ( option ) { if ( this . isSelected ( option ) ) { this . deselection . emit ( option ) ; } else { this . selection . emit ( option ) ; } } onOptionKeyDown ( event , option ? ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; switch ( event . code ) { case KeyboardKeys . ESCAPE : return this . close . emit ( true ) ; case KeyboardKeys . ARROW_DOWN : return this . focusNext ( ) ; case KeyboardKeys . ARROW_UP : return this . focusPrev ( ) ; case KeyboardKeys . ENTER : return this . isSelected ( option ) ? this . deselection . emit ( option ) : this . selection . emit ( option ) ; case KeyboardKeys . SPACE : return this . isSelected ( option ) ? this . keyboardDeselection . emit ( option ) : this . keyboardSelection . emit ( option ) ; } } focusNext ( ) { const options = this . options ; const len = options . length ; if ( this . focusIndex < len - 1 ) { for ( let i = this . focusIndex + 1 ; i < len ; i ++ ) { if ( ! options [ i ] . disabled && ! options [ i ] . hidden ) { this . focusIndex = i ; break ; } } } } focusPrev ( ) { const options = this . options ; if ( this . focusIndex > 0 ) { for ( let i = this . focusIndex - 1 ; i >= 0 ; i -- ) { if ( ! options [ i ] . disabled && ! options [ i ] . hidden ) { this . focusIndex = i ; break ; } } } } onAddClicked ( event , value ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; this . selection . emit ( { value , name : value } ) ; ( event . target as any ) . value = '' ; this . close . emit ( ) ; } onShiftEnterKeyDown ( event ) { if ( this . allowAdditions ) { this . onAddClicked ( event , this . filterQuery ) ; } } focusOn ( index ) { if ( index < 0 ) index = this . options . length + index ; this . focusIndex = index ; this . cdr . markForCheck ( ) ; } private focusElement ( index ) { const elements = this . element . getElementsByClassName ( 'ngx-select-dropdown-option' ) ; const element = elements [ index ] ; if ( element ) { setTimeout ( ( ) => element . focus ( ) , 5 ) ; } } private calculateGroups ( groupBy , options , filter ? ) { if ( ! options ) return [ ] ; const filterOptions = { filterCaseSensitive : this . filterCaseSensitive } ; if ( ! groupBy ) { if ( filter ) { options = options . filter ( o => { return containsFilter ( { name : o . name , value : o . value } , filter , filterOptions ) ; } ) ; } options = options . map ( ( option , index ) => { return { option , index } ; } ) ; return [ { options } ] ; } const map = new Map ( ) ; let i = 0 ; for ( const option of options ) { if ( filter && ! containsFilter ( { name : option . name , value : option . value } , filter , filterOptions ) ) { continue ; } const group = option . value [ groupBy ] ; const opt = map . get ( group ) ; const kv = { option , index : i ++ } ; if ( ! opt ) { map . set ( group , [ kv ] ) ; } else { opt . push ( kv ) ; } } const result = [ ] ; map . forEach ( ( value , key ) => { result . push ( { name : key , options : value } ) ; } ) ; return result ; } }