import { of } from 'rxjs' import { first, tap } from 'rxjs/operators' import { ListKeyManager } from '@angular/cdk/a11y' import { AfterViewChecked, AfterViewInit, Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core' import { ActivatedRoute, Params, Router } from '@angular/router' import { AuthService, ServerService } from '@app/core' import { SearchTargetType, ServerConfig } from '@shared/models' import { SuggestionComponent, SuggestionPayload, SuggestionPayloadType } from './suggestion.component' @Component({ selector: 'my-search-typeahead', templateUrl: './search-typeahead.component.html', styleUrls: [ './search-typeahead.component.scss' ] }) export class SearchTypeaheadComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy { @ViewChildren(SuggestionComponent) suggestionItems: QueryList hasChannel = false inChannel = false areSuggestionsOpened = true search = '' serverConfig: ServerConfig inThisChannelText: string keyboardEventsManager: ListKeyManager results: SuggestionPayload[] = [] activeSearch: SuggestionPayloadType private scheduleKeyboardEventsInit = false constructor ( private authService: AuthService, private router: Router, private route: ActivatedRoute, private serverService: ServerService ) {} ngOnInit () { this.route.queryParams .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null)) .subscribe(params => this.search = params.search) } ngAfterViewInit () { this.serverService.getConfig() .subscribe(config => { this.serverConfig = config this.computeTypeahead() this.serverService.configReloaded .subscribe(config => { this.serverConfig = config this.computeTypeahead() }) }) } ngAfterViewChecked () { if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) { // Avoid ExpressionChangedAfterItHasBeenCheckedError errors setTimeout(() => this.initKeyboardEventsManager(), 0) } } ngOnDestroy () { if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() } areInstructionsDisplayed () { return !this.search } showSearchGlobalHelp () { return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index' } canSearchAnyURI () { if (!this.serverConfig) return false return this.authService.isLoggedIn() ? this.serverConfig.search.remoteUri.users : this.serverConfig.search.remoteUri.anonymous } onSearchChange () { this.computeTypeahead() } initKeyboardEventsManager () { if (this.keyboardEventsManager) return this.keyboardEventsManager = new ListKeyManager(this.suggestionItems) const activeIndex = this.suggestionItems.toArray().findIndex(i => i.result.default === true) if (activeIndex === -1) { console.error('Cannot find active index.', { suggestionItems: this.suggestionItems }) } this.updateItemsState(activeIndex) this.keyboardEventsManager.change.subscribe( _ => this.updateItemsState() ) } computeTypeahead () { const searchIndexConfig = this.serverConfig.search.searchIndex if (!this.activeSearch) { if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) { this.activeSearch = 'search-index' } else { this.activeSearch = 'search-instance' } } this.areSuggestionsOpened = true this.results = [] if (!this.search) return if (searchIndexConfig.enabled === false || searchIndexConfig.disableLocalSearch !== true) { this.results.push({ text: this.search, type: 'search-instance', default: this.activeSearch === 'search-instance' }) } if (searchIndexConfig.enabled) { this.results.push({ text: this.search, type: 'search-index', default: this.activeSearch === 'search-index' }) } this.scheduleKeyboardEventsInit = true } updateItemsState (index?: number) { if (index !== undefined) { this.keyboardEventsManager.setActiveItem(index) } for (const item of this.suggestionItems) { if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === item) { item.active = true this.activeSearch = item.result.type continue } item.active = false } } onSuggestionlicked (payload: SuggestionPayload) { this.doSearch(this.buildSearchTarget(payload)) } onSuggestionHover (index: number) { this.updateItemsState(index) } handleKey (event: KeyboardEvent) { if (!this.keyboardEventsManager) return switch (event.key) { case 'ArrowDown': case 'ArrowUp': event.stopPropagation() this.keyboardEventsManager.onKeydown(event) break } } isOnSearch () { return window.location.pathname === '/search' } doSearch (searchTarget?: SearchTargetType) { this.areSuggestionsOpened = false const queryParams: Params = {} if (this.isOnSearch() && this.route.snapshot.queryParams) { Object.assign(queryParams, this.route.snapshot.queryParams) } if (!searchTarget) { searchTarget = this.buildSearchTarget(this.keyboardEventsManager.activeItem.result) } Object.assign(queryParams, { search: this.search, searchTarget }) const o = this.authService.isLoggedIn() ? this.loadUserLanguagesIfNeeded(queryParams) : of(true) o.subscribe(() => this.router.navigate([ '/search' ], { queryParams })) } private loadUserLanguagesIfNeeded (queryParams: any) { if (queryParams && queryParams.languageOneOf) return of(queryParams) return this.authService.userInformationLoaded .pipe( first(), tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages })) ) } private buildSearchTarget (result: SuggestionPayload): SearchTargetType { if (result.type === 'search-index') { return 'search-index' } return 'local' } }