X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fapp%2Fheader%2Fsearch-typeahead.component.ts;h=c546628ee66fb52b820fad529aed4de46fe4dd76;hb=5c0904fc664e3eb04ac75a9430c1297c2a14f853;hp=d12a9682e0dcb11aa276c5a87dd09a8b7a5a4bd6;hpb=f409f0c3b91d85c66b4841d72ea65b5fbe1483d8;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts index d12a9682e..c546628ee 100644 --- a/client/src/app/header/search-typeahead.component.ts +++ b/client/src/app/header/search-typeahead.component.ts @@ -1,111 +1,224 @@ -import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core' -import { Router, NavigationEnd } from '@angular/router' -import { AuthService } from '@app/core' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { filter } from 'rxjs/operators' -import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y' -import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' +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 { - @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef - @ViewChild('optionsList', { static: true }) optionsList: ElementRef +export class SearchTypeaheadComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy { + @ViewChildren(SuggestionComponent) suggestionItems: QueryList hasChannel = false inChannel = false - keyboardEventsManager: ListKeyManager + areSuggestionsOpened = true - searchInput: HTMLInputElement - URIPolicy: 'only-followed' | 'any' = 'any' + search = '' + serverConfig: ServerConfig - URIPolicyText: string - inAllText: string inThisChannelText: string - results: any[] = [] + keyboardEventsManager: ListKeyManager + results: SuggestionPayload[] = [] + + activeSearch: SuggestionPayloadType + + private scheduleKeyboardEventsInit = false constructor ( private authService: AuthService, private router: Router, - private i18n: I18n - ) { - this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content from its URL, or if your instance only allows doing so for instances it follows.') - this.inAllText = this.i18n('In all PeerTube') - this.inThisChannelText = this.i18n('In this channel') - } + private route: ActivatedRoute, + private serverService: ServerService + ) {} ngOnInit () { - this.router.events - .pipe(filter(event => event instanceof NavigationEnd)) - .subscribe((event: NavigationEnd) => { - this.hasChannel = event.url.startsWith('/videos/watch') - this.inChannel = event.url.startsWith('/video-channels') - this.computeResults() - }) + this.route.queryParams + .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null)) + .subscribe(params => this.search = params.search) } ngAfterViewInit () { - this.searchInput = this.contentWrapper.nativeElement.childNodes[0] - this.searchInput.addEventListener('input', this.computeResults.bind(this)) + 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 } - get hasSearch () { - return !!this.searchInput && !!this.searchInput.value + onSearchChange () { + this.computeTypeahead() } - computeResults () { - let results = [ - { - text: 'Maître poney', - type: 'channel' + 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' } - ] - - if (this.hasSearch) { - results = [ - { - text: this.searchInput.value, - type: 'search-channel' - }, - { - text: this.searchInput.value, - type: 'search-global' - }, - ...results - ] } - this.results = results.filter( - result => { - // if we're not in a channel or one of its videos/playlits, show all channel-related results - if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') - // if we're in a channel, show all channel-related results except for the channel redirection itself - if (this.inChannel) return !(result.type === 'channel') - return true + 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 + } } - isUserLoggedIn () { - return this.authService.isLoggedIn() + onSuggestionlicked (payload: SuggestionPayload) { + this.doSearch(this.buildSearchTarget(payload)) + } + + onSuggestionHover (index: number) { + this.updateItemsState(index) } - handleKeyUp (event: KeyboardEvent) { - event.stopImmediatePropagation() - if (this.keyboardEventsManager) { - if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { - // passing the event to key manager so we get a change fired + handleKey (event: KeyboardEvent) { + if (!this.keyboardEventsManager) return + + switch (event.key) { + case 'ArrowDown': + case 'ArrowUp': + event.stopPropagation() + this.keyboardEventsManager.onKeydown(event) - return false - } else if (event.keyCode === ENTER) { - // when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent` - // this.keyboardEventsManager.activeItem - return false - } + 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' } }