aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/header
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-05-29 16:16:24 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-06-10 14:02:41 +0200
commit5fb2e2888ce032c638e4b75d07458642f0833e52 (patch)
tree8830d873569316889b8134027e9a43b198cca38f /client/src/app/header
parent62e7be634bc189f942ae51cb4b080079ab503ff0 (diff)
downloadPeerTube-5fb2e2888ce032c638e4b75d07458642f0833e52.tar.gz
PeerTube-5fb2e2888ce032c638e4b75d07458642f0833e52.tar.zst
PeerTube-5fb2e2888ce032c638e4b75d07458642f0833e52.zip
First implem global search
Diffstat (limited to 'client/src/app/header')
-rw-r--r--client/src/app/header/index.ts1
-rw-r--r--client/src/app/header/search-typeahead.component.html41
-rw-r--r--client/src/app/header/search-typeahead.component.scss8
-rw-r--r--client/src/app/header/search-typeahead.component.ts196
-rw-r--r--client/src/app/header/suggestion.component.html21
-rw-r--r--client/src/app/header/suggestion.component.ts22
-rw-r--r--client/src/app/header/suggestions.component.html6
-rw-r--r--client/src/app/header/suggestions.component.ts24
8 files changed, 165 insertions, 154 deletions
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts
index a882d4d1f..005e0c97d 100644
--- a/client/src/app/header/index.ts
+++ b/client/src/app/header/index.ts
@@ -1,4 +1,3 @@
1export * from './header.component' 1export * from './header.component'
2export * from './search-typeahead.component' 2export * from './search-typeahead.component'
3export * from './suggestions.component'
4export * from './suggestion.component' 3export * from './suggestion.component'
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index bbf3834c5..4355b67af 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -1,38 +1,43 @@
1<div class="d-inline-flex position-relative" id="typeahead-container"> 1<div class="d-inline-flex position-relative" id="typeahead-container">
2 <input 2 <input
3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…" 3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKey($event)" (keydown.enter)="doSearch()" 4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)" (keydown.enter)="doSearch()"
5 aria-label="Search" 5 aria-label="Search" autocomplete="off"
6 > 6 >
7 <span class="icon icon-search" (click)="doSearch()"></span> 7 <span class="icon icon-search" (click)="doSearch()"></span>
8 8
9 <div class="position-absolute jump-to-suggestions"> 9 <div class="position-absolute jump-to-suggestions">
10 <!-- suggestions --> 10
11 <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions> 11 <ul [hidden]="!search || !areSuggestionsOpened" role="listbox" class="p-0 m-0">
12 <li
13 *ngFor="let result of results; let i = index" class="suggestion d-flex flex-justify-start flex-items-center p-0 f5"
14 role="option" aria-selected="true" (mouseenter)="onSuggestionHover(i)" (click)="onSuggestionlicked(result)"
15 >
16 <my-suggestion [result]="result" [highlight]="search"></my-suggestion>
17 </li>
18 </ul>
12 19
13 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> 20 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
14 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden"> 21 <div *ngIf="showSearchGlobalHelp()" id="typeahead-help" class="overflow-hidden">
15 <ng-container *ngIf="activeResult.type === 'search-global'"> 22 <div class="d-flex justify-content-between">
16 <div class="d-flex justify-content-between"> 23 <label class="small-title" i18n>GLOBAL SEARCH</label>
17 <label class="small-title" i18n>GLOBAL SEARCH</label> 24 <div class="advanced-search-status text-muted">
18 <div class="advanced-search-status text-muted"> 25 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.search.searchIndex.url }}</span>
19 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span> 26 <i class="glyphicon glyphicon-globe"></i>
20 <i class="glyphicon glyphicon-globe"></i>
21 </div>
22 </div> 27 </div>
23 <div class="text-muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div> 28 </div>
24 </ng-container> 29 <div class="text-muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div>
25 </div> 30 </div>
26 31
27 <!-- search instructions, when search input is empty --> 32 <!-- search instructions, when search input is empty -->
28 <div *ngIf="areInstructionsDisplayed" id="typeahead-instructions" class="overflow-hidden"> 33 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden">
29 <div class="d-flex justify-content-between"> 34 <div class="d-flex justify-content-between">
30 <label class="small-title" i18n>ADVANCED SEARCH</label> 35 <label class="small-title" i18n>ADVANCED SEARCH</label>
31 <div class="advanced-search-status c-help"> 36 <div class="advanced-search-status c-help">
32 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> 37 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
33 <span *ngIf="canSearchAnyURI" class="mr-1" i18n>any instance</span> 38 <span *ngIf="canSearchAnyURI()" class="mr-1" i18n>any instance</span>
34 <span *ngIf="!canSearchAnyURI" class="mr-1" i18n>only followed instances</span> 39 <span *ngIf="!canSearchAnyURI()" class="mr-1" i18n>only followed instances</span>
35 <i [ngClass]="canSearchAnyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> 40 <i [ngClass]="canSearchAnyURI() ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
36 </span> 41 </span>
37 </div> 42 </div>
38 </div> 43 </div>
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
index 0a30ebd55..4b56fd93a 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -36,7 +36,7 @@
36 36
37#typeahead-help, 37#typeahead-help,
38#typeahead-instructions, 38#typeahead-instructions,
39my-suggestions ::ng-deep ul { 39li.suggestion {
40 border: 1px solid pvar(--mainBackgroundColor); 40 border: 1px solid pvar(--mainBackgroundColor);
41 border-bottom-right-radius: 3px; 41 border-bottom-right-radius: 3px;
42 border-bottom-left-radius: 3px; 42 border-bottom-left-radius: 3px;
@@ -90,7 +90,7 @@ my-suggestions ::ng-deep ul {
90 } 90 }
91 91
92 & > div:last-child { 92 & > div:last-child {
93 // we have to switch the display and not the opacity, 93 // we have to switch the display and not the opacity,
94 // to avoid clashing with the rest of the interface. 94 // to avoid clashing with the rest of the interface.
95 display: none; 95 display: none;
96 } 96 }
@@ -101,10 +101,10 @@ my-suggestions ::ng-deep ul {
101 @media screen and (min-width: $mobile-view) { 101 @media screen and (min-width: $mobile-view) {
102 display: initial !important; 102 display: initial !important;
103 } 103 }
104 104
105 #typeahead-help, 105 #typeahead-help,
106 #typeahead-instructions, 106 #typeahead-instructions,
107 my-suggestions ::ng-deep ul { 107 li.suggestion {
108 box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px; 108 box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
109 } 109 }
110 } 110 }
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}
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
index d7ae3450a..ab4b4b678 100644
--- a/client/src/app/header/suggestion.component.html
+++ b/client/src/app/header/suggestion.component.html
@@ -1,22 +1,17 @@
1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active"> 1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active">
2 <div class="flex-shrink-0 mr-2 text-center"> 2 <div class="flex-shrink-0 mr-2 text-center">
3 <my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon> 3 <my-global-icon iconName="search"></my-global-icon>
4 <my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
5 </div> 4 </div>
6 5
7 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28"> 6 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
8 7
9 <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div> 8 <div
9 class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target"
10 [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"
11 ></div>
10 12
11 <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6"> 13 <div class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
12 <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
13 <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span> 14 <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
14 <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span> 15 <span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span>
15 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
16 </div> 16 </div>
17 17</a>
18 <div *ngIf="result.type === 'channel'" aria-hidden="true" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6 d-on-nav-focus" i18n>
19 Jump to channel
20 <span class="d-inline-block ml-1 v-align-middle">↵</span>
21 </div>
22</a> \ No newline at end of file
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts
index 69641b511..250a5411e 100644
--- a/client/src/app/header/suggestion.component.ts
+++ b/client/src/app/header/suggestion.component.ts
@@ -1,24 +1,24 @@
1import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' 1import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy, OnChanges } from '@angular/core'
2import { RouterLink } from '@angular/router' 2import { RouterLink } from '@angular/router'
3import { ListKeyManagerOption } from '@angular/cdk/a11y' 3import { ListKeyManagerOption } from '@angular/cdk/a11y'
4 4
5export type Result = { 5export type SuggestionPayload = {
6 text: string 6 text: string
7 type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' 7 type: SuggestionPayloadType
8 routerLink?: RouterLink, 8 routerLink?: RouterLink
9 default?: boolean 9 default: boolean
10} 10}
11 11
12export type SuggestionPayloadType = 'search-instance' | 'search-index'
13
12@Component({ 14@Component({
13 selector: 'my-suggestion', 15 selector: 'my-suggestion',
14 templateUrl: './suggestion.component.html', 16 templateUrl: './suggestion.component.html',
15 styleUrls: [ './suggestion.component.scss' ], 17 styleUrls: [ './suggestion.component.scss' ]
16 changeDetection: ChangeDetectionStrategy.OnPush
17}) 18})
18export class SuggestionComponent implements OnInit, ListKeyManagerOption { 19export class SuggestionComponent implements OnInit, ListKeyManagerOption {
19 @Input() result: Result 20 @Input() result: SuggestionPayload
20 @Input() highlight: string 21 @Input() highlight: string
21 @Output() selected = new EventEmitter()
22 22
23 disabled = false 23 disabled = false
24 active = false 24 active = false
@@ -30,8 +30,4 @@ export class SuggestionComponent implements OnInit, ListKeyManagerOption {
30 ngOnInit () { 30 ngOnInit () {
31 if (this.result.default) this.active = true 31 if (this.result.default) this.active = true
32 } 32 }
33
34 selectItem () {
35 this.selected.emit(this.result)
36 }
37} 33}
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html
deleted file mode 100644
index 8d017d78d..000000000
--- a/client/src/app/header/suggestions.component.html
+++ /dev/null
@@ -1,6 +0,0 @@
1<ul role="listbox" class="p-0 m-0">
2 <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
3 role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
4 <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
5 </li>
6</ul> \ No newline at end of file
diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts
deleted file mode 100644
index ee3ef73c2..000000000
--- a/client/src/app/header/suggestions.component.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
2import { SuggestionComponent } from './suggestion.component'
3
4@Component({
5 selector: 'my-suggestions',
6 templateUrl: './suggestions.component.html',
7 changeDetection: ChangeDetectionStrategy.OnPush
8})
9export class SuggestionsComponent implements AfterViewInit {
10 @Input() results: any[]
11 @Input() highlight: string
12 @ViewChildren(SuggestionComponent) listItems: QueryList<SuggestionComponent>
13 @Output() init = new EventEmitter()
14
15 ngAfterViewInit () {
16 this.listItems.changes.subscribe(
17 _ => this.init.emit({ items: this.listItems })
18 )
19 }
20
21 hoverItem (index: number) {
22 this.init.emit({ items: this.listItems, index: index })
23 }
24}