diff options
Diffstat (limited to 'client/src/app/header/search-typeahead.component.ts')
-rw-r--r-- | client/src/app/header/search-typeahead.component.ts | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts new file mode 100644 index 000000000..372601fa8 --- /dev/null +++ b/client/src/app/header/search-typeahead.component.ts | |||
@@ -0,0 +1,178 @@ | |||
1 | import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core' | ||
2 | import { ActivatedRoute, Params, Router } from '@angular/router' | ||
3 | import { AuthService, ServerService } from '@app/core' | ||
4 | import { first, tap } from 'rxjs/operators' | ||
5 | import { ListKeyManager } from '@angular/cdk/a11y' | ||
6 | import { Result, SuggestionComponent } from './suggestion.component' | ||
7 | import { of } from 'rxjs' | ||
8 | import { ServerConfig } from '@shared/models' | ||
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, OnDestroy { | ||
16 | @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement> | ||
17 | |||
18 | hasChannel = false | ||
19 | inChannel = false | ||
20 | newSearch = true | ||
21 | |||
22 | search = '' | ||
23 | serverConfig: ServerConfig | ||
24 | |||
25 | inThisChannelText: string | ||
26 | |||
27 | keyboardEventsManager: ListKeyManager<SuggestionComponent> | ||
28 | results: Result[] = [] | ||
29 | |||
30 | constructor ( | ||
31 | private authService: AuthService, | ||
32 | private router: Router, | ||
33 | private route: ActivatedRoute, | ||
34 | private serverService: ServerService | ||
35 | ) {} | ||
36 | |||
37 | ngOnInit () { | ||
38 | const query = this.route.snapshot.queryParams | ||
39 | if (query['search']) this.search = query['search'] | ||
40 | |||
41 | this.serverService.getConfig() | ||
42 | .subscribe(config => this.serverConfig = config) | ||
43 | } | ||
44 | |||
45 | ngOnDestroy () { | ||
46 | if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() | ||
47 | } | ||
48 | |||
49 | get activeResult () { | ||
50 | return this.keyboardEventsManager?.activeItem?.result | ||
51 | } | ||
52 | |||
53 | get areInstructionsDisplayed () { | ||
54 | return !this.search | ||
55 | } | ||
56 | |||
57 | get showHelp () { | ||
58 | return this.search && this.newSearch && this.activeResult?.type === 'search-global' | ||
59 | } | ||
60 | |||
61 | get canSearchAnyURI () { | ||
62 | if (!this.serverConfig) return false | ||
63 | return this.authService.isLoggedIn() | ||
64 | ? this.serverConfig.search.remoteUri.users | ||
65 | : this.serverConfig.search.remoteUri.anonymous | ||
66 | } | ||
67 | |||
68 | onSearchChange () { | ||
69 | this.computeResults() | ||
70 | } | ||
71 | |||
72 | computeResults () { | ||
73 | this.newSearch = true | ||
74 | let results: Result[] = [] | ||
75 | |||
76 | if (this.search) { | ||
77 | results = [ | ||
78 | /* Channel search is still unimplemented. Uncomment when it is. | ||
79 | { | ||
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 | } | ||
98 | |||
99 | this.results = results.filter( | ||
100 | (result: Result) => { | ||
101 | // if we're not in a channel or one of its videos/playlits, show all channel-related results | ||
102 | if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') | ||
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 | ) | ||
109 | } | ||
110 | |||
111 | setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) { | ||
112 | event.items.forEach(e => { | ||
113 | if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) { | ||
114 | this.keyboardEventsManager.activeItem.active = true | ||
115 | } else { | ||
116 | e.active = false | ||
117 | } | ||
118 | }) | ||
119 | } | ||
120 | |||
121 | initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) { | ||
122 | if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() | ||
123 | |||
124 | this.keyboardEventsManager = new ListKeyManager(event.items) | ||
125 | |||
126 | if (event.index !== undefined) { | ||
127 | this.keyboardEventsManager.setActiveItem(event.index) | ||
128 | } else { | ||
129 | this.keyboardEventsManager.setFirstItemActive() | ||
130 | } | ||
131 | |||
132 | this.keyboardEventsManager.change.subscribe( | ||
133 | _ => this.setEventItems(event) | ||
134 | ) | ||
135 | } | ||
136 | |||
137 | handleKeyUp (event: KeyboardEvent) { | ||
138 | event.stopImmediatePropagation() | ||
139 | if (!this.keyboardEventsManager) return | ||
140 | |||
141 | switch (event.key) { | ||
142 | case 'ArrowDown': | ||
143 | case 'ArrowUp': | ||
144 | this.keyboardEventsManager.onKeydown(event) | ||
145 | break | ||
146 | case 'Enter': | ||
147 | this.newSearch = false | ||
148 | this.doSearch() | ||
149 | break | ||
150 | } | ||
151 | } | ||
152 | |||
153 | doSearch () { | ||
154 | const queryParams: Params = {} | ||
155 | |||
156 | if (window.location.pathname === '/search' && this.route.snapshot.queryParams) { | ||
157 | Object.assign(queryParams, this.route.snapshot.queryParams) | ||
158 | } | ||
159 | |||
160 | Object.assign(queryParams, { search: this.search }) | ||
161 | |||
162 | const o = this.authService.isLoggedIn() | ||
163 | ? this.loadUserLanguagesIfNeeded(queryParams) | ||
164 | : of(true) | ||
165 | |||
166 | o.subscribe(() => this.router.navigate([ '/search' ], { queryParams })) | ||
167 | } | ||
168 | |||
169 | private loadUserLanguagesIfNeeded (queryParams: any) { | ||
170 | if (queryParams && queryParams.languageOneOf) return of(queryParams) | ||
171 | |||
172 | return this.authService.userInformationLoaded | ||
173 | .pipe( | ||
174 | first(), | ||
175 | tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages })) | ||
176 | ) | ||
177 | } | ||
178 | } | ||