import { ChangeDetectorRef , Component , ElementRef , EventEmitter , Inject , Input , OnDestroy , OnInit , Output , PLATFORM_ID , TemplateRef , ViewChild , } from '@angular/core' ; import { Configuration } from '../lib-configuration' ; import { Subject } from 'rxjs' ; import { debounceTime , tap } from 'rxjs/operators' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { ISearchService , ISearchQuery , SEARCH_SERVICE_TOKEN , DEBOUNCE_TIME , DEFAULT_LIMIT , DEFAULT_LIMIT_TYPE , DEFAULT_OFFSET , DEFAULT_SAVE_IN_RECENTS , DEFAULT_ORDER , IReturnType , RecentSearchEvent , TypeEvent , ItemClickedEvent , IModel , } from '../types' ; import { isPlatformBrowser } from '@angular/common' ; const ALL_LABEL = 'All' ; @ Component ( { selector : 'sourceloop-search' , templateUrl : './search.component.html' , styleUrls : [ './search.component.scss' ] , providers : [ { provide : NG_VALUE_ACCESSOR , useExisting : SearchComponent , multi : true , } , ] , } ) export class SearchComponent < T extends IReturnType > implements OnInit , OnDestroy , ControlValueAccessor { searchBoxInput = '' ; suggestionsDisplay = false ; categoryDisplay = false ; searching = false ; suggestions : T [ ] = [ ] ; recentSearches : ISearchQuery [ ] = [ ] ; category : string = ALL_LABEL ; searchRequest$ = new Subject < { input : string ; event : Event ; } > ( ) ; private _config ! : Configuration < T > ; public get config ( ) : Configuration < T > { return this . _config ; } @ Input ( ) public set config ( value ) { this . _config = value ; if ( value && value . models ) { value . models . unshift ( { name : ALL_LABEL , displayName : ALL_LABEL , } ) ; } else if ( value && ! value . models ) { value . models = [ { name : ALL_LABEL , displayName : ALL_LABEL , } , ] ; } else { } } @ Input ( ) titleTemplate ? : TemplateRef < any > ; @ Input ( ) subtitleTemplate ? : TemplateRef < any > ; @ Output ( ) clicked = new EventEmitter < ItemClickedEvent < T > > ( ) ; @ Output ( ) searched = new EventEmitter < RecentSearchEvent > ( ) ; onChange ! : ( value : string | undefined ) => void ; onTouched ! : ( ) => void ; disabled = false ; @ ViewChild ( 'searchInput' ) public searchInputElement ! : ElementRef ; constructor ( @ Inject ( SEARCH_SERVICE_TOKEN ) private readonly searchService , @ Inject ( PLATFORM_ID ) private readonly platformId , private readonly cdr ) { } ngOnInit ( ) { this . searchRequest$ . pipe ( tap ( v => ( this . suggestions = [ ] ) ) , debounceTime ( DEBOUNCE_TIME ) ) . subscribe ( ( value ) => { this . searched . emit ( { event : value . event , keyword : value . input , category : this . category , } ) ; this . getSuggestions ( value ) ; this . cdr . markForCheck ( ) ; } ) ; } writeValue ( value ) { this . searchBoxInput = value ; } registerOnChange ( fn ) { this . onChange = fn ; } registerOnTouched ( fn ) { this . onTouched = fn ; } setDisabledState ? ( isDisabled ) { this . disabled = isDisabled ; } getSuggestions ( eventValue ) { eventValue . input = eventValue . input . trim ( ) ; if ( ! eventValue . input . length ) { return ; } const order = this . config . order ?? DEFAULT_ORDER ; let orderString = '' ; order . forEach ( preference => ( orderString = `${ orderString }${ preference } ` ) ) ; let saveInRecents = this . config . saveInRecents ?? DEFAULT_SAVE_IN_RECENTS ; if ( this . config . saveInRecents && this . config . saveInRecentsOnlyOnEnter ) { if ( ! eventValue . event || ( eventValue . event instanceof KeyboardEvent && eventValue . event . key === 'Enter' ) ) { saveInRecents = true ; } else { saveInRecents = false ; } } const requestParameters = { match : eventValue . input , sources : this . _categoryToSourceName ( this . category ) , limit : this . config . limit ?? DEFAULT_LIMIT , limitByType : this . config . limitByType ?? DEFAULT_LIMIT_TYPE , order : orderString , offset : this . config . offset ?? DEFAULT_OFFSET , } ; this . searching = true ; this . cdr . markForCheck ( ) ; this . searchService . searchApiRequest ( requestParameters , saveInRecents ) . subscribe ( ( value ) => { this . suggestions = value ; this . searching = false ; this . cdr . markForCheck ( ) ; } , ( _error ) => { this . suggestions = [ ] ; this . searching = false ; this . cdr . markForCheck ( ) ; } ) ; } getRecentSearches ( ) { if ( ! this . config . hideRecentSearch && this . searchService . recentSearchApiRequest ) { this . searchService . recentSearchApiRequest ( ) . subscribe ( ( value ) => { this . recentSearches = value ; this . cdr . markForCheck ( ) ; } , ( _error ) => { this . recentSearches = [ ] ; this . cdr . markForCheck ( ) ; } ) ; } } hitSearchApi ( event ? ) { if ( ! this . searchBoxInput ) { this . suggestions = [ ] ; this . getRecentSearches ( ) ; return ; } if ( this . config . searchOnlyOnEnter ) { if ( ! event || ( event instanceof KeyboardEvent && event . key === 'Enter' ) ) { this . getSuggestions ( { input : this . searchBoxInput , event } ) ; } return ; } if ( ! event ) { this . getSuggestions ( { input : this . searchBoxInput , event } ) ; return ; } this . searchRequest$ . next ( { input : this . searchBoxInput , event , } ) ; } populateValue ( suggestion , event ) { const value = suggestion [ this . config . displayPropertyName ] as unknown as string ; this . searchBoxInput = value ; this . suggestionsDisplay = false ; this . onChange ( this . searchBoxInput ) ; this . getSuggestions ( { input : this . searchBoxInput , event } ) ; this . clicked . emit ( { item : suggestion , event } ) ; } populateValueRecentSearch ( recentSearch , event ) { event . stopPropagation ( ) ; event . preventDefault ( ) ; const value = recentSearch [ 'match' ] ; this . searchBoxInput = value ; this . suggestionsDisplay = false ; this . onChange ( this . searchBoxInput ) ; this . getSuggestions ( { input : this . searchBoxInput , event } ) ; this . focusInput ( ) ; this . showSuggestions ( ) ; } fetchModelImageUrlFromSuggestion ( suggestion ) { const modelName = suggestion [ 'source' as unknown as keyof T ] as unknown as string ; let url ; this . config . models . forEach ( model => { if ( model . name === modelName && model . imageUrl ) { url = model . imageUrl ; } } ) ; return url ; } boldString ( str , substr ) { const strRegExp = new RegExp ( `(${ substr })` , 'gi' ) ; const stringToMakeBold = str as unknown as string ; return stringToMakeBold . replace ( strRegExp , `<b>$1</b>` ) ; } hideSuggestions ( ) { this . suggestionsDisplay = false ; this . onTouched ( ) ; } showSuggestions ( ) { this . suggestionsDisplay = true ; this . getRecentSearches ( ) ; } focusInput ( ) { if ( isPlatformBrowser ( this . platformId ) ) { this . searchInputElement . nativeElement . focus ( ) ; } } setCategory ( category ) { this . category = category ; this . categoryDisplay = false ; if ( this . searchBoxInput ) { this . hitSearchApi ( ) ; this . focusInput ( ) ; this . showSuggestions ( ) ; } } showCategory ( ) { this . categoryDisplay = ! this . categoryDisplay ; } hideCategory ( ) { this . categoryDisplay = false ; } resetInput ( ) { this . searchBoxInput = '' ; this . suggestions = [ ] ; this . suggestionsDisplay = true ; this . focusInput ( ) ; this . onChange ( this . searchBoxInput ) ; this . getRecentSearches ( ) ; } ngOnDestroy ( ) { this . searchRequest$ . unsubscribe ( ) ; } _categoryToSourceName ( category ) { if ( category === ALL_LABEL ) { return [ ] ; } else { return [ category ] ; } } getModelFromModelName ( name ) { return this . config . models . find ( item => item . name === name ) as IModel ; } getModelsWithSuggestions ( ) { const modelsWithSuggestions = [ ] ; const sources = [ ] ; this . suggestions . forEach ( suggestion => { if ( sources . indexOf ( suggestion [ 'source' ] ) >= 0 ) { modelsWithSuggestions . every ( modelWithSuggestions => { if ( modelWithSuggestions . model . name === suggestion [ 'source' ] ) { modelWithSuggestions . items . push ( suggestion ) ; return false ; } return true ; } ) ; } else { const model = this . getModelFromModelName ( suggestion [ 'source' ] ) ; modelsWithSuggestions . push ( { model , items : [ suggestion ] } ) ; sources . push ( suggestion [ 'source' ] ) ; } } ) ; return modelsWithSuggestions ; } }