aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/header/search-typeahead.component.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/header/search-typeahead.component.ts')
-rw-r--r--client/src/app/header/search-typeahead.component.ts196
1 files changed, 121 insertions, 75 deletions
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts
index 2bf1072f4..6c8b8efee 100644
--- a/client/src/app/header/search-typeahead.component.ts
+++ b/client/src/app/header/search-typeahead.component.ts
@@ -1,23 +1,24 @@
1import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core' 1import { of } from 'rxjs'
2import { first, tap, delay } from 'rxjs/operators'
3import { ListKeyManager } from '@angular/cdk/a11y'
4import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren, AfterViewChecked } from '@angular/core'
2import { ActivatedRoute, Params, Router } from '@angular/router' 5import { ActivatedRoute, Params, Router } from '@angular/router'
3import { AuthService, ServerService } from '@app/core' 6import { AuthService, ServerService } from '@app/core'
4import { first, tap } from 'rxjs/operators'
5import { ListKeyManager } from '@angular/cdk/a11y'
6import { Result, SuggestionComponent } from './suggestion.component'
7import { of } from 'rxjs'
8import { ServerConfig } from '@shared/models' 7import { ServerConfig } from '@shared/models'
8import { SearchTargetType } from '@shared/models/search/search-target-query.model'
9import { SuggestionComponent, SuggestionPayload, SuggestionPayloadType } from './suggestion.component'
9 10
10@Component({ 11@Component({
11 selector: 'my-search-typeahead', 12 selector: 'my-search-typeahead',
12 templateUrl: './search-typeahead.component.html', 13 templateUrl: './search-typeahead.component.html',
13 styleUrls: [ './search-typeahead.component.scss' ] 14 styleUrls: [ './search-typeahead.component.scss' ]
14}) 15})
15export class SearchTypeaheadComponent implements OnInit, OnDestroy { 16export class SearchTypeaheadComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy {
16 @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement> 17 @ViewChildren(SuggestionComponent) suggestionItems: QueryList<SuggestionComponent>
17 18
18 hasChannel = false 19 hasChannel = false
19 inChannel = false 20 inChannel = false
20 newSearch = true 21 areSuggestionsOpened = true
21 22
22 search = '' 23 search = ''
23 serverConfig: ServerConfig 24 serverConfig: ServerConfig
@@ -25,7 +26,11 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
25 inThisChannelText: string 26 inThisChannelText: string
26 27
27 keyboardEventsManager: ListKeyManager<SuggestionComponent> 28 keyboardEventsManager: ListKeyManager<SuggestionComponent>
28 results: Result[] = [] 29 results: SuggestionPayload[] = []
30
31 activeSearch: SuggestionPayloadType
32
33 private scheduleKeyboardEventsInit = false
29 34
30 constructor ( 35 constructor (
31 private authService: AuthService, 36 private authService: AuthService,
@@ -38,109 +43,138 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
38 this.route.queryParams 43 this.route.queryParams
39 .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null)) 44 .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null))
40 .subscribe(params => this.search = params.search) 45 .subscribe(params => this.search = params.search)
46 }
47
48 ngAfterViewInit () {
41 this.serverService.getConfig() 49 this.serverService.getConfig()
42 .subscribe(config => this.serverConfig = config) 50 .subscribe(config => {
51 this.serverConfig = config
52
53 this.computeTypeahead()
54
55 this.serverService.configReloaded
56 .subscribe(config => {
57 this.serverConfig = config
58 this.computeTypeahead()
59 })
60 })
43 } 61 }
44 62
45 ngOnDestroy () { 63 ngAfterViewChecked () {
46 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() 64 if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) {
65 // Avoid ExpressionChangedAfterItHasBeenCheckedError errors
66 setTimeout(() => this.initKeyboardEventsManager(), 0)
67 }
47 } 68 }
48 69
49 get activeResult () { 70 ngOnDestroy () {
50 return this.keyboardEventsManager?.activeItem?.result 71 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
51 } 72 }
52 73
53 get areInstructionsDisplayed () { 74 areInstructionsDisplayed () {
54 return !this.search 75 return !this.search
55 } 76 }
56 77
57 get showHelp () { 78 showSearchGlobalHelp () {
58 return this.search && this.newSearch && this.activeResult?.type === 'search-global' 79 return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index'
59 } 80 }
60 81
61 get canSearchAnyURI () { 82 canSearchAnyURI () {
62 if (!this.serverConfig) return false 83 if (!this.serverConfig) return false
84
63 return this.authService.isLoggedIn() 85 return this.authService.isLoggedIn()
64 ? this.serverConfig.search.remoteUri.users 86 ? this.serverConfig.search.remoteUri.users
65 : this.serverConfig.search.remoteUri.anonymous 87 : this.serverConfig.search.remoteUri.anonymous
66 } 88 }
67 89
68 onSearchChange () { 90 onSearchChange () {
69 this.computeResults() 91 this.computeTypeahead()
70 } 92 }
71 93
72 computeResults () { 94 initKeyboardEventsManager () {
73 this.newSearch = true 95 if (this.keyboardEventsManager) return
74 let results: Result[] = [] 96
75 97 this.keyboardEventsManager = new ListKeyManager(this.suggestionItems)
76 if (this.search) { 98
77 results = [ 99 const activeIndex = this.suggestionItems.toArray().findIndex(i => i.result.default === true)
78 /* Channel search is still unimplemented. Uncomment when it is. 100 if (activeIndex === -1) {
79 { 101 console.error('Cannot find active index.', { suggestionItems: this.suggestionItems })
80 text: this.search,
81 type: 'search-channel'
82 },
83 */
84 {
85 text: this.search,
86 type: 'search-instance',
87 default: true
88 },
89 /* Global search is still unimplemented. Uncomment when it is.
90 {
91 text: this.search,
92 type: 'search-global'
93 },
94 */
95 ...results
96 ]
97 } 102 }
98 103
99 this.results = results.filter( 104 this.updateItemsState(activeIndex)
100 (result: Result) => { 105
101 // if we're not in a channel or one of its videos/playlits, show all channel-related results 106 this.keyboardEventsManager.change.subscribe(
102 if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') 107 _ => this.updateItemsState()
103 // if we're in a channel, show all channel-related results except for the channel redirection itself
104 if (this.inChannel) return result.type !== 'channel'
105 // all other result types are kept
106 return true
107 }
108 ) 108 )
109 } 109 }
110 110
111 setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) { 111 computeTypeahead () {
112 event.items.forEach(e => { 112 const searchIndexConfig = this.serverConfig.search.searchIndex
113 if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) { 113
114 this.keyboardEventsManager.activeItem.active = true 114 if (!this.activeSearch) {
115 if (searchIndexConfig.enabled && searchIndexConfig.isDefaultSearch) {
116 this.activeSearch = 'search-instance'
115 } else { 117 } else {
116 e.active = false 118 this.activeSearch = 'search-index'
117 } 119 }
118 }) 120 }
121
122 this.areSuggestionsOpened = true
123 this.results = []
124
125 if (!this.search) return
126
127 if (searchIndexConfig.enabled === false || searchIndexConfig.disableLocalSearch !== true) {
128 this.results.push({
129 text: this.search,
130 type: 'search-instance',
131 default: this.activeSearch === 'search-instance'
132 })
133 }
134
135 if (searchIndexConfig.enabled) {
136 this.results.push({
137 text: this.search,
138 type: 'search-index',
139 default: this.activeSearch === 'search-index'
140 })
141 }
142
143 this.scheduleKeyboardEventsInit = true
119 } 144 }
120 145
121 initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) { 146 updateItemsState (index?: number) {
122 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() 147 if (index !== undefined) {
148 this.keyboardEventsManager.setActiveItem(index)
149 }
123 150
124 this.keyboardEventsManager = new ListKeyManager(event.items) 151 for (const item of this.suggestionItems) {
152 if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === item) {
153 item.active = true
154 this.activeSearch = item.result.type
155 continue
156 }
125 157
126 if (event.index !== undefined) { 158 item.active = false
127 this.keyboardEventsManager.setActiveItem(event.index)
128 } else {
129 this.keyboardEventsManager.setFirstItemActive()
130 } 159 }
160 }
131 161
132 this.keyboardEventsManager.change.subscribe( 162 onSuggestionlicked (payload: SuggestionPayload) {
133 _ => this.setEventItems(event) 163 this.doSearch(this.buildSearchTarget(payload))
134 ) 164 }
165
166 onSuggestionHover (index: number) {
167 this.updateItemsState(index)
135 } 168 }
136 169
137 handleKey (event: KeyboardEvent) { 170 handleKey (event: KeyboardEvent) {
138 event.stopImmediatePropagation()
139 if (!this.keyboardEventsManager) return 171 if (!this.keyboardEventsManager) return
140 172
141 switch (event.key) { 173 switch (event.key) {
142 case 'ArrowDown': 174 case 'ArrowDown':
143 case 'ArrowUp': 175 case 'ArrowUp':
176 event.stopPropagation()
177
144 this.keyboardEventsManager.onKeydown(event) 178 this.keyboardEventsManager.onKeydown(event)
145 break 179 break
146 } 180 }
@@ -150,15 +184,19 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
150 return window.location.pathname === '/search' 184 return window.location.pathname === '/search'
151 } 185 }
152 186
153 doSearch () { 187 doSearch (searchTarget?: SearchTargetType) {
154 this.newSearch = false 188 this.areSuggestionsOpened = false
155 const queryParams: Params = {} 189 const queryParams: Params = {}
156 190
157 if (this.isOnSearch() && this.route.snapshot.queryParams) { 191 if (this.isOnSearch() && this.route.snapshot.queryParams) {
158 Object.assign(queryParams, this.route.snapshot.queryParams) 192 Object.assign(queryParams, this.route.snapshot.queryParams)
159 } 193 }
160 194
161 Object.assign(queryParams, { search: this.search }) 195 if (!searchTarget) {
196 searchTarget = this.buildSearchTarget(this.keyboardEventsManager.activeItem.result)
197 }
198
199 Object.assign(queryParams, { search: this.search, searchTarget })
162 200
163 const o = this.authService.isLoggedIn() 201 const o = this.authService.isLoggedIn()
164 ? this.loadUserLanguagesIfNeeded(queryParams) 202 ? this.loadUserLanguagesIfNeeded(queryParams)
@@ -176,4 +214,12 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy {
176 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages })) 214 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
177 ) 215 )
178 } 216 }
217
218 private buildSearchTarget (result: SuggestionPayload): SearchTargetType {
219 if (result.type === 'search-index') {
220 return 'search-index'
221 }
222
223 return 'local'
224 }
179} 225}