]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/header/search-typeahead.component.ts
Move to stylelint
[github/Chocobozzz/PeerTube.git] / client / src / app / header / search-typeahead.component.ts
1 import { of } from 'rxjs'
2 import { first, tap } from 'rxjs/operators'
3 import { ListKeyManager } from '@angular/cdk/a11y'
4 import { AfterViewChecked, AfterViewInit, Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'
5 import { ActivatedRoute, Params, Router } from '@angular/router'
6 import { AuthService, ServerService } from '@app/core'
7 import { SearchTargetType, ServerConfig } from '@shared/models'
8 import { SuggestionComponent, SuggestionPayload, SuggestionPayloadType } from './suggestion.component'
9
10 @Component({
11 selector: 'my-search-typeahead',
12 templateUrl: './search-typeahead.component.html',
13 styleUrls: [ './search-typeahead.component.scss' ]
14 })
15 export class SearchTypeaheadComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy {
16 @ViewChildren(SuggestionComponent) suggestionItems: QueryList<SuggestionComponent>
17
18 hasChannel = false
19 inChannel = false
20 areSuggestionsOpened = true
21
22 search = ''
23 serverConfig: ServerConfig
24
25 inThisChannelText: string
26
27 keyboardEventsManager: ListKeyManager<SuggestionComponent>
28 results: SuggestionPayload[] = []
29
30 activeSearch: SuggestionPayloadType
31
32 private scheduleKeyboardEventsInit = false
33
34 constructor (
35 private authService: AuthService,
36 private router: Router,
37 private route: ActivatedRoute,
38 private serverService: ServerService
39 ) {}
40
41 ngOnInit () {
42 this.route.queryParams
43 .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null))
44 .subscribe(params => this.search = params.search)
45 }
46
47 ngAfterViewInit () {
48 this.serverService.getConfig()
49 .subscribe(config => {
50 this.serverConfig = config
51
52 this.computeTypeahead()
53
54 this.serverService.configReloaded
55 .subscribe(config => {
56 this.serverConfig = config
57 this.computeTypeahead()
58 })
59 })
60 }
61
62 ngAfterViewChecked () {
63 if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) {
64 // Avoid ExpressionChangedAfterItHasBeenCheckedError errors
65 setTimeout(() => this.initKeyboardEventsManager(), 0)
66 }
67 }
68
69 ngOnDestroy () {
70 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
71 }
72
73 areInstructionsDisplayed () {
74 return !this.search
75 }
76
77 showSearchGlobalHelp () {
78 return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index'
79 }
80
81 canSearchAnyURI () {
82 if (!this.serverConfig) return false
83
84 return this.authService.isLoggedIn()
85 ? this.serverConfig.search.remoteUri.users
86 : this.serverConfig.search.remoteUri.anonymous
87 }
88
89 onSearchChange () {
90 this.computeTypeahead()
91 }
92
93 initKeyboardEventsManager () {
94 if (this.keyboardEventsManager) return
95
96 this.keyboardEventsManager = new ListKeyManager(this.suggestionItems)
97
98 const activeIndex = this.suggestionItems.toArray().findIndex(i => i.result.default === true)
99 if (activeIndex === -1) {
100 console.error('Cannot find active index.', { suggestionItems: this.suggestionItems })
101 }
102
103 this.updateItemsState(activeIndex)
104
105 this.keyboardEventsManager.change.subscribe(
106 _ => this.updateItemsState()
107 )
108 }
109
110 computeTypeahead () {
111 const searchIndexConfig = this.serverConfig.search.searchIndex
112
113 if (!this.activeSearch) {
114 if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) {
115 this.activeSearch = 'search-index'
116 } else {
117 this.activeSearch = 'search-instance'
118 }
119 }
120
121 this.areSuggestionsOpened = true
122 this.results = []
123
124 if (!this.search) return
125
126 if (searchIndexConfig.enabled === false || searchIndexConfig.disableLocalSearch !== true) {
127 this.results.push({
128 text: this.search,
129 type: 'search-instance',
130 default: this.activeSearch === 'search-instance'
131 })
132 }
133
134 if (searchIndexConfig.enabled) {
135 this.results.push({
136 text: this.search,
137 type: 'search-index',
138 default: this.activeSearch === 'search-index'
139 })
140 }
141
142 this.scheduleKeyboardEventsInit = true
143 }
144
145 updateItemsState (index?: number) {
146 if (index !== undefined) {
147 this.keyboardEventsManager.setActiveItem(index)
148 }
149
150 for (const item of this.suggestionItems) {
151 if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === item) {
152 item.active = true
153 this.activeSearch = item.result.type
154 continue
155 }
156
157 item.active = false
158 }
159 }
160
161 onSuggestionlicked (payload: SuggestionPayload) {
162 this.doSearch(this.buildSearchTarget(payload))
163 }
164
165 onSuggestionHover (index: number) {
166 this.updateItemsState(index)
167 }
168
169 handleKey (event: KeyboardEvent) {
170 if (!this.keyboardEventsManager) return
171
172 switch (event.key) {
173 case 'ArrowDown':
174 case 'ArrowUp':
175 event.stopPropagation()
176
177 this.keyboardEventsManager.onKeydown(event)
178 break
179 }
180 }
181
182 isOnSearch () {
183 return window.location.pathname === '/search'
184 }
185
186 doSearch (searchTarget?: SearchTargetType) {
187 this.areSuggestionsOpened = false
188 const queryParams: Params = {}
189
190 if (this.isOnSearch() && this.route.snapshot.queryParams) {
191 Object.assign(queryParams, this.route.snapshot.queryParams)
192 }
193
194 if (!searchTarget) {
195 searchTarget = this.buildSearchTarget(this.keyboardEventsManager.activeItem.result)
196 }
197
198 Object.assign(queryParams, { search: this.search, searchTarget })
199
200 const o = this.authService.isLoggedIn()
201 ? this.loadUserLanguagesIfNeeded(queryParams)
202 : of(true)
203
204 o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
205 }
206
207 private loadUserLanguagesIfNeeded (queryParams: any) {
208 if (queryParams && queryParams.languageOneOf) return of(queryParams)
209
210 return this.authService.userInformationLoaded
211 .pipe(
212 first(),
213 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
214 )
215 }
216
217 private buildSearchTarget (result: SuggestionPayload): SearchTargetType {
218 if (result.type === 'search-index') {
219 return 'search-index'
220 }
221
222 return 'local'
223 }
224 }