-import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core'
-import { ActivatedRoute, Params, Router } from '@angular/router'
-import { AuthService, ServerService } from '@app/core'
+import { of } from 'rxjs'
import { first, tap } from 'rxjs/operators'
import { ListKeyManager } from '@angular/cdk/a11y'
-import { Result, SuggestionComponent } from './suggestion.component'
-import { of } from 'rxjs'
-import { ServerConfig } from '@shared/models'
+import { AfterViewChecked, Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'
+import { ActivatedRoute, Params, Router } from '@angular/router'
+import { AuthService, ServerService } from '@app/core'
+import { logger } from '@root-helpers/logger'
+import { HTMLServerConfig, SearchTargetType } 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, OnDestroy {
- @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
+export class SearchTypeaheadComponent implements OnInit, AfterViewChecked, OnDestroy {
+ @ViewChildren(SuggestionComponent) suggestionItems: QueryList<SuggestionComponent>
hasChannel = false
inChannel = false
- newSearch = true
+ areSuggestionsOpened = true
search = ''
- serverConfig: ServerConfig
+ serverConfig: HTMLServerConfig
inThisChannelText: string
keyboardEventsManager: ListKeyManager<SuggestionComponent>
- results: Result[] = []
+ results: SuggestionPayload[] = []
+
+ activeSearch: SuggestionPayloadType
+
+ private scheduleKeyboardEventsInit = false
constructor (
private authService: AuthService,
) {}
ngOnInit () {
- const query = this.route.snapshot.queryParams
- if (query['search']) this.search = query['search']
+ this.route.queryParams
+ .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null))
+ .subscribe(params => this.search = params.search)
+
+ this.serverConfig = this.serverService.getHTMLConfig()
+ this.computeTypeahead()
+
+ this.serverService.configReloaded
+ .subscribe(config => {
+ this.serverConfig = config
+ this.computeTypeahead()
+ })
+ }
- this.serverService.getConfig()
- .subscribe(config => this.serverConfig = config)
+ ngAfterViewChecked () {
+ if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) {
+ // Avoid ExpressionChangedAfterItHasBeenCheckedError errors
+ setTimeout(() => this.initKeyboardEventsManager(), 0)
+ }
}
ngOnDestroy () {
if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
}
- get activeResult () {
- return this.keyboardEventsManager?.activeItem?.result
- }
-
- get areInstructionsDisplayed () {
+ areInstructionsDisplayed () {
return !this.search
}
- get showHelp () {
- return this.search && this.newSearch && this.activeResult?.type === 'search-global'
+ showSearchGlobalHelp () {
+ return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index'
}
- get canSearchAnyURI () {
+ canSearchAnyURI () {
if (!this.serverConfig) return false
+
return this.authService.isLoggedIn()
? this.serverConfig.search.remoteUri.users
: this.serverConfig.search.remoteUri.anonymous
}
onSearchChange () {
- this.computeResults()
- }
-
- computeResults () {
- this.newSearch = true
- let results: Result[] = []
-
- if (this.search) {
- results = [
- /* Channel search is still unimplemented. Uncomment when it is.
- {
- text: this.search,
- type: 'search-channel'
- },
- */
- {
- text: this.search,
- type: 'search-instance',
- default: true
- },
- /* Global search is still unimplemented. Uncomment when it is.
- {
- text: this.search,
- type: 'search-global'
- },
- */
- ...results
- ]
+ 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) {
+ logger.error('Cannot find active index.', { suggestionItems: this.suggestionItems })
}
- this.results = results.filter(
- (result: 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'
- // all other result types are kept
- return true
- }
+ this.updateItemsState(activeIndex)
+
+ this.keyboardEventsManager.change.subscribe(
+ _ => this.updateItemsState()
)
}
- setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
- event.items.forEach(e => {
- if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
- this.keyboardEventsManager.activeItem.active = true
+ computeTypeahead () {
+ const searchIndexConfig = this.serverConfig.search.searchIndex
+
+ if (!this.activeSearch) {
+ if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) {
+ this.activeSearch = 'search-index'
} else {
- e.active = false
+ 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
}
- initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
- if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
+ updateItemsState (index?: number) {
+ if (index !== undefined) {
+ this.keyboardEventsManager.setActiveItem(index)
+ }
- this.keyboardEventsManager = new ListKeyManager(event.items)
+ for (const item of this.suggestionItems) {
+ if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === item) {
+ item.active = true
+ this.activeSearch = item.result.type
+ continue
+ }
- if (event.index !== undefined) {
- this.keyboardEventsManager.setActiveItem(event.index)
- } else {
- this.keyboardEventsManager.setFirstItemActive()
+ item.active = false
}
+ }
- this.keyboardEventsManager.change.subscribe(
- _ => this.setEventItems(event)
- )
+ onSuggestionClicked (payload: SuggestionPayload) {
+ this.doSearch(this.buildSearchTarget(payload))
}
- handleKeyUp (event: KeyboardEvent) {
- event.stopImmediatePropagation()
+ 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
+
case 'Enter':
- this.newSearch = false
+ event.stopPropagation()
this.doSearch()
break
}
}
- doSearch () {
+ isOnSearch () {
+ return window.location.pathname === '/search'
+ }
+
+ doSearch (searchTarget?: SearchTargetType) {
+ this.areSuggestionsOpened = false
const queryParams: Params = {}
- if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
+ if (this.isOnSearch() && this.route.snapshot.queryParams) {
Object.assign(queryParams, this.route.snapshot.queryParams)
}
- Object.assign(queryParams, { search: this.search })
+ if (!searchTarget) {
+ searchTarget = this.buildSearchTarget(this.keyboardEventsManager.activeItem.result)
+ }
+
+ Object.assign(queryParams, { search: this.search, searchTarget })
const o = this.authService.isLoggedIn()
? this.loadUserLanguagesIfNeeded(queryParams)
}
private loadUserLanguagesIfNeeded (queryParams: any) {
- if (queryParams && queryParams.languageOneOf) return of(queryParams)
+ if (queryParams?.languageOneOf) return of(queryParams)
return this.authService.userInformationLoaded
.pipe(
tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
)
}
+
+ private buildSearchTarget (result: SuggestionPayload): SearchTargetType {
+ if (result.type === 'search-index') {
+ return 'search-index'
+ }
+
+ return 'local'
+ }
}