]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/app/header/search-typeahead.component.ts
First implem global search
[github/Chocobozzz/PeerTube.git] / client / src / app / header / search-typeahead.component.ts
index 372601fa82c2c6a5fee8332d73143dc77aacce87..6c8b8efee30f549898792919574178f988662d9f 100644 (file)
@@ -1,23 +1,24 @@
-import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core'
+import { of } from 'rxjs'
+import { first, tap, delay } from 'rxjs/operators'
+import { ListKeyManager } from '@angular/cdk/a11y'
+import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren, AfterViewChecked } from '@angular/core'
 import { ActivatedRoute, Params, Router } from '@angular/router'
 import { AuthService, ServerService } from '@app/core'
-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 { SearchTargetType } from '@shared/models/search/search-target-query.model'
+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, AfterViewInit, AfterViewChecked, OnDestroy {
+  @ViewChildren(SuggestionComponent) suggestionItems: QueryList<SuggestionComponent>
 
   hasChannel = false
   inChannel = false
-  newSearch = true
+  areSuggestionsOpened = true
 
   search = ''
   serverConfig: ServerConfig
@@ -25,7 +26,11 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
   inThisChannelText: string
 
   keyboardEventsManager: ListKeyManager<SuggestionComponent>
-  results: Result[] = []
+  results: SuggestionPayload[] = []
+
+  activeSearch: SuggestionPayloadType
+
+  private scheduleKeyboardEventsInit = false
 
   constructor (
     private authService: AuthService,
@@ -35,129 +40,163 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
   ) {}
 
   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)
+  }
 
+  ngAfterViewInit () {
     this.serverService.getConfig()
-      .subscribe(config => this.serverConfig = config)
+      .subscribe(config => {
+        this.serverConfig = config
+
+        this.computeTypeahead()
+
+        this.serverService.configReloaded
+          .subscribe(config => {
+            this.serverConfig = config
+            this.computeTypeahead()
+          })
+      })
   }
 
-  ngOnDestroy () {
-    if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
+  ngAfterViewChecked () {
+    if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) {
+      // Avoid ExpressionChangedAfterItHasBeenCheckedError errors
+      setTimeout(() => this.initKeyboardEventsManager(), 0)
+    }
   }
 
-  get activeResult () {
-    return this.keyboardEventsManager?.activeItem?.result
+  ngOnDestroy () {
+    if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
   }
 
-  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) {
+      console.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) {
+        this.activeSearch = 'search-instance'
       } else {
-        e.active = false
+        this.activeSearch = 'search-index'
       }
-    })
+    }
+
+    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)
-    )
+  onSuggestionlicked (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
-        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)
@@ -175,4 +214,12 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
                  tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
                )
   }
+
+  private buildSearchTarget (result: SuggestionPayload): SearchTargetType {
+    if (result.type === 'search-index') {
+      return 'search-index'
+    }
+
+    return 'local'
+  }
 }